Repository: SagerNet/sing-box Branch: testing Commit: c5bf6b3e71b6 Files: 1022 Total size: 3.6 MB Directory structure: gitextract_d_trzmkv/ ├── .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 │ ├── 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 ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── Dockerfile ├── Dockerfile.binary ├── LICENSE ├── Makefile ├── README.md ├── adapter/ │ ├── certificate.go │ ├── connections.go │ ├── dns.go │ ├── endpoint/ │ │ ├── adapter.go │ │ ├── manager.go │ │ └── registry.go │ ├── endpoint.go │ ├── experimental.go │ ├── fakeip.go │ ├── fakeip_metadata.go │ ├── handler.go │ ├── inbound/ │ │ ├── adapter.go │ │ ├── manager.go │ │ └── registry.go │ ├── inbound.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 │ ├── 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_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/ │ │ ├── chrome.go │ │ ├── mozilla.go │ │ └── store.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 │ ├── 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 │ ├── pipelistener/ │ │ └── listener.go │ ├── process/ │ │ ├── searcher.go │ │ ├── searcher_android.go │ │ ├── searcher_darwin.go │ │ ├── searcher_linux.go │ │ ├── searcher_linux_shared.go │ │ ├── searcher_stub.go │ │ └── searcher_windows.go │ ├── redir/ │ │ ├── redir_darwin.go │ │ ├── redir_linux.go │ │ ├── redir_other.go │ │ ├── tproxy_linux.go │ │ └── tproxy_other.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 │ ├── taskmonitor/ │ │ └── monitor.go │ ├── tls/ │ │ ├── acme.go │ │ ├── acme_contstant.go │ │ ├── acme_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 │ │ ├── time_wrapper.go │ │ ├── utls_client.go │ │ └── utls_stub.go │ ├── tlsfragment/ │ │ ├── conn.go │ │ ├── conn_test.go │ │ ├── index.go │ │ ├── index_test.go │ │ ├── wait_darwin.go │ │ ├── wait_linux.go │ │ ├── wait_stub.go │ │ └── wait_windows.go │ ├── uot/ │ │ └── router.go │ └── urltest/ │ └── urltest.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 │ ├── 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 │ ├── router.go │ ├── transport/ │ │ ├── base.go │ │ ├── connector.go │ │ ├── connector_test.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.go │ │ │ ├── local_darwin_dhcp.go │ │ │ ├── local_darwin_nodhcp.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 │ │ ├── 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 │ │ │ ├── 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 │ │ │ ├── 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 │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── ocm.md │ │ │ ├── ocm.zh.md │ │ │ ├── resolved.md │ │ │ ├── resolved.zh.md │ │ │ ├── ssm-api.md │ │ │ └── ssm-api.zh.md │ │ └── shared/ │ │ ├── dial.md │ │ ├── dial.zh.md │ │ ├── dns01_challenge.md │ │ ├── dns01_challenge.zh.md │ │ ├── listen.md │ │ ├── listen.zh.md │ │ ├── multiplex.md │ │ ├── multiplex.zh.md │ │ ├── neighbor.md │ │ ├── neighbor.zh.md │ │ ├── pre-match.md │ │ ├── pre-match.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 │ │ ├── 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 │ │ ├── config.go │ │ ├── deprecated.go │ │ ├── dns.go │ │ ├── fdroid.go │ │ ├── fdroid_mirrors.go │ │ ├── ffi.json │ │ ├── http.go │ │ ├── internal/ │ │ │ └── procfs/ │ │ │ └── procfs.go │ │ ├── iterator.go │ │ ├── link_flags_stub.go │ │ ├── link_flags_unix.go │ │ ├── log.go │ │ ├── memory.go │ │ ├── monitor.go │ │ ├── neighbor.go │ │ ├── neighbor_darwin.go │ │ ├── neighbor_linux.go │ │ ├── neighbor_stub.go │ │ ├── panic.go │ │ ├── pidfd_android.go │ │ ├── platform.go │ │ ├── pprof.go │ │ ├── profile_import.go │ │ ├── remote_profile.go │ │ ├── semver.go │ │ ├── semver_test.go │ │ ├── service.go │ │ ├── service_other.go │ │ ├── service_windows.go │ │ ├── setup.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/ │ ├── ccm.go │ ├── ccm_stub.go │ ├── ccm_stub_darwin.go │ ├── clashapi.go │ ├── clashapi_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/ │ ├── anytls.go │ ├── ccm.go │ ├── certificate.go │ ├── debug.go │ ├── direct.go │ ├── dns.go │ ├── dns_record.go │ ├── endpoint.go │ ├── experimental.go │ ├── group.go │ ├── hysteria.go │ ├── hysteria2.go │ ├── inbound.go │ ├── multiplex.go │ ├── naive.go │ ├── ntp.go │ ├── ocm.go │ ├── oom_killer.go │ ├── options.go │ ├── outbound.go │ ├── platform.go │ ├── redir.go │ ├── resolved.go │ ├── route.go │ ├── rule.go │ ├── rule_action.go │ ├── rule_dns.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 │ ├── direct/ │ │ ├── inbound.go │ │ ├── loopback_detect.go │ │ └── outbound.go │ ├── dns/ │ │ ├── handle.go │ │ └── outbound.go │ ├── group/ │ │ ├── selector.go │ │ └── urltest.go │ ├── http/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── hysteria/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── hysteria2/ │ │ ├── inbound.go │ │ └── outbound.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/ │ │ ├── dns_transport.go │ │ ├── endpoint.go │ │ ├── tun_device_unix.go │ │ └── tun_device_windows.go │ ├── tor/ │ │ ├── outbound.go │ │ └── proxy.go │ ├── trojan/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── tuic/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── tun/ │ │ ├── hook.go │ │ └── 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_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 │ ├── route.go │ ├── router.go │ ├── rule/ │ │ ├── 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_port.go │ │ ├── rule_item_port_range.go │ │ ├── rule_item_preferred_by.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_rule_set.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_network_interface_address.go │ │ ├── rule_set.go │ │ ├── rule_set_local.go │ │ └── rule_set_remote.go │ └── rule_conds.go ├── service/ │ ├── 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/ │ │ ├── config.go │ │ ├── service.go │ │ ├── service_stub.go │ │ └── service_timer.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/" --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 ================================================ ea7cd33752aed62603775af3df946c1b83f4b0b3 ================================================ 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 ARCHITECTURE="$1" VERSION="$2" BINARY_PATH="$3" OUTPUT_PATH="$4" if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then echo "Usage: $0 " 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) trap 'rm -rf "$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 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 ARCHITECTURE="$1" VERSION="$2" BINARY_PATH="$3" OUTPUT_PATH="$4" if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then echo "Usage: $0 " 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) trap 'rm -rf "$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 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/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.8" 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.8" 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.8 - 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.8 - 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.8 - 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.8 - 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.8 - 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 if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable' 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.8 - 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 if [[ $ref == *"-"* ]]; then latest=latest-beta else latest=latest fi echo "latest=$latest" echo "latest=$latest" >> $GITHUB_OUTPUT - 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 }}:${{ steps.ref.outputs.latest }}" \ -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 }}:${{ steps.ref.outputs.latest }} 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 jobs: build: name: Build runs-on: ubuntu-latest 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 - name: golangci-lint uses: golangci/golangci-lint-action@v8 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 forceBeta: description: "Force beta" required: false type: boolean default: false release: types: - published jobs: calculate_version: name: Calculate version if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable' 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.8 - 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.8 - 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: Set name if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta run: |- echo "NAME=sing-box" >> "$GITHUB_ENV" - name: Set beta name if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta run: |- echo "NAME=sing-box-beta" >> "$GITHUB_ENV" - 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: .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.25" 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: - govet - ineffassign - paralleltest - staticcheck settings: staticcheck: checks: - all - -S1000 - -S1008 - -S1017 - -ST1003 - -QF1001 - -QF1003 - -QF1008 exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - transport/simple-obfs - third_party$ - builtin$ - examples$ formatters: enable: - gci - gofumpt settings: gci: sections: - standard - prefix(github.com/sagernet/) - default custom-order: true exclusions: generated: lax paths: - transport/simple-obfs - third_party$ - builtin$ - examples$ ================================================ 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: @gofumpt -l -w . @gofmt -s -w . @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . fmt_docs: go run ./cmd/internal/format_docs fmt_install: go install -v mvdan.cc/gofumpt@latest go install -v github.com/daixiang0/gci@latest 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" 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" release_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 ================================================ > Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents Warp sponsorship --- # 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.go ================================================ package adapter import ( "context" "crypto/x509" "github.com/sagernet/sing/service" ) type CertificateStore interface { LifecycleService Pool() *x509.CertPool } func RootPoolFromContext(ctx context.Context) *x509.CertPool { store := service.FromContext[CertificateStore](ctx) if store == nil { return nil } return store.Pool() } ================================================ 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" 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(responseAddrs []netip.Addr) bool) (*dns.Msg, error) Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) ClearCache() } type DNSQueryOptions struct { Transport DNSTransport Strategy C.DomainStrategy LookupStrategy C.DomainStrategy DisableCache bool RewriteTTL *uint32 ClientSubnet netip.Prefix } func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { if options == nil { return &DNSQueryOptions{}, nil } transportManager := service.FromContext[DNSTransportManager](ctx) transport, loaded := transportManager.Transport(options.Server) if !loaded { return nil, E.New("domain resolver not found: " + options.Server) } return &DNSQueryOptions{ Transport: transport, Strategy: C.DomainStrategy(options.Strategy), DisableCache: options.DisableCache, RewriteTTL: options.RewriteTTL, 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 DNSTransport interface { Lifecycle Type() string Tag() string Dependencies() []string Reset() Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) } type LegacyDNSTransport interface { LegacyStrategy() C.DomainStrategy LegacyClientSubnet() netip.Prefix } type DNSTransportRegistry interface { option.DNSTransportOptionsRegistry CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error) } 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" "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.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() + "]" m.logger.Trace(stage, " ", name) startTime := time.Now() err := adapter.LegacyStart(endpoint, 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 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() + "]" m.logger.Trace("close ", name) startTime := time.Now() monitor.Start("close ", name) err = E.Append(err, endpoint.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 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 { m.logger.Trace(stage, " ", name) startTime := time.Now() err = adapter.LegacyStart(endpoint, stage) if err != nil { return E.Cause(err, stage, " ", name) } m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } } 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 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" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) // Deprecated type ConnectionHandler interface { NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error } type ConnectionHandlerEx interface { NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) } // Deprecated: use PacketHandlerEx instead type PacketHandler interface { NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error } type PacketHandlerEx interface { NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) } // Deprecated: use OOBPacketHandlerEx instead type OOBPacketHandler interface { NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error } type OOBPacketHandlerEx interface { NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) } // Deprecated type PacketConnectionHandler interface { NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error } type PacketConnectionHandlerEx interface { NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } // Deprecated: use TCPConnectionHandlerEx instead // //nolint:staticcheck type UpstreamHandlerAdapter interface { N.TCPConnectionHandler N.UDPConnectionHandler E.Handler } type UpstreamHandlerAdapterEx interface { N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } ================================================ 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" "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.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() + "]" m.logger.Trace(stage, " ", name) startTime := time.Now() err := adapter.LegacyStart(inbound, 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 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() + "]" m.logger.Trace("close ", name) startTime := time.Now() monitor.Start("close ", name) err = E.Append(err, inbound.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 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 { m.logger.Trace(stage, " ", name) startTime := time.Now() err = adapter.LegacyStart(inbound, stage) if err != nil { return E.Cause(err, stage, " ", name) } m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } } 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" 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" ) type Inbound interface { Lifecycle Type() string Tag() string } type TCPInjectableInbound interface { Inbound ConnectionHandlerEx } type UDPInjectableInbound interface { Inbound PacketConnectionHandlerEx } 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 NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration DestinationAddresses []netip.Addr 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.SourceAddressMatch = false c.SourcePortMatch = false c.DestinationAddressMatch = false c.DestinationPortMatch = false c.DidMatch = false } 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/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) logger.Trace(stage, " ", name) startTime := time.Now() err := service.Start(stage) if err != nil { return err } logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } return nil } func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error { for _, service := range services { logger.Trace(stage, " ", service.Name()) startTime := time.Now() err := service.Start(stage) if err != nil { return E.Cause(err, stage.String(), " ", service.Name()) } logger.Trace(stage, " ", service.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } return nil } ================================================ 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) Start() error Close() error } type NeighborUpdateListener interface { UpdateNeighborTable(entries []NeighborEntry) } ================================================ FILE: adapter/network.go ================================================ package adapter import ( "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 } 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" "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" "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() + "]" m.logger.Trace(stage, " ", name) startTime := time.Now() err := adapter.LegacyStart(outbound, 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) 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 { m.logger.Trace("start ", name) startTime := time.Now() monitor.Start("start ", name) err := starter.Start(adapter.StartStateStart) monitor.Finish() if err != nil { return E.Cause(err, "start ", name) } m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } else if starter, isStarter := outboundToStart.(interface { Start() error }); isStarter { m.logger.Trace("start ", name) startTime := time.Now() monitor.Start("start ", name) err := starter.Start() monitor.Finish() if err != nil { return E.Cause(err, "start ", name) } m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } } 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() + "]" m.logger.Trace("close ", name) startTime := time.Now() monitor.Start("close ", name) err = E.Append(err, closer.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 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 { m.logger.Trace(stage, " ", name) startTime := time.Now() err = adapter.LegacyStart(outbound, stage) if err != nil { return E.Cause(err, stage, " ", name) } m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } } 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 ( "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 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 AndroidPackageName 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" "crypto/tls" "net" "net/http" "sync" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-tun" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/x/list" "go4.org/netipx" ) 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 RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool ContainsIPCIDRRule bool } type HTTPStartContext struct { ctx context.Context access sync.Mutex httpClientCache map[string]*http.Client } func NewHTTPStartContext(ctx context.Context) *HTTPStartContext { return &HTTPStartContext{ ctx: ctx, httpClientCache: make(map[string]*http.Client), } } func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client { c.access.Lock() defer c.access.Unlock() if httpClient, loaded := c.httpClientCache[detour]; loaded { return httpClient } httpClient := &http.Client{ Transport: &http.Transport{ ForceAttemptHTTP2: true, TLSHandshakeTimeout: C.TCPTimeout, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, TLSClientConfig: &tls.Config{ Time: ntp.TimeFuncFromContext(c.ctx), RootCAs: RootPoolFromContext(c.ctx), }, }, } c.httpClientCache[detour] = httpClient return httpClient } func (c *HTTPStartContext) Close() { c.access.Lock() defer c.access.Unlock() for _, client := range c.httpClientCache { client.CloseIdleConnections() } } ================================================ FILE: adapter/rule.go ================================================ package adapter import ( C "github.com/sagernet/sing-box/constant" ) type HeadlessRule interface { Match(metadata *InboundContext) bool String() string } type Rule interface { HeadlessRule SimpleLifecycle Type() string Action() RuleAction } type DNSRule interface { Rule WithAddressLimit() bool MatchAddressLimit(metadata *InboundContext) bool } type RuleAction interface { Type() string String() string } func IsFinalAction(action RuleAction) bool { switch action.Type() { case C.RuleActionTypeSniff, C.RuleActionTypeResolve: 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" "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.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() + "]" m.logger.Trace(stage, " ", name) startTime := time.Now() err := adapter.LegacyStart(service, 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 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() + "]" m.logger.Trace("close ", name) startTime := time.Now() monitor.Start("close ", name) err = E.Append(err, service.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 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 { m.logger.Trace(stage, " ", name) startTime := time.Now() err = adapter.LegacyStart(service, stage) if err != nil { return E.Cause(err, stage, " ", name) } m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } } 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/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 ( ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) ) func NewUpstreamHandlerEx( metadata InboundContext, connectionHandler ConnectionHandlerFuncEx, packetHandler PacketConnectionHandlerFuncEx, ) UpstreamHandlerAdapterEx { return &myUpstreamHandlerWrapperEx{ metadata: metadata, connectionHandler: connectionHandler, packetHandler: packetHandler, } } var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil) type myUpstreamHandlerWrapperEx struct { metadata InboundContext connectionHandler ConnectionHandlerFuncEx packetHandler PacketConnectionHandlerFuncEx } func (w *myUpstreamHandlerWrapperEx) 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 *myUpstreamHandlerWrapperEx) 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 _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil) type myUpstreamContextHandlerWrapperEx struct { connectionHandler ConnectionHandlerFuncEx packetHandler PacketConnectionHandlerFuncEx } func NewUpstreamContextHandlerEx( connectionHandler ConnectionHandlerFuncEx, packetHandler PacketConnectionHandlerFuncEx, ) UpstreamHandlerAdapterEx { return &myUpstreamContextHandlerWrapperEx{ connectionHandler: connectionHandler, packetHandler: packetHandler, } } func (w *myUpstreamContextHandlerWrapperEx) 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 *myUpstreamContextHandlerWrapperEx) 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 NewRouteHandlerEx( metadata InboundContext, router ConnectionRouterEx, ) UpstreamHandlerAdapterEx { return &routeHandlerWrapperEx{ metadata: metadata, router: router, } } var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil) type routeHandlerWrapperEx struct { metadata InboundContext router ConnectionRouterEx } func (r *routeHandlerWrapperEx) 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 *routeHandlerWrapperEx) 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 NewRouteContextHandlerEx( router ConnectionRouterEx, ) UpstreamHandlerAdapterEx { return &routeContextHandlerWrapperEx{ router: router, } } var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil) type routeContextHandlerWrapperEx struct { router ConnectionRouterEx } func (r *routeContextHandlerWrapperEx) 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 *routeContextHandlerWrapperEx) 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 ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error // Deprecated PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error ) // Deprecated // //nolint:staticcheck func NewUpstreamHandler( metadata InboundContext, connectionHandler ConnectionHandlerFunc, packetHandler PacketConnectionHandlerFunc, errorHandler E.Handler, ) UpstreamHandlerAdapter { return &myUpstreamHandlerWrapper{ metadata: metadata, connectionHandler: connectionHandler, packetHandler: packetHandler, errorHandler: errorHandler, } } var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) // Deprecated: use myUpstreamHandlerWrapperEx instead. // //nolint:staticcheck type myUpstreamHandlerWrapper struct { metadata InboundContext connectionHandler ConnectionHandlerFunc packetHandler PacketConnectionHandlerFunc errorHandler E.Handler } // Deprecated: use myUpstreamHandlerWrapperEx instead. func (w *myUpstreamHandlerWrapper) 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 myUpstreamHandlerWrapperEx instead. func (w *myUpstreamHandlerWrapper) 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 myUpstreamHandlerWrapperEx instead. func (w *myUpstreamHandlerWrapper) 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 NewUpstreamContextHandlerEx instead. type myUpstreamContextHandlerWrapper struct { connectionHandler ConnectionHandlerFunc packetHandler PacketConnectionHandlerFunc errorHandler E.Handler } // Deprecated: Use NewUpstreamContextHandlerEx instead. func NewUpstreamContextHandler( connectionHandler ConnectionHandlerFunc, packetHandler PacketConnectionHandlerFunc, errorHandler E.Handler, ) UpstreamHandlerAdapter { return &myUpstreamContextHandlerWrapper{ connectionHandler: connectionHandler, packetHandler: packetHandler, errorHandler: errorHandler, } } // Deprecated: Use NewUpstreamContextHandlerEx instead. func (w *myUpstreamContextHandlerWrapper) 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 NewUpstreamContextHandlerEx instead. func (w *myUpstreamContextHandlerWrapper) 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 NewUpstreamContextHandlerEx instead. func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { w.errorHandler.NewError(ctx, err) } // Deprecated: Use ConnectionRouterEx instead. func NewRouteHandler( metadata InboundContext, router ConnectionRouter, logger logger.ContextLogger, ) UpstreamHandlerAdapter { return &routeHandlerWrapper{ metadata: metadata, router: router, logger: logger, } } // Deprecated: Use ConnectionRouterEx instead. func NewRouteContextHandler( router ConnectionRouter, logger logger.ContextLogger, ) UpstreamHandlerAdapter { return &routeContextHandlerWrapper{ router: router, logger: logger, } } var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) // Deprecated: Use ConnectionRouterEx instead. // //nolint:staticcheck type routeHandlerWrapper struct { metadata InboundContext router ConnectionRouter logger logger.ContextLogger } // Deprecated: Use ConnectionRouterEx instead. func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { 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 *routeHandlerWrapper) 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 *routeHandlerWrapper) NewError(ctx context.Context, err error) { w.logger.ErrorContext(ctx, err) } var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) // Deprecated: Use ConnectionRouterEx instead. type routeContextHandlerWrapper struct { router ConnectionRouter logger logger.ContextLogger } // Deprecated: Use ConnectionRouterEx instead. func (w *routeContextHandlerWrapper) 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 *routeContextHandlerWrapper) 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 *routeContextHandlerWrapper) 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" "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/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/dns/transport/local" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/cachefile" "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 dnsTransport *dns.TransportManager dnsRouter *dns.Router connection *route.ConnectionManager router *route.Router 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, ) 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) } 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) 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") } 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 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) } routeOptions := common.PtrValueOrDefault(options.Route) dnsOptions := common.PtrValueOrDefault(options.DNS) endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) 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) 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) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) service.MustRegister[adapter.DNSRouter](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) 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, 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, 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, "]") } } 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 local.NewTransport( ctx, logFactory.NewLogger("dns/local"), "local", option.LocalDNSServerOptions{}, ) }) if platformInterface != nil { err = platformInterface.Initialize(networkManager) if err != nil { return nil, E.Cause(err, "initialize platform interface") } } if needCacheFile { cacheFile := cachefile.New(ctx, 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, dnsRouter: dnsRouter, connection: connectionManager, router: router, 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) if err != nil { return err } err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router) 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.inbound, s.endpoint, 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.inbound, s.endpoint, 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.inbound, s.endpoint, 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}, {"endpoint", s.endpoint}, {"inbound", s.inbound}, {"outbound", s.outbound}, {"router", s.router}, {"connection", s.connection}, {"dns-router", s.dnsRouter}, {"dns-transport", s.dnsTransport}, {"network", s.network}, } { s.logger.Trace("close ", closeItem.name) startTime := time.Now() err = E.Append(err, closeItem.service.Close(), func(err error) error { return E.Cause(err, "close ", closeItem.name) }) s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } for _, lifecycleService := range s.internalService { s.logger.Trace("close ", lifecycleService.Name()) startTime := time.Now() err = E.Append(err, lifecycleService.Close(), func(err error) error { return E.Cause(err, "close ", lifecycleService.Name()) }) s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } s.logger.Trace("close logger") startTime := time.Now() err = E.Append(err, s.logFactory.Close(), func(err error) error { return E.Cause(err, "close logger") }) s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") 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) 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", } //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.Split(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.Split(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 import "crypto/x509" var mozillaIncluded *x509.CertPool func init() { mozillaIncluded = x509.NewCertPool() `) for { record, err := reader.Read() if err == io.EOF { break } else if err != nil { return err } if record[geoIndex] == "China" { continue } generated.WriteString("\n // ") generated.WriteString(record[nameIndex]) generated.WriteString("\n") generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`") cert := record[certIndex] // Remove single quotes cert = cert[1 : len(cert)-1] generated.WriteString(cert) generated.WriteString("`))\n") } generated.WriteString("}\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 import "crypto/x509" var chromeIncluded *x509.CertPool func init() { chromeIncluded = x509.NewCertPool() `) 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 } generated.WriteString("\n // ") generated.WriteString(record[subjectIndex]) generated.WriteString("\n") generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`") cert := record[certIndex] // Remove single quotes if present if len(cert) > 0 && cert[0] == '\'' { cert = cert[1 : len(cert)-1] } generated.WriteString(cert) generated.WriteString("`))\n") } generated.WriteString("}\n") 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 { optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options) if err != nil { return err } 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 ) if flagGeoipExportOutput == "stdout" { outputWriter = os.Stdout } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { outputFile, err = os.Create("geoip-" + countryCode + ".json") if err != nil { return err } defer outputFile.Close() outputWriter = outputFile } else { 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 ) if commandGeositeExportOutput == "stdout" { outputWriter = os.Stdout } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { outputFile, err = os.Create("geosite-" + category + ".json") if err != nil { return err } defer outputFile.Close() outputWriter = outputFile } else { 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.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_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) { if strings.HasPrefix(versionName, "v") { versionName = versionName[1:] } 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/chrome.go ================================================ // Code generated by 'make update_certificates'. DO NOT EDIT. package certificate import "crypto/x509" var chromeIncluded *x509.CertPool func init() { chromeIncluded = x509.NewCertPool() // CN=Actalis Authentication Root CA; O=Actalis S.p.A./03358520967; L=Milan; C=IT chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 4; O=Amazon; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 2; O=Amazon; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg 1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K 8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r 2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR 8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz 7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 +XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI 0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY +gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl 7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE 76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H 9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT 4PsJYGw= -----END CERTIFICATE-----`)) // CN=Amazon Root CA 3; O=Amazon; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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; O=Dhimyotis; C=FR chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q 130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG 9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE-----`)) // CN=Certigna Root CA; OU=0002 48146308100036; O=Dhimyotis; C=FR chromeIncluded.AppendCertsFromPEM([]byte(`-----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; O=certSIGN; C=RO chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do 0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ 44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN 9u6wWk5JRFRYX0KD -----END CERTIFICATE-----`)) // OU=certSIGN ROOT CA G2; O=CERTSIGN SA; C=RO chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 BR Root CA 1 2020; O=D-Trust GmbH; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 Root Class 3 CA 2 EV 2009; O=D-Trust GmbH; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp 3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 -----END CERTIFICATE-----`)) // CN=D-TRUST Root Class 3 CA 2 2009; O=D-Trust GmbH; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp /hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y Johw1+qRzT65ysCQblrGXnRl11z+o+I= -----END CERTIFICATE-----`)) // CN=T-TeleSec GlobalRoot Class 3; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN 8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ 1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT 91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p TpPDpFQUWw== -----END CERTIFICATE-----`)) // CN=T-TeleSec GlobalRoot Class 2; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----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=DigiCert TLS RSA4096 Root G5; O=DigiCert, Inc.; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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=DigiCert Assured ID Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe +o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== -----END CERTIFICATE-----`)) // CN=DigiCert Assured ID Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 CA; OU=www.digicert.com; O=DigiCert Inc; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= -----END CERTIFICATE-----`)) // CN=DigiCert Global Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 High Assurance EV Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K -----END CERTIFICATE-----`)) // CN=DigiCert Trusted Root G4; OU=www.digicert.com; O=DigiCert Inc; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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=QuoVadis Root CA 2; O=QuoVadis Limited; C=BM chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp +ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og /zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y 4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza 8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u -----END CERTIFICATE-----`)) // CN=QuoVadis Root CA 2 G3; O=QuoVadis Limited; C=BM chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz 8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l 7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE +V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M -----END CERTIFICATE-----`)) // CN=QuoVadis Root CA 3 G3; O=QuoVadis Limited; C=BM chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR /xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP 0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf 3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl 8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 -----END CERTIFICATE-----`)) // CN=CA Disig Root R2; O=Disig a.s.; L=Bratislava; C=SK chromeIncluded.AppendCertsFromPEM([]byte(`-----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 ECC Root CA - G3; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD +JbNR6iC8hZVdyR+EhCVBCyj -----END CERTIFICATE-----`)) // CN=emSign Root CA - G1; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN chromeIncluded.AppendCertsFromPEM([]byte(`-----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=AffirmTrust Commercial; O=AffirmTrust; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= -----END CERTIFICATE-----`)) // CN=Atos TrustedRoot 2011; O=Atos; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----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 ECC TLS 2021; O=Atos; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo 9H1/IISpQuQo -----END CERTIFICATE-----`)) // CN=Atos TrustedRoot Root CA RSA TLS 2021; O=Atos; C=DE chromeIncluded.AppendCertsFromPEM([]byte(`-----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; OU=GlobalSign Root CA - R6; O=GlobalSign chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw 1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R 8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= -----END CERTIFICATE-----`)) // CN=GlobalSign Root E46; O=GlobalSign nv-sa; C=BE chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ 7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 +RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= -----END CERTIFICATE-----`)) // CN=GlobalSign Root R46; O=GlobalSign nv-sa; C=BE chromeIncluded.AppendCertsFromPEM([]byte(`-----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; OU=GlobalSign ECC Root CA - R5; O=GlobalSign chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc 8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg 515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO xwy8p2Fp8fc74SrL+SvzZpA3 -----END CERTIFICATE-----`)) // CN=GlobalSign; OU=GlobalSign Root CA - R3; O=GlobalSign chromeIncluded.AppendCertsFromPEM([]byte(`-----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=Starfield Root Certificate Authority - G2; O=Starfield Technologies, Inc.; L=Scottsdale; ST=Arizona; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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=GlobalSign; OU=GlobalSign ECC Root CA - R4; O=GlobalSign chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ +wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm -----END CERTIFICATE-----`)) // CN=GTS Root R4; O=Google Trust Services LLC; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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=ACCVRAIZ1; OU=PKIACCV; O=ACCV; C=ES chromeIncluded.AppendCertsFromPEM([]byte(`-----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-----`)) // OU=AC RAIZ FNMT-RCM; O=FNMT-RCM; C=ES chromeIncluded.AppendCertsFromPEM([]byte(`-----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=AC RAIZ FNMT-RCM SERVIDORES SEGUROS; OU=Ceres; O=FNMT-RCM; C=ES; OrganizationIdentifier=VATES-Q2826004J chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy v+c= -----END CERTIFICATE-----`)) // 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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 Root CA 2017; O=Microsec Ltd.; L=Budapest; C=HU; OrganizationIdentifier=VATHU-23584497 chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ +efcMQ== -----END CERTIFICATE-----`)) // CN=Microsec e-Szigno Root CA 2009; O=Microsec Ltd.; L=Budapest; C=HU; EmailAddress=info@e-szigno.hu chromeIncluded.AppendCertsFromPEM([]byte(`-----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=Microsoft ECC Root Certificate Authority 2017; O=Microsoft Corporation; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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=Entrust Root Certification Authority; OU=www.entrust.net/CPS is incorporated by reference, (c) 2006 Entrust, Inc.; O=Entrust, Inc.; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi 94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP 9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE-----`)) // CN=Sectigo Public Server Authentication Root E46; O=Sectigo Limited; C=GB chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ 6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q 4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== -----END CERTIFICATE-----`)) // CN=COMODO ECC Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE-----`)) // CN=COMODO Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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=Sectigo Public Server Authentication Root R46; O=Sectigo Limited; C=GB chromeIncluded.AppendCertsFromPEM([]byte(`-----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=Entrust Root Certification Authority - G2; OU=See www.entrust.net/legal-terms, (c) 2009 Entrust, Inc. - for authorized use only; O=Entrust, Inc.; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v 1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== -----END CERTIFICATE-----`)) // CN=Entrust Root Certification Authority - EC1; OU=See www.entrust.net/legal-terms, (c) 2012 Entrust, Inc. - for authorized use only; O=Entrust, Inc.; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G -----END CERTIFICATE-----`)) // CN=SSL.com Root Certification Authority RSA; O=SSL Corporation; L=Houston; ST=Texas; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh /l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm +Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY Ic2wBlX7Jz9TkHCpBB5XJ7k= -----END CERTIFICATE-----`)) // CN=SSL.com TLS ECC Root CA 2022; O=SSL Corporation; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 TLS RSA Root CA 2022; O=SSL Corporation; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 chromeIncluded.AppendCertsFromPEM([]byte(`-----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 EV Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX 5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== -----END CERTIFICATE-----`)) // CN=SwissSign Gold CA - G2; O=SwissSign AG; C=CH chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c 6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn 8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a 77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ -----END CERTIFICATE-----`)) // CN=TWCA CYBER Root CA; OU=Root CA; O=TAIWAN-CA; C=TW chromeIncluded.AppendCertsFromPEM([]byte(`-----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=TWCA Global Root CA; OU=Root CA; O=TAIWAN-CA; C=TW chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF 10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz 0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc 46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm 4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB /zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL 1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh 15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW 6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy KwbQBM0= -----END CERTIFICATE-----`)) // CN=TeliaSonera Root CA v1; O=TeliaSonera chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ /jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs 81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG 9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx 0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= -----END CERTIFICATE-----`)) // CN=Telia Root CA v2; O=Telia Finland Oyj; C=FI chromeIncluded.AppendCertsFromPEM([]byte(`-----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-----`)) // CN=Trustwave Global ECC P384 Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF 1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu Sw== -----END CERTIFICATE-----`)) // CN=Trustwave Global ECC P256 Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 -----END CERTIFICATE-----`)) // CN=SecureTrust CA; O=SecureTrust Corporation; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO 0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj 7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS 8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB /zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ 3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR 3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= -----END CERTIFICATE-----`)) // CN=Trustwave Global Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu 7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW 80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W 0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK yeC2nOnOcXHebD8WpHk= -----END CERTIFICATE-----`)) } ================================================ FILE: common/certificate/mozilla.go ================================================ // Code generated by 'make update_certificates'. DO NOT EDIT. package certificate import "crypto/x509" var mozillaIncluded *x509.CertPool func init() { mozillaIncluded = x509.NewCertPool() // Actalis Authentication Root CA mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg 1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K 8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r 2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR 8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz 7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 +XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI 0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY +gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl 7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE 76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H 9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT 4PsJYGw= -----END CERTIFICATE-----`)) // Amazon Root CA 3 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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-----`)) // FIRMAPROFESIONAL CA ROOT-A WEB mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB /wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG XSaQpYXFuXqUPoeovQA= -----END CERTIFICATE-----`)) // ANF Secure Server Root CA mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q 130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG 9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE-----`)) // Certigna Root CA mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do 0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ 44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN 9u6wWk5JRFRYX0KD -----END CERTIFICATE-----`)) // certSIGN ROOT CA G2 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp /hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y Johw1+qRzT65ysCQblrGXnRl11z+o+I= -----END CERTIFICATE-----`)) // D-TRUST Root Class 3 CA 2 EV 2009 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp 3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 -----END CERTIFICATE-----`)) // D-Trust SBR Root CA 1 2022 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN 8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ 1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT 91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p TpPDpFQUWw== -----END CERTIFICATE-----`)) // Telekom Security SMIME ECC Root 2021 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe +o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== -----END CERTIFICATE-----`)) // DigiCert Assured ID Root G2 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= -----END CERTIFICATE-----`)) // DigiCert Global Root G2 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K -----END CERTIFICATE-----`)) // DigiCert SMIME ECC P384 Root G5 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp +ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og /zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y 4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza 8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u -----END CERTIFICATE-----`)) // QuoVadis Root CA 2 G3 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz 8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l 7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE +V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M -----END CERTIFICATE-----`)) // QuoVadis Root CA 3 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR /xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP 0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf 3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl 8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 -----END CERTIFICATE-----`)) // DIGITALSIGN GLOBAL ROOT ECDSA CA mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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-----`)) // GLOBALTRUST 2020 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd 6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf +I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn 4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== -----END CERTIFICATE-----`)) // emSign ECC Root CA - C3 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD +JbNR6iC8hZVdyR+EhCVBCyj -----END CERTIFICATE-----`)) // emSign Root CA - C1 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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-----`)) // AffirmTrust Commercial mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= -----END CERTIFICATE-----`)) // AffirmTrust Networking mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp 6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= -----END CERTIFICATE-----`)) // AffirmTrust Premium mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ +jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S 5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B 8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc 0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e KeC2uAloGRwYQw== -----END CERTIFICATE-----`)) // AffirmTrust Premium ECC mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D 0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== -----END CERTIFICATE-----`)) // Atos TrustedRoot 2011 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo 9H1/IISpQuQo -----END CERTIFICATE-----`)) // Atos TrustedRoot Root CA RSA G2 2020 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw 1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R 8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= -----END CERTIFICATE-----`)) // GlobalSign mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc 8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg 515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO xwy8p2Fp8fc74SrL+SvzZpA3 -----END CERTIFICATE-----`)) // GlobalSign Root CA mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ 7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 +RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= -----END CERTIFICATE-----`)) // GlobalSign Root R46 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ +wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm -----END CERTIFICATE-----`)) // GTS Root R1 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy v+c= -----END CERTIFICATE-----`)) // Staat der Nederlanden Root CA - G3 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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) mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ +efcMQ== -----END CERTIFICATE-----`)) // Microsec e-Szigno Root CA 2009 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE-----`)) // COMODO RSA Certification Authority mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi 94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP 9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE-----`)) // Entrust Root Certification Authority - EC1 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G -----END CERTIFICATE-----`)) // Entrust Root Certification Authority - G2 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v 1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== -----END CERTIFICATE-----`)) // Entrust Root Certification Authority - G4 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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) mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ 6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q 4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== -----END CERTIFICATE-----`)) // Sectigo Public Server Authentication Root R46 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX 5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== -----END CERTIFICATE-----`)) // SSL.com EV Root Certification Authority RSA R2 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh /l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm +Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY Ic2wBlX7Jz9TkHCpBB5XJ7k= -----END CERTIFICATE-----`)) // SSL.com TLS ECC Root CA 2022 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c 6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn 8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a 77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ -----END CERTIFICATE-----`)) // SwissSign RSA SMIME Root CA 2022 - 1 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF 10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz 0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc 46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm 4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB /zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL 1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh 15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW 6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy KwbQBM0= -----END CERTIFICATE-----`)) // TWCA Global Root CA G2 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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-----`)) // TeliaSonera Root CA v1 mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ /jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs 81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG 9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx 0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= -----END CERTIFICATE-----`)) // TrustAsia Global Root CA G3 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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 mozillaIncluded.AppendCertsFromPEM([]byte(`-----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-----`)) // Secure Global CA mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa /FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW -----END CERTIFICATE-----`)) // SecureTrust CA mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO 0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj 7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS 8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB /zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ 3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR 3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= -----END CERTIFICATE-----`)) // Trustwave Global Certification Authority mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu 7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW 80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W 0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK yeC2nOnOcXHebD8WpHk= -----END CERTIFICATE-----`)) // Trustwave Global ECC P256 Certification Authority mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 -----END CERTIFICATE-----`)) // Trustwave Global ECC P384 Certification Authority mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF 1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu Sw== -----END CERTIFICATE-----`)) // XRamp Global Certification Authority mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ O+7ETPTsJ3xCwnR8gooJybQDJbw= -----END CERTIFICATE-----`)) } ================================================ FILE: common/certificate/store.go ================================================ package certificate import ( "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 systemPool *x509.CertPool currentPool *x509.CertPool certificate string certificatePaths []string certificateDirectoryPaths []string watcher *fswatch.Watcher } func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) { var systemPool *x509.CertPool switch options.Store { 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: systemPool = mozillaIncluded case C.CertificateStoreChrome: systemPool = chromeIncluded case C.CertificateStoreNone: systemPool = nil default: return nil, E.New("unknown certificate store: ", options.Store) } store := &Store{ 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 { if s.watcher != nil { return s.watcher.Close() } return nil } func (s *Store) Pool() *x509.CertPool { s.access.RLock() defer s.access.RUnlock() return s.currentPool } func (s *Store) update() error { s.access.Lock() defer s.access.Unlock() var currentPool *x509.CertPool if s.systemPool == nil { currentPool = x509.NewCertPool() } else { currentPool = s.systemPool.Clone() } if s.certificate != "" { if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) { return E.New("invalid certificate PEM strings") } } 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) } } 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) } } } if firstErr != nil { return firstErr } s.currentPool = currentPool return nil } 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/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 } if strings.HasSuffix(ruleLine, "|") { ruleLine = ruleLine[:len(ruleLine)-1] } 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.Split(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 } if strings.HasSuffix(ruleLine, "|") { ruleLine = ruleLine[:len(ruleLine)-1] } 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) { idx := strings.Index(ruleLine, " ") if idx == -1 { return "", os.ErrInvalid } address, err := netip.ParseAddr(ruleLine[:idx]) if err != nil { return "", err } if !address.IsUnspecified() { return "", nil } domain := ruleLine[idx+1:] 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 { 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.dialer6.Dialer } else { return d.dialer4.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 { select { 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...) } } } } 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() 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 legacyDNSDialer bool dialer N.Dialer initOnce sync.Once initErr error } func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer { return &DetourDialer{ outboundManager: outboundManager, detour: detour, legacyDNSDialer: legacyDNSDialer, } } 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() { dialer, loaded := d.outboundManager.Outbound(d.detour) if !loaded { d.initErr = E.New("outbound detour not found: ", d.detour) return } if !d.legacyDNSDialer { 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 LegacyDNSDialer bool DirectOutbound 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 ) if dialOptions.Detour != "" { outboundManager := service.FromContext[adapter.OutboundManager](options.Context) if outboundManager == nil { return nil, E.New("missing outbound manager") } dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer) } else { dialer, err = NewDefault(options.Context, dialOptions) if err != nil { return nil, err } } if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") { 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, DisableCache: dialOptions.DomainResolver.DisableCache, 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 oldWriteItem(writer varbin.Writer, item Item) error { //nolint:staticcheck return varbin.Write(writer, binary.BigEndian, item) } 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 := 0; i < count; i++ { 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 } type geositeMetadata struct { Code string Index uint64 Length uint64 } 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) { 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/interrupt/conn.go ================================================ package interrupt import ( "net" "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 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 := 0; i < numCiphers; i++ { 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 := 0; i < numCurves; i++ { 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 := 0; i < numPF; i++ { 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 ( "fmt" "golang.org/x/crypto/cryptobyte" ) // The marshalingFunction type is an adapter to allow the use of ordinary // functions as cryptobyte.MarshalingValue. type marshalingFunction func(b *cryptobyte.Builder) error func (f marshalingFunction) Marshal(b *cryptobyte.Builder) error { return f(b) } // addBytesWithLength appends a sequence of bytes to the cryptobyte.Builder. If // the length of the sequence is not the value specified, it produces an error. func addBytesWithLength(b *cryptobyte.Builder, v []byte, n int) { b.AddValue(marshalingFunction(func(b *cryptobyte.Builder) error { if len(v) != n { return fmt.Errorf("invalid value length: expected %d, got %d", n, len(v)) } b.AddBytes(v) return nil })) } // addUint64 appends a big-endian, 64-bit value to the cryptobyte.Builder. func addUint64(b *cryptobyte.Builder, v uint64) { b.AddUint32(uint32(v >> 32)) b.AddUint32(uint32(v)) } // readUint64 decodes a big-endian, 64-bit value into out and advances over it. // It reports whether the read was successful. func readUint64(s *cryptobyte.String, out *uint64) bool { var hi, lo uint32 if !s.ReadUint32(&hi) || !s.ReadUint32(&lo) { return false } *out = uint64(hi)<<32 | uint64(lo) return true } // 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)) } // readUint24LengthPrefixed acts like s.ReadUint24LengthPrefixed, but targets a // []byte instead of a cryptobyte.String. func readUint24LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { return s.ReadUint24LengthPrefixed((*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 compression types. const ( compressionNone uint8 = 0 ) // 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) } /*for len(data) > 0 { m := len(data) if maxPayload := c.maxPayloadSizeForWrite(typ); m > maxPayload { m = maxPayload } _, err = c.writeKernelRecord(typ, data[:m]) if err != nil { return } n += m data = data[m:] }*/ return c.writeKernelRecord(typ, data) } const ( // tcpMSSEstimate is a conservative estimate of the TCP maximum segment // size (MSS). A constant is used, rather than querying the kernel for // the actual MSS, to avoid complexity. The value here is the IPv6 // minimum MTU (1280 bytes) minus the overhead of an IPv6 header (40 // bytes) and a TCP header with timestamps (32 bytes). tcpMSSEstimate = 1208 // recordSizeBoostThreshold is the number of bytes of application data // sent after which the TLS record size will be increased to the // maximum. recordSizeBoostThreshold = 128 * 1024 ) func (c *Conn) maxPayloadSizeForWrite(typ uint16) int { if /*c.config.DynamicRecordSizingDisabled ||*/ typ != recordTypeApplicationData { return maxPlaintext } if *c.rawConn.PacketsSent >= recordSizeBoostThreshold { return maxPlaintext } // Subtract TLS overheads to get the maximum payload size. payloadBytes := tcpMSSEstimate - recordHeaderLen - c.rawConn.Out.ExplicitNonceLen() if rawCipher := *c.rawConn.Out.Cipher; rawCipher != nil { switch ciph := rawCipher.(type) { case cipher.Stream: payloadBytes -= (*c.rawConn.Out.Mac).Size() case cipher.AEAD: payloadBytes -= ciph.Overhead() /*case cbcMode: blockSize := ciph.BlockSize() // The payload must fit in a multiple of blockSize, with // room for at least one padding byte. payloadBytes = (payloadBytes & ^(blockSize - 1)) - 1 // The RawMac is appended before padding so affects the // payload size directly. payloadBytes -= c.out.mac.Size()*/ default: panic("unknown cipher type") } } if *c.rawConn.Vers == tls.VersionTLS13 { payloadBytes-- // encrypted ContentType } // Allow packet growth in arithmetic progression up to max. pkt := *c.rawConn.PacketsSent *c.rawConn.PacketsSent++ if pkt > 1000 { return maxPlaintext // avoid overflow in multiply below } n := payloadBytes * int(pkt+1) if n > maxPlaintext { n = maxPlaintext } return n } ================================================ 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.ConnectionHandlerEx packetHandler adapter.PacketHandlerEx oobPacketHandler adapter.OOBPacketHandlerEx 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.ConnectionHandlerEx PacketHandler adapter.PacketHandlerEx OOBPacketHandler adapter.OOBPacketHandlerEx 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 { 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.NewConnectionEx(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" "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" ) 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) 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.NewPacketEx(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.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) } } } func (l *Listener) loopUDPOut() { for { select { case packet := <-l.packetOutbound: destination := packet.Destination.AddrPort() _, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination) packet.Buffer.Release() N.PutPacketBuffer(packet) if err != nil { if l.shutdown.Load() && E.IsClosed(err) { return } l.logger.Error("udp listener write back: ", destination, ": ", err) continue } continue case <-l.packetOutboundClosed: } 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) 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.NewRouteContextHandlerEx(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/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) } 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 { 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" ) var _ Searcher = (*androidSearcher)(nil) type androidSearcher struct { packageManager tun.PackageManager } func NewSearcher(config Config) (Searcher, error) { return &androidSearcher{config.PackageManager}, nil } func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { _, uid, err := resolveSocketByNetlink(network, source, destination) if err != nil { return nil, err } if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded { return &adapter.ConnectionOwner{ UserId: int32(uid), AndroidPackageName: sharedPackage, }, nil } if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded { return &adapter.ConnectionOwner{ UserId: int32(uid), AndroidPackageName: packageName, }, nil } return &adapter.ConnectionOwner{UserId: int32(uid)}, nil } ================================================ FILE: common/process/searcher_darwin.go ================================================ package process import ( "context" "encoding/binary" "net/netip" "os" "strconv" "strings" "syscall" "unsafe" "github.com/sagernet/sing-box/adapter" N "github.com/sagernet/sing/common/network" "golang.org/x/sys/unix" ) var _ Searcher = (*darwinSearcher)(nil) type darwinSearcher struct{} func NewSearcher(_ Config) (Searcher, error) { return &darwinSearcher{}, nil } func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { processName, err := findProcessName(network, source.Addr(), int(source.Port())) if err != nil { return nil, err } return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil } 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 } }() func findProcessName(network string, ip netip.Addr, port int) (string, error) { var spath string switch network { case N.NetworkTCP: spath = "net.inet.tcp.pcblist_n" case N.NetworkUDP: spath = "net.inet.udp.pcblist_n" default: return "", os.ErrInvalid } isIPv4 := ip.Is4() value, err := unix.SysctlRaw(spath) if err != nil { return "", err } buf := value // 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)) itemSize := structSize if network == N.NetworkTCP { // rup8(sizeof(xtcpcb_n)) itemSize += 208 } var fallbackUDPProcess string // skip the first xinpgen(24 bytes) block for i := 24; i+itemSize <= len(buf); i += itemSize { // offset of xinpcb_n and xsocket_n inp, so := i, i+104 srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20]) if uint16(port) != srcPort { continue } // xinpcb_n.inp_vflag flag := buf[inp+44] var srcIP netip.Addr srcIsIPv4 := false switch { case flag&0x1 > 0 && isIPv4: // ipv4 srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80])) srcIsIPv4 = true case flag&0x2 > 0 && !isIPv4: // ipv6 srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80])) default: continue } if ip == srcIP { // xsocket_n.so_last_pid pid := readNativeUint32(buf[so+68 : so+72]) return getExecPathFromPID(pid) } // udp packet connection may be not equal with srcIP if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 { pid := readNativeUint32(buf[so+68 : so+72]) fallbackUDPProcess, _ = getExecPathFromPID(pid) } } if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 { return fallbackUDPProcess, nil } return "", ErrNotFound } 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 } func readNativeUint32(b []byte) uint32 { return *(*uint32)(unsafe.Pointer(&b[0])) } ================================================ FILE: common/process/searcher_linux.go ================================================ //go:build linux && !android package process import ( "context" "net/netip" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" ) var _ Searcher = (*linuxSearcher)(nil) type linuxSearcher struct { logger log.ContextLogger } func NewSearcher(config Config) (Searcher, error) { return &linuxSearcher{config.Logger}, nil } func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { inode, uid, err := resolveSocketByNetlink(network, source, destination) if err != nil { return nil, err } processPath, err := resolveProcessNameByProcSearch(inode, uid) if err != nil { s.logger.DebugContext(ctx, "find process path: ", err) } return &adapter.ConnectionOwner{ UserId: int32(uid), ProcessPath: processPath, }, nil } ================================================ FILE: common/process/searcher_linux_shared.go ================================================ //go:build linux package process import ( "bytes" "encoding/binary" "fmt" "net" "net/netip" "os" "path" "strings" "syscall" "unicode" "unsafe" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) // from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62 var nativeEndian = func() binary.ByteOrder { var x uint32 = 0x01020304 if *(*byte)(unsafe.Pointer(&x)) == 0x01 { return binary.BigEndian } return binary.LittleEndian }() const ( sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48 socketDiagByFamily = 20 pathProc = "/proc" ) func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { var family uint8 var protocol uint8 switch network { case N.NetworkTCP: protocol = syscall.IPPROTO_TCP case N.NetworkUDP: protocol = syscall.IPPROTO_UDP default: return 0, 0, os.ErrInvalid } if source.Addr().Is4() { family = syscall.AF_INET } else { family = syscall.AF_INET6 } req := packSocketDiagRequest(family, protocol, source) socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG) if err != nil { return 0, 0, E.Cause(err, "dial netlink") } defer syscall.Close(socket) syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100}) syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100}) err = syscall.Connect(socket, &syscall.SockaddrNetlink{ Family: syscall.AF_NETLINK, Pad: 0, Pid: 0, Groups: 0, }) if err != nil { return } _, err = syscall.Write(socket, req) if err != nil { return 0, 0, E.Cause(err, "write netlink request") } buffer := buf.New() defer buffer.Release() n, err := syscall.Read(socket, buffer.FreeBytes()) if err != nil { return 0, 0, E.Cause(err, "read netlink response") } buffer.Truncate(n) messages, err := syscall.ParseNetlinkMessage(buffer.Bytes()) if err != nil { return 0, 0, E.Cause(err, "parse netlink message") } else if len(messages) == 0 { return 0, 0, E.New("unexcepted netlink response") } message := messages[0] if message.Header.Type&syscall.NLMSG_ERROR != 0 { return 0, 0, E.New("netlink message: NLMSG_ERROR") } inode, uid = unpackSocketDiagResponse(&messages[0]) return } func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte { s := make([]byte, 16) copy(s, source.Addr().AsSlice()) buf := make([]byte, sizeOfSocketDiagRequest) nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest) nativeEndian.PutUint16(buf[4:6], socketDiagByFamily) nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP) nativeEndian.PutUint32(buf[8:12], 0) nativeEndian.PutUint32(buf[12:16], 0) buf[16] = family buf[17] = protocol buf[18] = 0 buf[19] = 0 nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF) binary.BigEndian.PutUint16(buf[24:26], source.Port()) binary.BigEndian.PutUint16(buf[26:28], 0) copy(buf[28:44], s) copy(buf[44:60], net.IPv6zero) nativeEndian.PutUint32(buf[60:64], 0) nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF) return buf } func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { if len(msg.Data) < 72 { return 0, 0 } data := msg.Data uid = nativeEndian.Uint32(data[64:68]) inode = nativeEndian.Uint32(data[68:72]) return } func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { files, err := os.ReadDir(pathProc) if err != nil { return "", err } buffer := make([]byte, syscall.PathMax) socket := []byte(fmt.Sprintf("socket:[%d]", inode)) for _, f := range files { if !f.IsDir() || !isPid(f.Name()) { continue } info, err := f.Info() if err != nil { return "", err } if info.Sys().(*syscall.Stat_t).Uid != uid { continue } processPath := path.Join(pathProc, f.Name()) fdPath := path.Join(processPath, "fd") fds, err := os.ReadDir(fdPath) if err != nil { continue } for _, fd := range fds { n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer) if err != nil { continue } if bytes.Equal(buffer[:n], socket) { return os.Readlink(path.Join(processPath, "exe")) } } } return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode) } func isPid(s string) bool { return strings.IndexFunc(s, func(r rune) bool { return !unicode.IsDigit(r) }) == -1 } ================================================ 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) 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/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/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.Split(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 []interface{} err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services) if err != nil { return adapter.WIFIState{} } for _, service := range services { servicePair, ok := service.([]interface{}) 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 ================================================ 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 strings.HasPrefix(line, "wpa_state=") { state := strings.TrimPrefix(line, "wpa_state=") connected = state == "COMPLETED" } else if strings.HasPrefix(line, "ssid=") { ssid = strings.TrimPrefix(line, "ssid=") } else if strings.HasPrefix(line, "bssid=") { bssid = strings.TrimPrefix(line, "bssid=") } } 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 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) explicitNonceLen() int { return 0 } 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 ( "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 { for _, curve := range fingerprint.EllipticCurves { if curve == x25519Kyber768Draft00 { return true } } for _, ext := range fingerprint.Extensions { if ext == extensionRenegotiationInfo { return true } } return false } ================================================ 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 i := 0; i < 10; i++ { 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 i := 0; i < 5; i++ { // 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 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 := uint64(0); i < length; i++ { 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 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.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 := uint64(0); i < length; i++ { 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 := 0; i < count; i++ { 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/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" "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 } type acmeLogWriter struct { logger logger.Logger } func (w *acmeLogWriter) Write(p []byte) (n int, err error) { logLine := strings.ReplaceAll(string(p), " ", ": ") switch { case strings.HasPrefix(logLine, "error: "): w.logger.Error(logLine[7:]) case strings.HasPrefix(logLine, "warn: "): w.logger.Warn(logLine[6:]) case strings.HasPrefix(logLine, "info: "): w.logger.Info(logLine[6:]) case strings.HasPrefix(logLine, "debug: "): w.logger.Debug(logLine[7:]) default: w.logger.Debug(logLine) } return len(p), nil } func (w *acmeLogWriter) Sync() error { return nil } func encoderConfig() zapcore.EncoderConfig { config := zap.NewProductionEncoderConfig() config.TimeKey = zapcore.OmitKey return config } func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { var acmeServer string switch options.Provider { 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(encoderConfig()), &acmeLogWriter{logger: logger}, zap.DebugLevel, )) config := &certmagic.Config{ DefaultServerName: options.DefaultServerName, Storage: storage, Logger: zapLogger, } acmeConfig := certmagic.ACMEIssuer{ CA: acmeServer, Email: options.Email, Agreed: true, 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{ACMETLS1Protocol}, } } return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil } ================================================ FILE: common/tls/acme_contstant.go ================================================ package tls const ACMETLS1Protocol = "acme-tls/1" ================================================ 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/client.go ================================================ package tls import ( "context" "crypto/tls" "errors" "net" "os" "github.com/sagernet/sing-box/common/badtls" C "github.com/sagernet/sing-box/constant" "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" aTLS "github.com/sagernet/sing/common/tls" ) 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 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") } if options.Options.Reality != nil && options.Options.Reality.Enabled { return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options) } else if options.Options.UTLS != nil && options.Options.UTLS.Enabled { return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options) } return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options) } func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) defer cancel() tlsConn, err := aTLS.ClientHandshake(ctx, conn, config) if err != nil { return nil, err } 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) { if options.UTLS == nil || !options.UTLS.Enabled { return nil, E.New("uTLS is required by reality client") } uClient, err := NewUTLSClient(ctx, logger, serverAddress, options) 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) 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.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") certs := *(*([]*x509.Certificate))(unsafe.Pointer(uintptr(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 } func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { var tlsConfig utls.RealityConfig 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 config ServerConfig = &RealityServerConfig{&tlsConfig} 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) 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(), } } 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) { 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" 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 fragment bool fragmentFallbackDelay time.Duration recordFragment bool } func (c *STDClientConfig) ServerName() string { return c.config.ServerName } func (c *STDClientConfig) SetServerName(serverName string) { 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) STDConfig() (*STDConfig, error) { return c.config, nil } func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { if c.recordFragment { conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) } return tls.Client(conn, c.config), nil } func (c *STDClientConfig) Clone() Config { return &STDClientConfig{ ctx: c.ctx, config: c.config.Clone(), fragment: c.fragment, fragmentFallbackDelay: c.fragmentFallbackDelay, recordFragment: c.recordFragment, } } 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) { var serverName string if options.ServerName != "" { serverName = options.ServerName } else if serverAddress != "" { serverName = serverAddress } if serverName == "" && !options.Insecure { return nil, E.New("missing server_name or insecure=true") } var tlsConfig tls.Config tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) if !options.DisableSNI { tlsConfig.ServerName = serverName } if options.Insecure { tlsConfig.InsecureSkipVerify = options.Insecure } else if options.DisableSNI { tlsConfig.InsecureSkipVerify = true tlsConfig.VerifyConnection = func(state tls.ConnectionState) error { verifyOptions := x509.VerifyOptions{ Roots: tlsConfig.RootCAs, DNSName: serverName, Intermediates: x509.NewCertPool(), } for _, cert := range state.PeerCertificates[1:] { verifyOptions.Intermediates.AddCert(cert) } if tlsConfig.Time != nil { verifyOptions.CurrentTime = tlsConfig.Time() } _, err := state.PeerCertificates[0].Verify(verifyOptions) return err } } if len(options.CertificatePublicKeySHA256) > 0 { if len(options.Certificate) > 0 || options.CertificatePath != "" { 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, tlsConfig.Time) } } 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 config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} 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 verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) 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/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" ) var errInsecureUnused = E.New("tls: insecure unused") type STDServerConfig struct { access sync.RWMutex config *tls.Config logger log.Logger 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.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { return c.config.NextProtos[1:] } else { return c.config.NextProtos } } func (c *STDServerConfig) SetNextProtos(nextProto []string) { c.access.Lock() defer c.access.Unlock() config := c.config.Clone() if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { config.NextProtos = append(c.config.NextProtos[:1], nextProto...) } else { config.NextProtos = nextProto } c.config = config } 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(), } } func (c *STDServerConfig) Start() error { if c.acmeService != nil { return c.acmeService.Start() } else { 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 { if path == c.certificatePath { certificate, err := os.ReadFile(c.certificatePath) if err != nil { return E.Cause(err, "reload certificate from ", c.certificatePath) } c.certificate = certificate } else if path == 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 { if c.acmeService != nil { return c.acmeService.Close() } if c.watcher != nil { return c.watcher.Close() } return nil } func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { if !options.Enabled { return nil, nil } var tlsConfig *tls.Config var acmeService adapter.SimpleLifecycle var err error if options.ACME != nil && len(options.ACME.Domain) > 0 { //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 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 { if tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { tlsConfig.ClientAuth = tls.RequireAnyClientCert } else if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven { tlsConfig.ClientAuth = tls.RequestClientCert } tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time) } } 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 } } serverConfig := &STDServerConfig{ config: tlsConfig, logger: logger, 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.Lock() defer serverConfig.access.Unlock() 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 } ================================================ 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" 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 id utls.ClientHelloID fragment bool fragmentFallbackDelay time.Duration recordFragment bool } func (c *UTLSClientConfig) ServerName() string { return c.config.ServerName } func (c *UTLSClientConfig) SetServerName(serverName string) { 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) STDConfig() (*STDConfig, error) { return nil, E.New("unsupported usage for uTLS") } func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { if c.recordFragment { conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) } 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 { return &UTLSClientConfig{ c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment, } } 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) { var serverName string if options.ServerName != "" { serverName = options.ServerName } else if serverAddress != "" { serverName = serverAddress } if serverName == "" && !options.Insecure { return nil, E.New("missing server_name or insecure=true") } var tlsConfig utls.Config tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) if !options.DisableSNI { tlsConfig.ServerName = serverName } if options.Insecure { tlsConfig.InsecureSkipVerify = options.Insecure } else if options.DisableSNI { if options.Reality != nil && options.Reality.Enabled { return nil, E.New("disable_sni is unsupported in reality") } tlsConfig.InsecureServerNameToVerify = serverName } if len(options.CertificatePublicKeySHA256) > 0 { if len(options.Certificate) > 0 || options.CertificatePath != "" { 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, tlsConfig.Time) } } 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") } id, err := uTLSClientHelloID(options.UTLS.Fingerprint) if err != nil { return nil, err } var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} 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_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 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 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/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 } 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 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 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/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: constant/certificate.go ================================================ package constant const ( CertificateStoreSystem = "system" CertificateStoreMozilla = "mozilla" CertificateStoreChrome = "chrome" CertificateStoreNone = "none" ) ================================================ FILE: constant/cgo.go ================================================ //go:build cgo package constant const CGO_ENABLED = true ================================================ FILE: constant/cgo_disabled.go ================================================ //go:build !cgo package constant const CGO_ENABLED = false ================================================ FILE: constant/dhcp.go ================================================ package constant import "time" const ( DHCPTTL = time.Hour DHCPTimeout = 5 * time.Second ) ================================================ FILE: constant/dns.go ================================================ package constant const ( DefaultDNSTTL = 600 ) type DomainStrategy = uint8 const ( DomainStrategyAsIS DomainStrategy = iota DomainStrategyPreferIPv4 DomainStrategyPreferIPv6 DomainStrategyIPv4Only DomainStrategyIPv6Only ) const ( DNSTypeLegacy = "legacy" DNSTypeLegacyRcode = "legacy_rcode" DNSTypeUDP = "udp" DNSTypeTCP = "tcp" DNSTypeTLS = "tls" DNSTypeHTTPS = "https" DNSTypeQUIC = "quic" DNSTypeHTTP3 = "h3" DNSTypeLocal = "local" DNSTypeHosts = "hosts" DNSTypeFakeIP = "fakeip" DNSTypeDHCP = "dhcp" DNSTypeTailscale = "tailscale" ) const ( DNSProviderAliDNS = "alidns" DNSProviderCloudflare = "cloudflare" DNSProviderACMEDNS = "acmedns" ) ================================================ FILE: constant/err.go ================================================ package constant import E "github.com/sagernet/sing/common/exceptions" var ErrTLSRequired = E.New("TLS required") var ErrQUICNotIncluded = E.New(`QUIC is not included in this build, rebuild with -tags with_quic`) ================================================ FILE: constant/goos/gengoos.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. //go:build ignore package main import ( "bytes" "fmt" "log" "os" "strconv" "strings" ) var gooses []string func main() { data, err := os.ReadFile("../../go/build/syslist.go") if err != nil { log.Fatal(err) } const goosPrefix = `const goosList = ` for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, goosPrefix) { text, err := strconv.Unquote(strings.TrimPrefix(line, goosPrefix)) if err != nil { log.Fatalf("parsing goosList: %v", err) } gooses = strings.Fields(text) } } for _, target := range gooses { if target == "nacl" { continue } var tags []string if target == "linux" { tags = append(tags, "!android") // must explicitly exclude android for linux } if target == "solaris" { tags = append(tags, "!illumos") // must explicitly exclude illumos for solaris } if target == "darwin" { tags = append(tags, "!ios") // must explicitly exclude ios for darwin } tags = append(tags, target) // must explicitly include target for bootstrapping purposes var buf bytes.Buffer fmt.Fprintf(&buf, "// Code generated by gengoos.go using 'go generate'. DO NOT EDIT.\n\n") fmt.Fprintf(&buf, "//go:build %s\n", strings.Join(tags, " && ")) fmt.Fprintf(&buf, "package goos\n\n") fmt.Fprintf(&buf, "const GOOS = `%s`\n\n", target) for _, goos := range gooses { value := 0 if goos == target { value = 1 } fmt.Fprintf(&buf, "const Is%s = %d\n", strings.Title(goos), value) } err := os.WriteFile("zgoos_"+target+".go", buf.Bytes(), 0o666) if err != nil { log.Fatal(err) } } } ================================================ FILE: constant/goos/goos.go ================================================ // Copyright 2015 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. // package goos contains GOOS-specific constants. package goos // The next line makes 'go generate' write the zgoos*.go files with // per-OS information, including constants named Is$GOOS for every // known GOOS. The constant is 1 on the current system, 0 otherwise; // multiplying by them is useful for defining GOOS-specific constants. //go:generate go run gengoos.go ================================================ FILE: constant/goos/zgoos_aix.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build aix package goos const GOOS = `aix` const IsAix = 1 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_android.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build android package goos const GOOS = `android` const IsAix = 0 const IsAndroid = 1 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_darwin.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build !ios && darwin package goos const GOOS = `darwin` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 1 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_dragonfly.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build dragonfly package goos const GOOS = `dragonfly` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 1 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_freebsd.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build freebsd package goos const GOOS = `freebsd` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 1 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_hurd.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build hurd package goos const GOOS = `hurd` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 1 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_illumos.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build illumos package goos const GOOS = `illumos` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 1 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_ios.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build ios package goos const GOOS = `ios` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 1 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_js.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build js package goos const GOOS = `js` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 1 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_linux.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build !android && linux package goos const GOOS = `linux` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 1 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_netbsd.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build netbsd package goos const GOOS = `netbsd` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 1 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_openbsd.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build openbsd package goos const GOOS = `openbsd` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 1 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_plan9.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build plan9 package goos const GOOS = `plan9` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 1 const IsSolaris = 0 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_solaris.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build !illumos && solaris package goos const GOOS = `solaris` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 1 const IsWindows = 0 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_windows.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build windows package goos const GOOS = `windows` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 1 const IsZos = 0 ================================================ FILE: constant/goos/zgoos_zos.go ================================================ // Code generated by gengoos.go using 'go generate'. DO NOT EDIT. //go:build zos package goos const GOOS = `zos` const IsAix = 0 const IsAndroid = 0 const IsDarwin = 0 const IsDragonfly = 0 const IsFreebsd = 0 const IsHurd = 0 const IsIllumos = 0 const IsIos = 0 const IsJs = 0 const IsLinux = 0 const IsNacl = 0 const IsNetbsd = 0 const IsOpenbsd = 0 const IsPlan9 = 0 const IsSolaris = 0 const IsWindows = 0 const IsZos = 1 ================================================ FILE: constant/hysteria2.go ================================================ package constant const ( Hysterai2MasqueradeTypeFile = "file" Hysterai2MasqueradeTypeProxy = "proxy" Hysterai2MasqueradeTypeString = "string" ) ================================================ FILE: constant/network.go ================================================ package constant import ( "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" ) type InterfaceType uint8 const ( InterfaceTypeWIFI InterfaceType = iota InterfaceTypeCellular InterfaceTypeEthernet InterfaceTypeOther ) var ( interfaceTypeToString = map[InterfaceType]string{ InterfaceTypeWIFI: "wifi", InterfaceTypeCellular: "cellular", InterfaceTypeEthernet: "ethernet", InterfaceTypeOther: "other", } StringToInterfaceType = common.ReverseMap(interfaceTypeToString) ) func (t InterfaceType) String() string { name, loaded := interfaceTypeToString[t] if !loaded { return F.ToString(int(t)) } return name } type NetworkStrategy uint8 const ( NetworkStrategyDefault NetworkStrategy = iota NetworkStrategyFallback NetworkStrategyHybrid ) var ( networkStrategyToString = map[NetworkStrategy]string{ NetworkStrategyDefault: "default", NetworkStrategyFallback: "fallback", NetworkStrategyHybrid: "hybrid", } StringToNetworkStrategy = common.ReverseMap(networkStrategyToString) ) func (s NetworkStrategy) String() string { name, loaded := networkStrategyToString[s] if !loaded { return F.ToString(int(s)) } return name } ================================================ FILE: constant/os.go ================================================ package constant import ( "github.com/sagernet/sing-box/constant/goos" ) const IsAndroid = goos.IsAndroid == 1 const IsDarwin = goos.IsDarwin == 1 || goos.IsIos == 1 const IsDragonfly = goos.IsDragonfly == 1 const IsFreebsd = goos.IsFreebsd == 1 const IsHurd = goos.IsHurd == 1 const IsIllumos = goos.IsIllumos == 1 const IsIos = goos.IsIos == 1 const IsJs = goos.IsJs == 1 const IsLinux = goos.IsLinux == 1 || goos.IsAndroid == 1 const IsNacl = goos.IsNacl == 1 const IsNetbsd = goos.IsNetbsd == 1 const IsOpenbsd = goos.IsOpenbsd == 1 const IsPlan9 = goos.IsPlan9 == 1 const IsSolaris = goos.IsSolaris == 1 const IsWindows = goos.IsWindows == 1 const IsZos = goos.IsZos == 1 ================================================ FILE: constant/path.go ================================================ package constant import ( "os" "path/filepath" "github.com/sagernet/sing/common/rw" ) const dirName = "sing-box" var resourcePaths []string func FindPath(name string) (string, bool) { name = os.ExpandEnv(name) if rw.IsFile(name) { return name, true } for _, dir := range resourcePaths { if path := filepath.Join(dir, dirName, name); rw.IsFile(path) { return path, true } if path := filepath.Join(dir, name); rw.IsFile(path) { return path, true } } return name, false } func init() { resourcePaths = append(resourcePaths, ".") if home := os.Getenv("HOME"); home != "" { resourcePaths = append(resourcePaths, home) } if userConfigDir, err := os.UserConfigDir(); err == nil { resourcePaths = append(resourcePaths, userConfigDir) } if userCacheDir, err := os.UserCacheDir(); err == nil { resourcePaths = append(resourcePaths, userCacheDir) } } ================================================ FILE: constant/path_unix.go ================================================ //go:build unix || linux package constant import ( "os" ) func init() { resourcePaths = append(resourcePaths, "/etc") resourcePaths = append(resourcePaths, "/usr/share") resourcePaths = append(resourcePaths, "/usr/local/etc") resourcePaths = append(resourcePaths, "/usr/local/share") if homeDir := os.Getenv("HOME"); homeDir != "" { resourcePaths = append(resourcePaths, homeDir+"/.local/share") } } ================================================ FILE: constant/protocol.go ================================================ package constant const ( ProtocolTLS = "tls" ProtocolHTTP = "http" ProtocolQUIC = "quic" ProtocolDNS = "dns" ProtocolSTUN = "stun" ProtocolBitTorrent = "bittorrent" ProtocolDTLS = "dtls" ProtocolSSH = "ssh" ProtocolRDP = "rdp" ProtocolNTP = "ntp" ) const ( ClientChromium = "chromium" ClientSafari = "safari" ClientFirefox = "firefox" ClientQUICGo = "quic-go" ClientUnknown = "unknown" ) ================================================ FILE: constant/proxy.go ================================================ package constant const ( TypeTun = "tun" TypeRedirect = "redirect" TypeTProxy = "tproxy" TypeDirect = "direct" TypeBlock = "block" TypeDNS = "dns" TypeSOCKS = "socks" TypeHTTP = "http" TypeMixed = "mixed" TypeShadowsocks = "shadowsocks" TypeVMess = "vmess" TypeTrojan = "trojan" TypeNaive = "naive" TypeWireGuard = "wireguard" TypeHysteria = "hysteria" TypeTor = "tor" TypeSSH = "ssh" TypeShadowTLS = "shadowtls" TypeAnyTLS = "anytls" TypeShadowsocksR = "shadowsocksr" TypeVLESS = "vless" TypeTUIC = "tuic" TypeHysteria2 = "hysteria2" TypeTailscale = "tailscale" TypeDERP = "derp" TypeResolved = "resolved" TypeSSMAPI = "ssm-api" TypeCCM = "ccm" TypeOCM = "ocm" TypeOOMKiller = "oom-killer" ) const ( TypeSelector = "selector" TypeURLTest = "urltest" ) func ProxyDisplayName(proxyType string) string { switch proxyType { case TypeTun: return "TUN" case TypeRedirect: return "Redirect" case TypeTProxy: return "TProxy" case TypeDirect: return "Direct" case TypeBlock: return "Block" case TypeDNS: return "DNS" case TypeSOCKS: return "SOCKS" case TypeHTTP: return "HTTP" case TypeMixed: return "Mixed" case TypeShadowsocks: return "Shadowsocks" case TypeVMess: return "VMess" case TypeTrojan: return "Trojan" case TypeNaive: return "Naive" case TypeWireGuard: return "WireGuard" case TypeHysteria: return "Hysteria" case TypeTor: return "Tor" case TypeSSH: return "SSH" case TypeShadowTLS: return "ShadowTLS" case TypeShadowsocksR: return "ShadowsocksR" case TypeVLESS: return "VLESS" case TypeTUIC: return "TUIC" case TypeHysteria2: return "Hysteria2" case TypeAnyTLS: return "AnyTLS" case TypeTailscale: return "Tailscale" case TypeSelector: return "Selector" case TypeURLTest: return "URLTest" default: return "Unknown" } } ================================================ FILE: constant/quic.go ================================================ //go:build with_quic package constant const WithQUIC = true ================================================ FILE: constant/quic_stub.go ================================================ //go:build !with_quic package constant const WithQUIC = false ================================================ FILE: constant/rule.go ================================================ package constant const ( RuleTypeDefault = "default" RuleTypeLogical = "logical" ) const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) const ( RuleSetTypeInline = "inline" RuleSetTypeLocal = "local" RuleSetTypeRemote = "remote" RuleSetFormatSource = "source" RuleSetFormatBinary = "binary" ) const ( RuleSetVersion1 = 1 + iota RuleSetVersion2 RuleSetVersion3 RuleSetVersion4 RuleSetVersionCurrent = RuleSetVersion4 ) const ( RuleActionTypeRoute = "route" RuleActionTypeRouteOptions = "route-options" RuleActionTypeDirect = "direct" RuleActionTypeBypass = "bypass" RuleActionTypeReject = "reject" RuleActionTypeHijackDNS = "hijack-dns" RuleActionTypeSniff = "sniff" RuleActionTypeResolve = "resolve" RuleActionTypePredefined = "predefined" ) const ( RuleActionRejectMethodDefault = "default" RuleActionRejectMethodDrop = "drop" RuleActionRejectMethodReply = "reply" ) ================================================ FILE: constant/speed.go ================================================ package constant const MbpsToBps = 125000 ================================================ FILE: constant/time.go ================================================ package constant const TimeLayout = "2006-01-02 15:04:05 -0700" ================================================ FILE: constant/timeout.go ================================================ package constant import "time" const ( TCPKeepAliveInitial = 5 * time.Minute TCPKeepAliveInterval = 75 * time.Second TCPConnectTimeout = 5 * time.Second TCPTimeout = 15 * time.Second ReadPayloadTimeout = 300 * time.Millisecond DNSTimeout = 10 * time.Second UDPTimeout = 5 * time.Minute DefaultURLTestInterval = 3 * time.Minute DefaultURLTestIdleTimeout = 30 * time.Minute StartTimeout = 10 * time.Second StopTimeout = 5 * time.Second FatalStopTimeout = 10 * time.Second FakeIPMetadataSaveInterval = 10 * time.Second TLSFragmentFallbackDelay = 500 * time.Millisecond ) var PortProtocols = map[uint16]string{ 53: ProtocolDNS, 123: ProtocolNTP, 3478: ProtocolSTUN, 443: ProtocolQUIC, } var ProtocolTimeouts = map[string]time.Duration{ ProtocolDNS: 10 * time.Second, ProtocolNTP: 10 * time.Second, ProtocolSTUN: 10 * time.Second, ProtocolQUIC: 30 * time.Second, ProtocolDTLS: 30 * time.Second, } ================================================ FILE: constant/v2ray.go ================================================ package constant const ( V2RayTransportTypeHTTP = "http" V2RayTransportTypeWebsocket = "ws" V2RayTransportTypeQUIC = "quic" V2RayTransportTypeGRPC = "grpc" V2RayTransportTypeHTTPUpgrade = "httpupgrade" ) ================================================ FILE: constant/version.go ================================================ package constant var Version = "unknown" ================================================ FILE: daemon/deprecated.go ================================================ package daemon import ( "sync" "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing/common" ) var _ deprecated.Manager = (*deprecatedManager)(nil) type deprecatedManager struct { access sync.Mutex notes []deprecated.Note } func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) { m.access.Lock() defer m.access.Unlock() m.notes = common.Uniq(append(m.notes, feature)) } func (m *deprecatedManager) Get() []deprecated.Note { m.access.Lock() defer m.access.Unlock() notes := m.notes m.notes = nil return notes } ================================================ FILE: daemon/instance.go ================================================ package daemon import ( "bytes" "context" "github.com/sagernet/sing-box" "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/deprecated" "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/service" "github.com/sagernet/sing/service/pause" ) type Instance struct { ctx context.Context cancel context.CancelFunc instance *box.Box connectionManager adapter.ConnectionManager clashServer adapter.ClashServer cacheFile adapter.CacheFile pauseManager pause.Manager urlTestHistoryStorage *urltest.HistoryStorage } func (s *StartedService) CheckConfig(configContent string) error { options, err := parseConfig(s.ctx, configContent) if err != nil { return err } ctx, cancel := context.WithCancel(s.ctx) defer cancel() instance, err := box.New(box.Options{ Context: ctx, Options: options, }) if err == nil { instance.Close() } return err } func (s *StartedService) FormatConfig(configContent string) (string, error) { options, err := parseConfig(s.ctx, configContent) if err != nil { return "", err } var buffer bytes.Buffer encoder := json.NewEncoder(&buffer) encoder.SetIndent("", " ") err = encoder.Encode(options) if err != nil { return "", err } return buffer.String(), nil } type OverrideOptions struct { AutoRedirect bool IncludePackage []string ExcludePackage []string } func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) { ctx := s.ctx service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) ctx, cancel := context.WithCancel(include.Context(ctx)) options, err := parseConfig(ctx, profileContent) if err != nil { cancel() return nil, err } if overrideOptions != nil { for _, inbound := range options.Inbounds { if tunInboundOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN { tunInboundOptions.AutoRedirect = overrideOptions.AutoRedirect tunInboundOptions.IncludePackage = append(tunInboundOptions.IncludePackage, overrideOptions.IncludePackage...) tunInboundOptions.ExcludePackage = append(tunInboundOptions.ExcludePackage, overrideOptions.ExcludePackage...) break } } } if s.oomKiller && C.IsIos { if !common.Any(options.Services, func(it option.Service) bool { return it.Type == C.TypeOOMKiller }) { options.Services = append(options.Services, option.Service{ Type: C.TypeOOMKiller, }) } } urlTestHistoryStorage := urltest.NewHistoryStorage() ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) i := &Instance{ ctx: ctx, cancel: cancel, urlTestHistoryStorage: urlTestHistoryStorage, } boxInstance, err := box.New(box.Options{ Context: ctx, Options: options, PlatformLogWriter: s, }) if err != nil { cancel() return nil, err } i.instance = boxInstance i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx) i.clashServer = service.FromContext[adapter.ClashServer](ctx) i.pauseManager = service.FromContext[pause.Manager](ctx) i.cacheFile = service.FromContext[adapter.CacheFile](ctx) log.SetStdLogger(boxInstance.LogFactory().Logger()) return i, nil } func (i *Instance) Start() error { return i.instance.Start() } func (i *Instance) Close() error { i.cancel() i.urlTestHistoryStorage.Close() return i.instance.Close() } func (i *Instance) Box() *box.Box { return i.instance } func (i *Instance) PauseManager() pause.Manager { return i.pauseManager } 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 } ================================================ FILE: daemon/platform.go ================================================ package daemon type PlatformHandler interface { ServiceStop() error ServiceReload() error SystemProxyStatus() (*SystemProxyStatus, error) SetSystemProxyEnabled(enabled bool) error WriteDebugMessage(message string) } ================================================ FILE: daemon/started_service.go ================================================ package daemon import ( "context" "os" "runtime" "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/gofrs/uuid/v5" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" ) var _ StartedServiceServer = (*StartedService)(nil) type StartedService struct { ctx context.Context // platform adapter.PlatformInterface handler PlatformHandler debug bool logMaxLines int oomKiller bool // workingDirectory string // tempDirectory string // userID int // groupID int // systemProxyEnabled bool serviceAccess sync.RWMutex serviceStatus *ServiceStatus serviceStatusSubscriber *observable.Subscriber[*ServiceStatus] serviceStatusObserver *observable.Observer[*ServiceStatus] logAccess sync.RWMutex logLines list.List[*log.Entry] logSubscriber *observable.Subscriber[*log.Entry] logObserver *observable.Observer[*log.Entry] instance *Instance startedAt time.Time urlTestSubscriber *observable.Subscriber[struct{}] urlTestObserver *observable.Observer[struct{}] urlTestHistoryStorage *urltest.HistoryStorage clashModeSubscriber *observable.Subscriber[struct{}] clashModeObserver *observable.Observer[struct{}] connectionEventSubscriber *observable.Subscriber[trafficontrol.ConnectionEvent] connectionEventObserver *observable.Observer[trafficontrol.ConnectionEvent] } type ServiceOptions struct { Context context.Context // Platform adapter.PlatformInterface Handler PlatformHandler Debug bool LogMaxLines int OOMKiller bool // WorkingDirectory string // TempDirectory string // UserID int // GroupID int // SystemProxyEnabled bool } func NewStartedService(options ServiceOptions) *StartedService { s := &StartedService{ ctx: options.Context, // platform: options.Platform, handler: options.Handler, debug: options.Debug, logMaxLines: options.LogMaxLines, oomKiller: options.OOMKiller, // workingDirectory: options.WorkingDirectory, // tempDirectory: options.TempDirectory, // userID: options.UserID, // groupID: options.GroupID, // systemProxyEnabled: options.SystemProxyEnabled, serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE}, serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4), logSubscriber: observable.NewSubscriber[*log.Entry](128), urlTestSubscriber: observable.NewSubscriber[struct{}](1), urlTestHistoryStorage: urltest.NewHistoryStorage(), clashModeSubscriber: observable.NewSubscriber[struct{}](1), connectionEventSubscriber: observable.NewSubscriber[trafficontrol.ConnectionEvent](256), } s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2) s.logObserver = observable.NewObserver(s.logSubscriber, 64) s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1) s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1) s.connectionEventObserver = observable.NewObserver(s.connectionEventSubscriber, 64) return s } func (s *StartedService) resetLogs() { s.logAccess.Lock() s.logLines = list.List[*log.Entry]{} s.logAccess.Unlock() s.logSubscriber.Emit(nil) } func (s *StartedService) updateStatus(newStatus ServiceStatus_Type) { statusObject := &ServiceStatus{Status: newStatus} s.serviceStatusSubscriber.Emit(statusObject) s.serviceStatus = statusObject } func (s *StartedService) updateStatusError(err error) error { statusObject := &ServiceStatus{Status: ServiceStatus_FATAL, ErrorMessage: err.Error()} s.serviceStatusSubscriber.Emit(statusObject) s.serviceStatus = statusObject s.serviceAccess.Unlock() return err } func (s *StartedService) waitForStarted(ctx context.Context) error { s.serviceAccess.RLock() currentStatus := s.serviceStatus.Status s.serviceAccess.RUnlock() switch currentStatus { case ServiceStatus_STARTED: return nil case ServiceStatus_STARTING: default: return os.ErrInvalid } subscription, done, err := s.serviceStatusObserver.Subscribe() if err != nil { return err } defer s.serviceStatusObserver.UnSubscribe(subscription) for { select { case <-ctx.Done(): return ctx.Err() case <-s.ctx.Done(): return s.ctx.Err() case status := <-subscription: switch status.Status { case ServiceStatus_STARTED: return nil case ServiceStatus_FATAL: return E.New(status.ErrorMessage) case ServiceStatus_IDLE, ServiceStatus_STOPPING: return os.ErrInvalid } case <-done: return os.ErrClosed } } } func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error { s.serviceAccess.Lock() switch s.serviceStatus.Status { case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING: default: s.serviceAccess.Unlock() return os.ErrInvalid } oldInstance := s.instance if oldInstance != nil { s.updateStatus(ServiceStatus_STOPPING) s.serviceAccess.Unlock() _ = oldInstance.Close() s.serviceAccess.Lock() } s.updateStatus(ServiceStatus_STARTING) s.resetLogs() instance, err := s.newInstance(profileContent, options) if err != nil { return s.updateStatusError(err) } s.instance = instance instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber) if instance.clashServer != nil { instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber) instance.clashServer.(*clashapi.Server).TrafficManager().SetEventHook(s.connectionEventSubscriber) } s.serviceAccess.Unlock() err = instance.Start() s.serviceAccess.Lock() if s.serviceStatus.Status != ServiceStatus_STARTING { s.serviceAccess.Unlock() return nil } if err != nil { return s.updateStatusError(err) } s.startedAt = time.Now() s.updateStatus(ServiceStatus_STARTED) s.serviceAccess.Unlock() runtime.GC() return nil } func (s *StartedService) Close() { s.serviceStatusSubscriber.Close() s.logSubscriber.Close() s.urlTestSubscriber.Close() s.clashModeSubscriber.Close() s.connectionEventSubscriber.Close() } func (s *StartedService) CloseService() error { s.serviceAccess.Lock() switch s.serviceStatus.Status { case ServiceStatus_STARTING, ServiceStatus_STARTED: default: s.serviceAccess.Unlock() return os.ErrInvalid } s.updateStatus(ServiceStatus_STOPPING) if s.instance != nil { err := s.instance.Close() if err != nil { return s.updateStatusError(err) } } s.instance = nil s.startedAt = time.Time{} s.updateStatus(ServiceStatus_IDLE) s.serviceAccess.Unlock() runtime.GC() return nil } func (s *StartedService) SetError(err error) { s.serviceAccess.Lock() s.updateStatusError(err) s.WriteMessage(log.LevelError, err.Error()) } func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { err := s.handler.ServiceStop() if err != nil { return nil, err } return &emptypb.Empty{}, nil } func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { err := s.handler.ServiceReload() if err != nil { return nil, err } return &emptypb.Empty{}, nil } func (s *StartedService) SubscribeServiceStatus(empty *emptypb.Empty, server grpc.ServerStreamingServer[ServiceStatus]) error { subscription, done, err := s.serviceStatusObserver.Subscribe() if err != nil { return err } defer s.serviceStatusObserver.UnSubscribe(subscription) err = server.Send(s.serviceStatus) if err != nil { return err } for { select { case <-s.ctx.Done(): return s.ctx.Err() case <-server.Context().Done(): return server.Context().Err() case newStatus := <-subscription: err = server.Send(newStatus) if err != nil { return err } case <-done: return nil } } } func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerStreamingServer[Log]) error { var savedLines []*log.Entry s.logAccess.Lock() savedLines = make([]*log.Entry, 0, s.logLines.Len()) for element := s.logLines.Front(); element != nil; element = element.Next() { savedLines = append(savedLines, element.Value) } subscription, done, err := s.logObserver.Subscribe() s.logAccess.Unlock() if err != nil { return err } defer s.logObserver.UnSubscribe(subscription) err = server.Send(&Log{ Messages: common.Map(savedLines, func(it *log.Entry) *Log_Message { return &Log_Message{ Level: LogLevel(it.Level), Message: it.Message, } }), Reset_: true, }) if err != nil { return err } for { select { case <-s.ctx.Done(): return s.ctx.Err() case <-server.Context().Done(): return server.Context().Err() case message := <-subscription: var rawMessage Log if message == nil { rawMessage.Reset_ = true } else { rawMessage.Messages = append(rawMessage.Messages, &Log_Message{ Level: LogLevel(message.Level), Message: message.Message, }) } fetch: for { select { case message = <-subscription: if message == nil { rawMessage.Messages = nil rawMessage.Reset_ = true } else { rawMessage.Messages = append(rawMessage.Messages, &Log_Message{ Level: LogLevel(message.Level), Message: message.Message, }) } default: break fetch } } err = server.Send(&rawMessage) if err != nil { return err } case <-done: return nil } } } func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.Empty) (*DefaultLogLevel, error) { s.serviceAccess.RLock() switch s.serviceStatus.Status { case ServiceStatus_STARTING, ServiceStatus_STARTED: default: s.serviceAccess.RUnlock() return nil, os.ErrInvalid } logLevel := s.instance.instance.LogFactory().Level() s.serviceAccess.RUnlock() return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil } func (s *StartedService) ClearLogs(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { s.resetLogs() return &emptypb.Empty{}, nil } func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error { interval := time.Duration(request.Interval) if interval <= 0 { interval = time.Second // Default to 1 second } ticker := time.NewTicker(interval) defer ticker.Stop() status := s.readStatus() uploadTotal := status.UplinkTotal downloadTotal := status.DownlinkTotal for { err := server.Send(status) if err != nil { return err } select { case <-s.ctx.Done(): return s.ctx.Err() case <-server.Context().Done(): return server.Context().Err() case <-ticker.C: } status = s.readStatus() upload := status.UplinkTotal - uploadTotal download := status.DownlinkTotal - downloadTotal uploadTotal = status.UplinkTotal downloadTotal = status.DownlinkTotal status.Uplink = upload status.Downlink = download } } func (s *StartedService) readStatus() *Status { var status Status status.Memory = memory.Total() status.Goroutines = int32(runtime.NumGoroutine()) s.serviceAccess.RLock() nowService := s.instance s.serviceAccess.RUnlock() if nowService != nil && nowService.connectionManager != nil { status.ConnectionsOut = int32(nowService.connectionManager.Count()) } if nowService != nil { if clashServer := nowService.clashServer; clashServer != nil { status.TrafficAvailable = true trafficManager := clashServer.(*clashapi.Server).TrafficManager() status.UplinkTotal, status.DownlinkTotal = trafficManager.Total() status.ConnectionsIn = int32(trafficManager.ConnectionsLen()) } } return &status } func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) 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 } groups := s.readGroups() s.serviceAccess.RUnlock() err = server.Send(groups) 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 (s *StartedService) readGroups() *Groups { historyStorage := s.instance.urlTestHistoryStorage boxService := s.instance outbounds := boxService.instance.Outbound().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { if group, isGroup := it.(adapter.OutboundGroup); isGroup { iGroups = append(iGroups, group) } } var gs Groups for _, iGroup := range iGroups { var g Group g.Tag = iGroup.Tag() g.Type = iGroup.Type() _, g.Selectable = iGroup.(*group.Selector) g.Selected = iGroup.Now() if boxService.cacheFile != nil { if isExpand, loaded := boxService.cacheFile.LoadGroupExpand(g.Tag); loaded { g.IsExpand = isExpand } } for _, itemTag := range iGroup.All() { itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag) if !isLoaded { continue } var item GroupItem item.Tag = itemTag item.Type = itemOutbound.Type() if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil { item.UrlTestTime = history.Time.Unix() item.UrlTestDelay = int32(history.Delay) } g.Items = append(g.Items, &item) } if len(g.Items) < 2 { continue } gs.Group = append(gs.Group, &g) } return &gs } func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.Empty) (*ClashModeStatus, error) { s.serviceAccess.RLock() if s.serviceStatus.Status != ServiceStatus_STARTED { s.serviceAccess.RUnlock() return nil, os.ErrInvalid } clashServer := s.instance.clashServer s.serviceAccess.RUnlock() if clashServer == nil { return nil, os.ErrInvalid } return &ClashModeStatus{ ModeList: clashServer.ModeList(), CurrentMode: clashServer.Mode(), }, nil } func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error { err := s.waitForStarted(server.Context()) if err != nil { return err } subscription, done, err := s.clashModeObserver.Subscribe() if err != nil { return err } defer s.clashModeObserver.UnSubscribe(subscription) for { s.serviceAccess.RLock() if s.serviceStatus.Status != ServiceStatus_STARTED { s.serviceAccess.RUnlock() return os.ErrInvalid } message := &ClashMode{Mode: s.instance.clashServer.Mode()} s.serviceAccess.RUnlock() err = server.Send(message) 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 (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (*emptypb.Empty, error) { s.serviceAccess.RLock() if s.serviceStatus.Status != ServiceStatus_STARTED { s.serviceAccess.RUnlock() return nil, os.ErrInvalid } clashServer := s.instance.clashServer s.serviceAccess.RUnlock() clashServer.(*clashapi.Server).SetMode(request.Mode) return &emptypb.Empty{}, nil } func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (*emptypb.Empty, error) { s.serviceAccess.RLock() if s.serviceStatus.Status != ServiceStatus_STARTED { s.serviceAccess.RUnlock() return nil, os.ErrInvalid } boxService := s.instance s.serviceAccess.RUnlock() groupTag := request.OutboundTag abstractOutboundGroup, isLoaded := boxService.instance.Outbound().Outbound(groupTag) if !isLoaded { return nil, E.New("outbound group not found: ", groupTag) } outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup) if !isOutboundGroup { return nil, E.New("outbound is not a group: ", groupTag) } urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest) if isURLTest { go urlTest.CheckOutbounds() } else { historyStorage := boxService.urlTestHistoryStorage outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { itOutbound, _ := boxService.instance.Outbound().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { return false } _, isGroup := it.(adapter.OutboundGroup) if isGroup { return false } return true }) b, _ := batch.New(boxService.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { t, err := urltest.URLTest(boxService.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { historyStorage.StoreURLTestHistory(outboundTag, &adapter.URLTestHistory{ Time: time.Now(), Delay: t, }) } return nil, nil }) } } return &emptypb.Empty{}, nil } func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutboundRequest) (*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.instance s.serviceAccess.RUnlock() outboundGroup, isLoaded := boxService.Outbound().Outbound(request.GroupTag) if !isLoaded { return nil, E.New("selector not found: ", request.GroupTag) } selector, isSelector := outboundGroup.(*group.Selector) if !isSelector { return nil, E.New("outbound is not a selector: ", request.GroupTag) } if !selector.SelectOutbound(request.OutboundTag) { return nil, E.New("outbound not found in selector: ", request.OutboundTag) } s.urlTestObserver.Emit(struct{}{}) return &emptypb.Empty{}, nil } func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupExpandRequest) (*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() if boxService.cacheFile != nil { err := boxService.cacheFile.StoreGroupExpand(request.GroupTag, request.IsExpand) if err != nil { return nil, err } } return &emptypb.Empty{}, nil } func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) { return s.handler.SystemProxyStatus() } func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { err := s.handler.SetSystemProxyEnabled(request.Enabled) if err != nil { return nil, err } return nil, err } func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error { err := s.waitForStarted(server.Context()) if err != nil { return err } s.serviceAccess.RLock() boxService := s.instance s.serviceAccess.RUnlock() if boxService.clashServer == nil { return E.New("clash server not available") } trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager() subscription, done, err := s.connectionEventObserver.Subscribe() if err != nil { return err } defer s.connectionEventObserver.UnSubscribe(subscription) connectionSnapshots := make(map[uuid.UUID]connectionSnapshot) initialEvents := s.buildInitialConnectionState(trafficManager, connectionSnapshots) err = server.Send(&ConnectionEvents{ Events: initialEvents, Reset_: true, }) if err != nil { return err } interval := time.Duration(request.Interval) if interval <= 0 { interval = time.Second } ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-s.ctx.Done(): return s.ctx.Err() case <-server.Context().Done(): return server.Context().Err() case <-done: return nil case event := <-subscription: var pendingEvents []*ConnectionEvent if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil { pendingEvents = append(pendingEvents, protoEvent) } drain: for { select { case event = <-subscription: if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil { pendingEvents = append(pendingEvents, protoEvent) } default: break drain } } if len(pendingEvents) > 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, PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName, } } 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, } }), }, 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) 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 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 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[16] 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[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 SubscribeConnectionsRequest.ProtoReflect.Descriptor instead. func (*SubscribeConnectionsRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{16} } 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[17] 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[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 ConnectionEvent.ProtoReflect.Descriptor instead. func (*ConnectionEvent) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{17} } 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[18] 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[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 ConnectionEvents.ProtoReflect.Descriptor instead. func (*ConnectionEvents) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{18} } 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[19] 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[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 Connection.ProtoReflect.Descriptor instead. func (*Connection) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{19} } 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"` PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ProcessInfo) Reset() { *x = ProcessInfo{} mi := &file_daemon_started_service_proto_msgTypes[20] 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[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 ProcessInfo.ProtoReflect.Descriptor instead. func (*ProcessInfo) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{20} } 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) GetPackageName() string { if x != nil { return x.PackageName } return "" } 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[21] 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[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 CloseConnectionRequest.ProtoReflect.Descriptor instead. func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{21} } 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[22] 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[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 DeprecatedWarnings.ProtoReflect.Descriptor instead. func (*DeprecatedWarnings) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{22} } 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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeprecatedWarning) Reset() { *x = DeprecatedWarning{} mi := &file_daemon_started_service_proto_msgTypes[23] 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[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 DeprecatedWarning.ProtoReflect.Descriptor instead. func (*DeprecatedWarning) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{23} } 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 "" } 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[24] 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[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 StartedAt.ProtoReflect.Descriptor instead. func (*StartedAt) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{24} } func (x *StartedAt) GetStartedAt() int64 { if x != nil { return x.StartedAt } return 0 } type Log_Message struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` 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[25] 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[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 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\"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\"\xa3\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" + "\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\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\"q\n" + "\x11DeprecatedWarning\x12\x18\n" + "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + "\timpending\x18\x02 \x01(\bR\timpending\x12$\n" + "\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\")\n" + "\tStartedAt\x12\x1c\n" + "\tstartedAt\x18\x01 \x01(\x03R\tstartedAt*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\xe5\v\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\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\"\x00B%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, 3) file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType (ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type (*ServiceStatus)(nil), // 3: daemon.ServiceStatus (*ReloadServiceRequest)(nil), // 4: daemon.ReloadServiceRequest (*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest (*Log)(nil), // 6: daemon.Log (*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel (*Status)(nil), // 8: daemon.Status (*Groups)(nil), // 9: daemon.Groups (*Group)(nil), // 10: daemon.Group (*GroupItem)(nil), // 11: daemon.GroupItem (*URLTestRequest)(nil), // 12: daemon.URLTestRequest (*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest (*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest (*ClashMode)(nil), // 15: daemon.ClashMode (*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus (*SystemProxyStatus)(nil), // 17: daemon.SystemProxyStatus (*SetSystemProxyEnabledRequest)(nil), // 18: daemon.SetSystemProxyEnabledRequest (*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest (*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent (*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents (*Connection)(nil), // 22: daemon.Connection (*ProcessInfo)(nil), // 23: daemon.ProcessInfo (*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest (*DeprecatedWarnings)(nil), // 25: daemon.DeprecatedWarnings (*DeprecatedWarning)(nil), // 26: daemon.DeprecatedWarning (*StartedAt)(nil), // 27: daemon.StartedAt (*Log_Message)(nil), // 28: daemon.Log.Message (*emptypb.Empty)(nil), // 29: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type 28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 10, // 3: daemon.Groups.group:type_name -> daemon.Group 11, // 4: daemon.Group.items:type_name -> daemon.GroupItem 1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType 22, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection 20, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent 23, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo 26, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning 0, // 10: daemon.Log.Message.level:type_name -> daemon.LogLevel 29, // 11: daemon.StartedService.StopService:input_type -> google.protobuf.Empty 29, // 12: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty 29, // 13: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty 29, // 14: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty 29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty 29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty 5, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest 29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty 29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty 29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty 15, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode 12, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest 13, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest 14, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest 29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty 18, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest 19, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest 24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest 29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty 29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty 29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty 29, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty 29, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty 3, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus 6, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log 7, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel 29, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty 8, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status 9, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups 16, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus 15, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode 29, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty 29, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty 29, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty 29, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty 17, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus 29, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty 21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents 29, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty 29, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty 25, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings 27, // 52: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt 32, // [32:53] is the sub-list for method output_type 11, // [11:32] is the sub-list for method input_type 11, // [11:11] is the sub-list for extension type_name 11, // [11:11] is the sub-list for extension extendee 0, // [0:11] is the sub-list for field type_name } 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: 3, NumMessages: 26, 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 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) {} } 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 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; string packageName = 5; } message CloseConnectionRequest { string id = 1; } message DeprecatedWarnings { repeated DeprecatedWarning warnings = 1; } message DeprecatedWarning { string message = 1; bool impending = 2; string migrationLink = 3; } message StartedAt { int64 startedAt = 1; } ================================================ 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_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" ) // 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) 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) } 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) 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 } // 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) 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) 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) 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) 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_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) } // 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: "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, }, }, 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" "strings" "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" 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/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 { timeout time.Duration disableCache bool disableExpire bool independentCache bool clientSubnet netip.Prefix rdrc adapter.RDRCStore initRDRCFunc func() adapter.RDRCStore logger logger.ContextLogger cache freelru.Cache[dns.Question, *dns.Msg] cacheLock compatible.Map[dns.Question, chan struct{}] transportCache freelru.Cache[transportCacheKey, *dns.Msg] transportCacheLock compatible.Map[dns.Question, chan struct{}] } type ClientOptions struct { Timeout time.Duration DisableCache bool DisableExpire bool IndependentCache bool CacheCapacity uint32 ClientSubnet netip.Prefix RDRC func() adapter.RDRCStore Logger logger.ContextLogger } func NewClient(options ClientOptions) *Client { client := &Client{ timeout: options.Timeout, disableCache: options.DisableCache, disableExpire: options.DisableExpire, independentCache: options.IndependentCache, clientSubnet: options.ClientSubnet, initRDRCFunc: options.RDRC, logger: options.Logger, } if client.timeout == 0 { client.timeout = C.DNSTimeout } cacheCapacity := options.CacheCapacity if cacheCapacity < 1024 { cacheCapacity = 1024 } if !client.disableCache { if !client.independentCache { client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32)) } else { client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32)) } } return client } type transportCacheKey struct { dns.Question transportTag string } func (c *Client) Start() { if c.initRDRCFunc != nil { c.rdrc = c.initRDRCFunc() } } 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 (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) 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 } clientSubnet := options.ClientSubnet if !clientSubnet.IsValid() { clientSubnet = c.clientSubnet } if clientSubnet.IsValid() { message = SetClientSubnet(message, clientSubnet) } 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 { if c.cache != nil { cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{})) if loaded { select { case <-cond: case <-ctx.Done(): return nil, ctx.Err() } } else { defer func() { c.cacheLock.Delete(question) close(cond) }() } } else if c.transportCache != nil { cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{})) if loaded { select { case <-cond: case <-ctx.Done(): return nil, ctx.Err() } } else { defer func() { c.transportCacheLock.Delete(question) close(cond) }() } } response, ttl := c.loadResponse(question, transport) if response != nil { 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 } } ctx, cancel := context.WithTimeout(ctx, c.timeout) response, err := transport.Exchange(ctx, message) cancel() if err != nil { var rcodeError RcodeError if errors.As(err, &rcodeError) { response = FixedResponseStatus(message, int(rcodeError)) } else { return nil, err } } /*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA { validResponse := response loop: for { var ( addresses int queryCNAME string ) for _, rawRR := range validResponse.Answer { switch rr := rawRR.(type) { case *dns.A: break loop case *dns.AAAA: break loop case *dns.CNAME: queryCNAME = rr.Target } } if queryCNAME == "" { break } exMessage := *message exMessage.Question = []dns.Question{{ Name: queryCNAME, Qtype: question.Qtype, }} validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker) if err != nil { return nil, err } } if validResponse != response { response.Answer = append(response.Answer, validResponse.Answer...) } }*/ disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError) if responseChecker != nil { var rejected bool // TODO: add accept_any rule and support to check response instead of addresses if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { rejected = true } else if len(response.Answer) == 0 { rejected = !responseChecker(nil) } else { rejected = !responseChecker(MessageToAddresses(response)) } 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 } } if question.Qtype == dns.TypeHTTPS { if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only { for _, rr := range response.Answer { https, isHTTPS := rr.(*dns.HTTPS) if !isHTTPS { continue } content := https.SVCB content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool { if options.Strategy == C.DomainStrategyIPv4Only { return it.Key() != dns.SVCB_IPV6HINT } else { return it.Key() != dns.SVCB_IPV4HINT } }) https.SVCB = content } } } var timeToLive uint32 if len(response.Answer) == 0 { if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA { timeToLive = soaTTL } } if timeToLive == 0 { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { timeToLive = record.Header().Ttl } } } } if options.RewriteTTL != nil { timeToLive = *options.RewriteTTL } for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { record.Header().Ttl = timeToLive } } 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(responseAddrs []netip.Addr) 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 } if strategy == C.DomainStrategyIPv4Only { return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) } else if strategy == 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() } else if c.transportCache != nil { c.transportCache.Purge() } } 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.disableExpire { if !c.independentCache { c.cache.Add(question, message) } else { c.transportCache.Add(transportCacheKey{ Question: question, transportTag: transport.Tag(), }, message) } } else { if !c.independentCache { c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive)) } else { c.transportCache.AddWithLifetime(transportCacheKey{ Question: question, transportTag: transport.Tag(), }, message, time.Second*time.Duration(timeToLive)) } } } func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { question := dns.Question{ Name: name, Qtype: qType, Qclass: dns.ClassINET, } disableCache := c.disableCache || options.DisableCache if !disableCache { cachedAddresses, err := c.questionCache(question, transport) if err != ErrNotCached { return cachedAddresses, err } } message := dns.Msg{ MsgHdr: dns.MsgHdr{ RecursionDesired: true, }, Question: []dns.Question{question}, } 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(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) { response, _ := c.loadResponse(question, transport) if response == nil { return nil, ErrNotCached } if response.Rcode != dns.RcodeSuccess { return nil, RcodeError(response.Rcode) } return MessageToAddresses(response), nil } func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) { var ( response *dns.Msg loaded bool ) if c.disableExpire { if !c.independentCache { response, loaded = c.cache.Get(question) } else { response, loaded = c.transportCache.Get(transportCacheKey{ Question: question, transportTag: transport.Tag(), }) } if !loaded { return nil, 0 } return response.Copy(), 0 } else { var expireAt time.Time if !c.independentCache { response, expireAt, loaded = c.cache.GetWithLifetime(question) } else { response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{ Question: question, transportTag: transport.Tag(), }) } if !loaded { return nil, 0 } timeNow := time.Now() if timeNow.After(expireAt) { if !c.independentCache { c.cache.Remove(question) } else { c.transportCache.Remove(transportCacheKey{ Question: question, transportTag: transport.Tag(), }) } return nil, 0 } var originTTL int for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL { originTTL = int(record.Header().Ttl) } } } nowTTL := int(expireAt.Sub(timeNow).Seconds()) if nowTTL < 0 { nowTTL = 0 } response = response.Copy() if originTTL > 0 { duration := uint32(originTTL - nowTTL) for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { record.Header().Ttl = record.Header().Ttl - duration } } } else { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { record.Header().Ttl = uint32(nowTTL) } } } return response, nowTTL } } func MessageToAddresses(response *dns.Msg) []netip.Addr { if response == nil || response.Rcode != dns.RcodeSuccess { return nil } addresses := make([]netip.Addr, 0, len(response.Answer)) for _, rawAnswer := range response.Answer { switch answer := rawAnswer.(type) { case *dns.A: addresses = append(addresses, M.AddrFromIP(answer.A)) case *dns.AAAA: addresses = append(addresses, M.AddrFromIP(answer.AAAA)) case *dns.HTTPS: for _, value := range answer.SVCB.Value { if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT { addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...) } } } } return addresses } func wrapError(err error) error { switch dnsErr := err.(type) { case *net.DNSError: if dnsErr.IsNotFound { return RcodeNameError } case *net.AddrError: return RcodeNameError } return err } 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 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 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 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/router.go ================================================ package dns import ( "context" "errors" "net/netip" "strings" "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-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/contrab/freelru" "github.com/sagernet/sing/contrab/maphash" "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" ) var _ adapter.DNSRouter = (*Router)(nil) type Router struct { ctx context.Context logger logger.ContextLogger transport adapter.DNSTransportManager outbound adapter.OutboundManager client adapter.DNSClient rules []adapter.DNSRule defaultDomainStrategy C.DomainStrategy dnsReverseMapping freelru.Cache[netip.Addr, string] platformInterface adapter.PlatformInterface } func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router { router := &Router{ ctx: ctx, logger: logFactory.NewLogger("dns"), transport: service.FromContext[adapter.DNSTransportManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), rules: make([]adapter.DNSRule, 0, len(options.Rules)), defaultDomainStrategy: C.DomainStrategy(options.Strategy), } router.client = NewClient(ClientOptions{ DisableCache: options.DNSClientOptions.DisableCache, DisableExpire: options.DNSClientOptions.DisableExpire, IndependentCache: options.DNSClientOptions.IndependentCache, CacheCapacity: options.DNSClientOptions.CacheCapacity, ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), RDRC: func() adapter.RDRCStore { cacheFile := service.FromContext[adapter.CacheFile](ctx) if cacheFile == nil { return nil } if !cacheFile.StoreRDRC() { return nil } 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 } func (r *Router) Initialize(rules []option.DNSRule) error { for i, ruleOptions := range rules { dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true) if err != nil { return E.Cause(err, "parse dns rule[", i, "]") } r.rules = append(r.rules, dnsRule) } 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() for i, rule := range r.rules { monitor.Start("initialize DNS rule[", i, "]") err := rule.Start() monitor.Finish() if err != nil { return E.Cause(err, "initialize DNS rule[", i, "]") } } } return nil } func (r *Router) Close() error { monitor := taskmonitor.New(r.logger, C.StopTimeout) var err error for i, rule := range r.rules { monitor.Start("close dns rule[", i, "]") err = E.Append(err, rule.Close(), func(err error) error { return E.Cause(err, "close dns rule[", i, "]") }) monitor.Finish() } return err } func (r *Router) matchDNS(ctx context.Context, 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(r.rules); currentRuleIndex++ { currentRule := r.rules[currentRuleIndex] if currentRule.WithAddressLimit() && !isAddressQuery { continue } metadata.ResetRuleCache() if currentRule.Match(metadata) { displayRuleIndex := currentRuleIndex if displayRuleIndex != -1 { displayRuleIndex += displayRuleIndex + 1 } ruleDescription := currentRule.String() if ruleDescription != "" { r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action()) } else { r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", 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.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { if options.Strategy == C.DomainStrategyAsIS { options.Strategy = legacyTransport.LegacyStrategy() } if !options.ClientSubnet.IsValid() { options.ClientSubnet = legacyTransport.LegacyClientSubnet() } } return transport, currentRule, currentRuleIndex case *R.RuleActionDNSRouteOptions: if action.Strategy != C.DomainStrategyAsIS { options.Strategy = action.Strategy } if action.DisableCache { options.DisableCache = true } if action.RewriteTTL != nil { options.RewriteTTL = action.RewriteTTL } 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() if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { if options.Strategy == C.DomainStrategyAsIS { options.Strategy = legacyTransport.LegacyStrategy() } if !options.ClientSubnet.IsValid() { options.ClientSubnet = legacyTransport.LegacyClientSubnet() } } return transport, nil, -1 } 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.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 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 legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { if options.Strategy == C.DomainStrategyAsIS { options.Strategy = legacyTransport.LegacyStrategy() } if !options.ClientSubnet.IsValid() { options.ClientSubnet = legacyTransport.LegacyClientSubnet() } } if options.Strategy == C.DomainStrategyAsIS { options.Strategy = r.defaultDomainStrategy } response, err = r.client.Exchange(ctx, transport, message, options, nil) } else { var ( rule adapter.DNSRule ruleIndex int ) ruleIndex = -1 for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options transport, rule, ruleIndex = r.matchDNS(ctx, 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: return action.Response(message), nil } } 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 } } 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) { 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 { 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) if options.Transport != nil { transport := options.Transport if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { if options.Strategy == C.DomainStrategyAsIS { options.Strategy = legacyTransport.LegacyStrategy() } if !options.ClientSubnet.IsValid() { options.ClientSubnet = legacyTransport.LegacyClientSubnet() } } if options.Strategy == C.DomainStrategyAsIS { options.Strategy = r.defaultDomainStrategy } responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil) } else { var ( transport adapter.DNSTransport rule adapter.DNSRule ruleIndex int ) ruleIndex = -1 for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options transport, rule, ruleIndex = r.matchDNS(ctx, 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(responseAddrs []netip.Addr) bool { if rule == nil || !rule.WithAddressLimit() { return nil } responseMetadata := *metadata return func(responseAddrs []netip.Addr) bool { checkMetadata := responseMetadata checkMetadata.DestinationAddresses = responseAddrs return rule.MatchAddressLimit(&checkMetadata) } } func (r *Router) ClearCache() { r.client.ClearCache() if r.platformInterface != nil { r.platformInterface.ClearDNSCache() } } 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() } } ================================================ FILE: dns/transport/base.go ================================================ package transport import ( "context" "os" "sync" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" ) type TransportState int const ( StateNew TransportState = iota StateStarted StateClosing StateClosed ) var ( ErrTransportClosed = os.ErrClosed ErrConnectionReset = E.New("connection reset") ) type BaseTransport struct { dns.TransportAdapter Logger logger.ContextLogger mutex sync.Mutex state TransportState inFlight int32 queriesComplete chan struct{} closeCtx context.Context closeCancel context.CancelFunc } func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport { ctx, cancel := context.WithCancel(context.Background()) return &BaseTransport{ TransportAdapter: adapter, Logger: logger, state: StateNew, closeCtx: ctx, closeCancel: cancel, } } func (t *BaseTransport) State() TransportState { t.mutex.Lock() defer t.mutex.Unlock() return t.state } func (t *BaseTransport) SetStarted() error { t.mutex.Lock() defer t.mutex.Unlock() switch t.state { case StateNew: t.state = StateStarted return nil case StateStarted: return nil default: return ErrTransportClosed } } func (t *BaseTransport) BeginQuery() bool { t.mutex.Lock() defer t.mutex.Unlock() if t.state != StateStarted { return false } t.inFlight++ return true } func (t *BaseTransport) EndQuery() { t.mutex.Lock() if t.inFlight > 0 { t.inFlight-- } if t.inFlight == 0 && t.queriesComplete != nil { close(t.queriesComplete) t.queriesComplete = nil } t.mutex.Unlock() } func (t *BaseTransport) CloseContext() context.Context { return t.closeCtx } func (t *BaseTransport) Shutdown(ctx context.Context) error { t.mutex.Lock() if t.state >= StateClosing { t.mutex.Unlock() return nil } if t.state == StateNew { t.state = StateClosed t.mutex.Unlock() t.closeCancel() return nil } t.state = StateClosing if t.inFlight == 0 { t.state = StateClosed t.mutex.Unlock() t.closeCancel() return nil } t.queriesComplete = make(chan struct{}) queriesComplete := t.queriesComplete t.mutex.Unlock() t.closeCancel() select { case <-queriesComplete: t.mutex.Lock() t.state = StateClosed t.mutex.Unlock() return nil case <-ctx.Done(): t.mutex.Lock() t.state = StateClosed t.mutex.Unlock() return ctx.Err() } } func (t *BaseTransport) Close() error { ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout) defer cancel() return t.Shutdown(ctx) } ================================================ FILE: dns/transport/connector.go ================================================ package transport import ( "context" "net" "sync" "time" E "github.com/sagernet/sing/common/exceptions" ) type ConnectorCallbacks[T any] struct { IsClosed func(connection T) bool Close func(connection T) Reset func(connection T) } type Connector[T any] struct { dial func(ctx context.Context) (T, error) callbacks ConnectorCallbacks[T] access sync.Mutex connection T hasConnection bool connectionCancel context.CancelFunc connecting chan struct{} closeCtx context.Context closed bool } func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] { return &Connector[T]{ dial: dial, callbacks: callbacks, closeCtx: closeCtx, } } func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] { return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{ IsClosed: func(connection *Connection) bool { return connection.IsClosed() }, Close: func(connection *Connection) { connection.CloseWithError(ErrTransportClosed) }, Reset: func(connection *Connection) { connection.CloseWithError(ErrConnectionReset) }, }) } type contextKeyConnecting struct{} var errRecursiveConnectorDial = E.New("recursive connector dial") type connectorDialResult[T any] struct { connection T cancel context.CancelFunc err error } func (c *Connector[T]) Get(ctx context.Context) (T, error) { var zero T for { c.access.Lock() if c.closed { c.access.Unlock() return zero, ErrTransportClosed } if c.hasConnection && !c.callbacks.IsClosed(c.connection) { connection := c.connection c.access.Unlock() return connection, nil } c.hasConnection = false if c.connectionCancel != nil { c.connectionCancel() c.connectionCancel = nil } if isRecursiveConnectorDial(ctx, c) { c.access.Unlock() return zero, errRecursiveConnectorDial } if c.connecting != nil { connecting := c.connecting c.access.Unlock() select { case <-connecting: continue case <-ctx.Done(): return zero, ctx.Err() case <-c.closeCtx.Done(): return zero, ErrTransportClosed } } if err := ctx.Err(); err != nil { c.access.Unlock() return zero, err } connecting := make(chan struct{}) c.connecting = connecting dialContext := context.WithValue(ctx, contextKeyConnecting{}, c) dialResult := make(chan connectorDialResult[T], 1) c.access.Unlock() go func() { connection, cancel, err := c.dialWithCancellation(dialContext) dialResult <- connectorDialResult[T]{ connection: connection, cancel: cancel, err: err, } }() select { case result := <-dialResult: return c.completeDial(ctx, connecting, result) case <-ctx.Done(): go func() { result := <-dialResult _, _ = c.completeDial(ctx, connecting, result) }() return zero, ctx.Err() case <-c.closeCtx.Done(): go func() { result := <-dialResult _, _ = c.completeDial(ctx, connecting, result) }() return zero, ErrTransportClosed } } } func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool { dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T]) return loaded && dialConnector == connector } func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) { var zero T c.access.Lock() defer c.access.Unlock() defer func() { if c.connecting == connecting { c.connecting = nil } close(connecting) }() if result.err != nil { return zero, result.err } if c.closed || c.closeCtx.Err() != nil { result.cancel() c.callbacks.Close(result.connection) return zero, ErrTransportClosed } if err := ctx.Err(); err != nil { result.cancel() c.callbacks.Close(result.connection) return zero, err } c.connection = result.connection c.hasConnection = true c.connectionCancel = result.cancel return c.connection, nil } func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) { var zero T if err := ctx.Err(); err != nil { return zero, nil, err } connCtx, cancel := context.WithCancel(c.closeCtx) var ( stateAccess sync.Mutex dialComplete bool ) stopCancel := context.AfterFunc(ctx, func() { stateAccess.Lock() if !dialComplete { cancel() } stateAccess.Unlock() }) select { case <-ctx.Done(): stateAccess.Lock() dialComplete = true stateAccess.Unlock() stopCancel() cancel() return zero, nil, ctx.Err() default: } connection, err := c.dial(valueContext{connCtx, ctx}) stateAccess.Lock() dialComplete = true stateAccess.Unlock() stopCancel() if err != nil { cancel() return zero, nil, err } return connection, cancel, nil } type valueContext struct { context.Context parent context.Context } func (v valueContext) Value(key any) any { return v.parent.Value(key) } func (v valueContext) Deadline() (time.Time, bool) { return v.parent.Deadline() } func (c *Connector[T]) Close() error { c.access.Lock() defer c.access.Unlock() if c.closed { return nil } c.closed = true if c.connectionCancel != nil { c.connectionCancel() c.connectionCancel = nil } if c.hasConnection { c.callbacks.Close(c.connection) c.hasConnection = false } return nil } func (c *Connector[T]) Reset() { c.access.Lock() defer c.access.Unlock() if c.connectionCancel != nil { c.connectionCancel() c.connectionCancel = nil } if c.hasConnection { c.callbacks.Reset(c.connection) c.hasConnection = false } } type Connection struct { net.Conn closeOnce sync.Once done chan struct{} closeError error } func WrapConnection(conn net.Conn) *Connection { return &Connection{ Conn: conn, done: make(chan struct{}), } } func (c *Connection) Done() <-chan struct{} { return c.done } func (c *Connection) IsClosed() bool { select { case <-c.done: return true default: return false } } func (c *Connection) CloseError() error { select { case <-c.done: if c.closeError != nil { return c.closeError } return ErrTransportClosed default: return nil } } func (c *Connection) Close() error { return c.CloseWithError(ErrTransportClosed) } func (c *Connection) CloseWithError(err error) error { var returnError error c.closeOnce.Do(func() { c.closeError = err returnError = c.Conn.Close() close(c.done) }) return returnError } ================================================ FILE: dns/transport/connector_test.go ================================================ package transport import ( "context" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" ) type testConnectorConnection struct{} func TestConnectorRecursiveGetFailsFast(t *testing.T) { t.Parallel() var ( dialCount atomic.Int32 closeCount atomic.Int32 connector *Connector[*testConnectorConnection] ) dial := func(ctx context.Context) (*testConnectorConnection, error) { dialCount.Add(1) _, err := connector.Get(ctx) if err != nil { return nil, err } return &testConnectorConnection{}, nil } connector = NewConnector(context.Background(), dial, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) { closeCount.Add(1) }, Reset: func(connection *testConnectorConnection) { closeCount.Add(1) }, }) _, err := connector.Get(context.Background()) require.ErrorIs(t, err, errRecursiveConnectorDial) require.EqualValues(t, 1, dialCount.Load()) require.EqualValues(t, 0, closeCount.Load()) } func TestConnectorRecursiveGetAcrossConnectorsAllowed(t *testing.T) { t.Parallel() var ( outerDialCount atomic.Int32 innerDialCount atomic.Int32 outerConnector *Connector[*testConnectorConnection] innerConnector *Connector[*testConnectorConnection] ) innerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { innerDialCount.Add(1) return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) {}, Reset: func(connection *testConnectorConnection) {}, }) outerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { outerDialCount.Add(1) _, err := innerConnector.Get(ctx) if err != nil { return nil, err } return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) {}, Reset: func(connection *testConnectorConnection) {}, }) _, err := outerConnector.Get(context.Background()) require.NoError(t, err) require.EqualValues(t, 1, outerDialCount.Load()) require.EqualValues(t, 1, innerDialCount.Load()) } func TestConnectorDialContextPreservesValueAndDeadline(t *testing.T) { t.Parallel() type contextKey struct{} var ( dialValue any dialDeadline time.Time dialHasDeadline bool ) connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { dialValue = ctx.Value(contextKey{}) dialDeadline, dialHasDeadline = ctx.Deadline() return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) {}, Reset: func(connection *testConnectorConnection) {}, }) deadline := time.Now().Add(time.Minute) requestContext, cancel := context.WithDeadline(context.WithValue(context.Background(), contextKey{}, "test-value"), deadline) defer cancel() _, err := connector.Get(requestContext) require.NoError(t, err) require.Equal(t, "test-value", dialValue) require.True(t, dialHasDeadline) require.WithinDuration(t, deadline, dialDeadline, time.Second) } func TestConnectorDialSkipsCanceledRequest(t *testing.T) { t.Parallel() var dialCount atomic.Int32 connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { dialCount.Add(1) return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) {}, Reset: func(connection *testConnectorConnection) {}, }) requestContext, cancel := context.WithCancel(context.Background()) cancel() _, err := connector.Get(requestContext) require.ErrorIs(t, err, context.Canceled) require.EqualValues(t, 0, dialCount.Load()) } func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) { t.Parallel() var ( dialCount atomic.Int32 closeCount atomic.Int32 ) dialStarted := make(chan struct{}, 1) releaseDial := make(chan struct{}) connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { dialCount.Add(1) select { case dialStarted <- struct{}{}: default: } <-releaseDial return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) { closeCount.Add(1) }, Reset: func(connection *testConnectorConnection) {}, }) requestContext, cancel := context.WithCancel(context.Background()) result := make(chan error, 1) go func() { _, err := connector.Get(requestContext) result <- err }() <-dialStarted cancel() close(releaseDial) err := <-result require.ErrorIs(t, err, context.Canceled) require.EqualValues(t, 1, dialCount.Load()) require.Eventually(t, func() bool { return closeCount.Load() == 1 }, time.Second, 10*time.Millisecond) _, err = connector.Get(context.Background()) require.NoError(t, err) require.EqualValues(t, 2, dialCount.Load()) } func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(t *testing.T) { t.Parallel() var ( dialCount atomic.Int32 closeCount atomic.Int32 ) dialStarted := make(chan struct{}, 1) releaseDial := make(chan struct{}) connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { dialCount.Add(1) select { case dialStarted <- struct{}{}: default: } <-releaseDial return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) { closeCount.Add(1) }, Reset: func(connection *testConnectorConnection) {}, }) requestContext, cancel := context.WithCancel(context.Background()) result := make(chan error, 1) go func() { _, err := connector.Get(requestContext) result <- err }() <-dialStarted cancel() select { case err := <-result: require.ErrorIs(t, err, context.Canceled) case <-time.After(time.Second): t.Fatal("Get did not return after request cancel") } require.EqualValues(t, 1, dialCount.Load()) require.EqualValues(t, 0, closeCount.Load()) close(releaseDial) require.Eventually(t, func() bool { return closeCount.Load() == 1 }, time.Second, 10*time.Millisecond) _, err := connector.Get(context.Background()) require.NoError(t, err) require.EqualValues(t, 2, dialCount.Load()) } func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) { t.Parallel() var ( dialCount atomic.Int32 closeCount atomic.Int32 ) firstDialStarted := make(chan struct{}, 1) secondDialStarted := make(chan struct{}, 1) releaseFirstDial := make(chan struct{}) connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { attempt := dialCount.Add(1) switch attempt { case 1: select { case firstDialStarted <- struct{}{}: default: } <-releaseFirstDial case 2: select { case secondDialStarted <- struct{}{}: default: } } return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) { closeCount.Add(1) }, Reset: func(connection *testConnectorConnection) {}, }) requestContext, cancel := context.WithCancel(context.Background()) firstResult := make(chan error, 1) go func() { _, err := connector.Get(requestContext) firstResult <- err }() <-firstDialStarted cancel() secondResult := make(chan error, 1) go func() { _, err := connector.Get(context.Background()) secondResult <- err }() select { case <-secondDialStarted: t.Fatal("second dial started before first dial completed") case <-time.After(100 * time.Millisecond): } select { case err := <-firstResult: require.ErrorIs(t, err, context.Canceled) case <-time.After(time.Second): t.Fatal("first Get did not return after request cancel") } close(releaseFirstDial) require.Eventually(t, func() bool { return closeCount.Load() == 1 }, time.Second, 10*time.Millisecond) select { case <-secondDialStarted: case <-time.After(time.Second): t.Fatal("second dial did not start after first dial completed") } err := <-secondResult require.NoError(t, err) require.EqualValues(t, 2, dialCount.Load()) } func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) { t.Parallel() var dialContext context.Context connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { dialContext = ctx return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) {}, Reset: func(connection *testConnectorConnection) {}, }) requestContext, cancel := context.WithCancel(context.Background()) _, err := connector.Get(requestContext) require.NoError(t, err) require.NotNil(t, dialContext) cancel() select { case <-dialContext.Done(): t.Fatal("dial context canceled by request context after successful dial") case <-time.After(100 * time.Millisecond): } err = connector.Close() require.NoError(t, err) } func TestConnectorDialContextCanceledOnClose(t *testing.T) { t.Parallel() var dialContext context.Context connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { dialContext = ctx return &testConnectorConnection{}, nil }, ConnectorCallbacks[*testConnectorConnection]{ IsClosed: func(connection *testConnectorConnection) bool { return false }, Close: func(connection *testConnectorConnection) {}, Reset: func(connection *testConnectorConnection) {}, }) _, err := connector.Get(context.Background()) require.NoError(t, err) require.NotNil(t, dialContext) select { case <-dialContext.Done(): t.Fatal("dial context canceled before connector close") default: } err = connector.Close() require.NoError(t, err) select { case <-dialContext.Done(): case <-time.After(time.Second): t.Fatal("dial context not canceled after connector close") } } ================================================ 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 i := 0; i < 5; i++ { 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" "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) if err == nil { if response.Rcode != mDNS.RcodeSuccess { err = dns.RcodeError(response.Rcode) } else if len(dns.MessageToAddresses(response)) == 0 { err = dns.RcodeSuccess } } 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 := 0; j < sLen; j++ { 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 } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) { store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{})) return &Transport{ TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil), logger: logger, store: store, }, 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") } 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) 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 { files = append(files, NewFile(DefaultPath)) } 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) 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" "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 (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_test import ( "net/netip" "testing" "github.com/sagernet/sing-box/dns/transport/hosts" "github.com/stretchr/testify/require" ) func TestHosts(t *testing.T) { t.Parallel() require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost")) require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost")) } ================================================ FILE: dns/transport/hosts/hosts_unix.go ================================================ //go:build !windows package hosts var DefaultPath = "/etc/hosts" ================================================ FILE: dns/transport/hosts/hosts_windows.go ================================================ package hosts import ( "path/filepath" "golang.org/x/sys/windows" ) var DefaultPath string func init() { systemDirectory, err := windows.GetSystemDirectory() if err != nil { systemDirectory = "C:\\Windows\\System32" } DefaultPath = filepath.Join(systemDirectory, "Drivers/etc/hosts") } ================================================ 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.transportAccess.Lock() defer t.transportAccess.Unlock() t.transport.CloseIdleConnections() t.transport = t.transport.Clone() 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 ================================================ //go:build !darwin package local import ( "context" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport/hosts" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" ) func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport) } var _ adapter.DNSTransport = (*Transport)(nil) type Transport struct { dns.TransportAdapter ctx context.Context logger logger.ContextLogger hosts *hosts.File dialer N.Dialer preferGo bool resolved ResolvedResolver } 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 } return &Transport{ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), ctx: ctx, logger: logger, hosts: hosts.NewFile(hosts.DefaultPath), dialer: transportDialer, preferGo: options.PreferGo, }, nil } func (t *Transport) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateInitialize: if !t.preferGo { if 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")) } } } } } return nil } func (t *Transport) Close() error { if t.resolved != nil { return t.resolved.Close() } return nil } func (t *Transport) Reset() { } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if t.resolved != nil { return t.resolved.Exchange(ctx, message) } question := message.Question[0] if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) if len(addresses) > 0 { return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil } } return t.exchange(ctx, message, question.Name) } ================================================ FILE: dns/transport/local/local_darwin.go ================================================ //go:build darwin package local import ( "context" "errors" "net" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport/hosts" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" 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) type Transport struct { dns.TransportAdapter ctx context.Context logger logger.ContextLogger hosts *hosts.File dialer N.Dialer preferGo bool fallback bool dhcpTransport dhcpTransport resolver net.Resolver } 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 } transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options) return &Transport{ TransportAdapter: transportAdapter, ctx: ctx, logger: logger, hosts: hosts.NewFile(hosts.DefaultPath), dialer: transportDialer, preferGo: options.PreferGo, }, nil } func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } inboundManager := service.FromContext[adapter.InboundManager](t.ctx) for _, inbound := range inboundManager.Inbounds() { if inbound.Type() == C.TypeTun { t.fallback = true break } } if t.fallback { t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) if t.dhcpTransport != nil { err := t.dhcpTransport.Start(stage) if err != nil { return err } } } return nil } func (t *Transport) Close() error { return common.Close( t.dhcpTransport, ) } func (t *Transport) Reset() { if t.dhcpTransport != nil { t.dhcpTransport.Reset() } } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { question := message.Question[0] if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) if len(addresses) > 0 { return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil } } if !t.fallback { return t.exchange(ctx, message, question.Name) } if t.dhcpTransport != nil { dhcpTransports := t.dhcpTransport.Fetch() if len(dhcpTransports) > 0 { return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports) } } if t.preferGo { // Assuming the user knows what they are doing, we still execute the query which will fail. return t.exchange(ctx, message, question.Name) } if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { var network string if question.Qtype == mDNS.TypeA { network = "ip4" } else { network = "ip6" } addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name) if err != nil { var dnsError *net.DNSError if errors.As(err, &dnsError) && dnsError.IsNotFound { return nil, dns.RcodeRefused } return nil, err } return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil } return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.") } ================================================ FILE: dns/transport/local/local_darwin_dhcp.go ================================================ //go:build darwin && 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_darwin_nodhcp.go ================================================ //go:build darwin && !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_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 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" "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) if err == nil { if response.Rcode != mDNS.RcodeSuccess { err = dns.RcodeError(response.Rcode) } else if len(dns.MessageToAddresses(response)) == 0 { err = E.New(fqdn, ": empty result") } } 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 := uint32(0); j < sLen; j++ { 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 ================================================ 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 ================================================ 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/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 { *transport.BaseTransport ctx context.Context dialer N.Dialer serverAddr M.Socksaddr tlsConfig tls.Config connector *transport.Connector[*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) } t := &Transport{ BaseTransport: transport.NewBaseTransport( dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), logger, ), ctx: ctx, dialer: transportDialer, serverAddr: serverAddr, tlsConfig: tlsConfig, } t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{ IsClosed: func(connection *quic.Conn) bool { return common.Done(connection.Context()) }, Close: func(connection *quic.Conn) { connection.CloseWithError(0, "") }, Reset: func(connection *quic.Conn) { connection.CloseWithError(0, "") }, }) return t, nil } func (t *Transport) dial(ctx context.Context) (*quic.Conn, error) { conn, 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(conn), t.serverAddr.UDPAddr(), t.tlsConfig, nil, ) if err != nil { conn.Close() return nil, E.Cause(err, "establish QUIC connection") } return earlyConnection, nil } func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } err := t.SetStarted() if err != nil { return err } return dialer.InitializeDetour(t.dialer) } func (t *Transport) Close() error { return E.Errors(t.BaseTransport.Close(), t.connector.Close()) } func (t *Transport) Reset() { t.connector.Reset() } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if !t.BeginQuery() { return nil, transport.ErrTransportClosed } defer t.EndQuery() var ( conn *quic.Conn err error response *mDNS.Msg ) for i := 0; i < 2; i++ { conn, err = t.connector.Get(ctx) if err != nil { return nil, err } response, err = t.exchange(ctx, message, conn) if err == nil { return response, nil } else if !isQUICRetryError(err) { return nil, err } else { t.connector.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" "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" 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() 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 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" "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" 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" mDNS "github.com/miekg/dns" ) var _ adapter.DNSTransport = (*TLSTransport)(nil) func RegisterTLS(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS) } type TLSTransport struct { *BaseTransport dialer tls.Dialer serverAddr M.Socksaddr tlsConfig tls.Config access sync.Mutex connections list.List[*tlsDNSConn] } type tlsDNSConn struct { tls.Conn queryId uint16 } 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{ BaseTransport: NewBaseTransport(adapter, logger), dialer: tls.NewDialer(dialer, tlsConfig), serverAddr: serverAddr, tlsConfig: tlsConfig, } } func (t *TLSTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } err := t.SetStarted() if err != nil { return err } return dialer.InitializeDetour(t.dialer) } func (t *TLSTransport) Close() error { t.access.Lock() for connection := t.connections.Front(); connection != nil; connection = connection.Next() { connection.Value.Close() } t.connections.Init() t.access.Unlock() return t.BaseTransport.Close() } func (t *TLSTransport) Reset() { t.access.Lock() defer t.access.Unlock() for connection := t.connections.Front(); connection != nil; connection = connection.Next() { connection.Value.Close() } t.connections.Init() } func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if !t.BeginQuery() { return nil, ErrTransportClosed } defer t.EndQuery() t.access.Lock() conn := t.connections.PopFront() t.access.Unlock() if conn != nil { response, err := t.exchange(ctx, message, conn) if err == nil { return response, nil } t.Logger.DebugContext(ctx, "discarded pooled connection: ", err) } tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) if err != nil { return nil, E.Cause(err, "dial TLS connection") } return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn}) } func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { if deadline, ok := ctx.Deadline(); ok { conn.SetDeadline(deadline) } conn.queryId++ err := WriteMessage(conn, conn.queryId, message) if err != nil { conn.Close() return nil, E.Cause(err, "write request") } response, err := ReadMessage(conn) if err != nil { conn.Close() return nil, E.Cause(err, "read response") } t.access.Lock() if t.State() >= StateClosing { t.access.Unlock() conn.Close() return response, nil } conn.SetDeadline(time.Time{}) t.connections.PushBack(conn) t.access.Unlock() return response, nil } ================================================ FILE: dns/transport/udp.go ================================================ package transport import ( "context" "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" 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 { *BaseTransport dialer N.Dialer serverAddr M.Socksaddr udpSize atomic.Int32 connector *Connector[*Connection] 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{ BaseTransport: NewBaseTransport(adapter, logger), dialer: dialerInstance, serverAddr: serverAddr, callbacks: make(map[uint16]*udpCallback), } t.udpSize.Store(2048) t.connector = NewSingleflightConnector(t.CloseContext(), t.dial) return t } func (t *UDPTransport) dial(ctx context.Context) (*Connection, error) { rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) if err != nil { return nil, E.Cause(err, "dial UDP connection") } conn := WrapConnection(rawConn) go t.recvLoop(conn) return conn, nil } func (t *UDPTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } err := t.SetStarted() if err != nil { return err } return dialer.InitializeDetour(t.dialer) } func (t *UDPTransport) Close() error { return E.Errors(t.BaseTransport.Close(), t.connector.Close()) } func (t *UDPTransport) Reset() { t.connector.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) { if !t.BeginQuery() { return nil, ErrTransportClosed } defer t.EndQuery() 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() 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.connector.Reset() break } } } conn, err := t.connector.Get(ctx) if err != nil { return nil, err } callback := &udpCallback{ done: make(chan struct{}), } t.callbackAccess.Lock() queryId, err := t.nextAvailableQueryId() if err != nil { t.callbackAccess.Unlock() 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 { conn.CloseWithError(err) return nil, E.Cause(err, "write request") } select { case <-callback.done: callback.response.Id = originalId return callback.response, nil case <-conn.Done(): return nil, conn.CloseError() case <-t.CloseContext().Done(): return nil, ErrTransportClosed case <-ctx.Done(): return nil, ctx.Err() } } func (t *UDPTransport) recvLoop(conn *Connection) { for { buffer := buf.NewSize(int(t.udpSize.Load())) _, err := buffer.ReadOnceFrom(conn) if err != nil { buffer.Release() conn.CloseWithError(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 ( "net/netip" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" ) var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil) type TransportAdapter struct { transportType string transportTag string dependencies []string strategy C.DomainStrategy clientSubnet netip.Prefix } func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter { 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, strategy: C.DomainStrategy(localOptions.LegacyStrategy), clientSubnet: localOptions.LegacyClientSubnet, } } 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) } if remoteOptions.LegacyAddressResolver != "" { dependencies = append(dependencies, remoteOptions.LegacyAddressResolver) } return TransportAdapter{ transportType: transportType, transportTag: transportTag, dependencies: dependencies, strategy: C.DomainStrategy(remoteOptions.LegacyStrategy), clientSubnet: remoteOptions.LegacyClientSubnet, } } func (a *TransportAdapter) Type() string { return a.transportType } func (a *TransportAdapter) Tag() string { return a.transportTag } func (a *TransportAdapter) Dependencies() []string { return a.dependencies } func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy { return a.strategy } func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix { return a.clientSubnet } ================================================ FILE: dns/transport_dialer.go ================================================ package dns import ( "context" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" ) func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) { if options.LegacyDefaultDialer { return dialer.NewDefaultOutbound(ctx), nil } else { return dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, DirectResolver: true, LegacyDNSDialer: options.Legacy, }) } } func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) { if options.LegacyDefaultDialer { transportDialer := dialer.NewDefaultOutbound(ctx) if options.LegacyAddressResolver != "" { transport := service.FromContext[adapter.DNSTransportManager](ctx) resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver) if !loaded { return nil, E.New("address resolver not found: ", options.LegacyAddressResolver) } transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay)) } else if options.ServerIsDomain() { return nil, E.New("missing address resolver for server: ", options.Server) } return transportDialer, nil } else { return dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, RemoteIsDomain: options.ServerIsDomain(), DirectResolver: true, LegacyDNSDialer: options.Legacy, }) } } type legacyTransportDialer struct { dialer N.Dialer dnsRouter adapter.DNSRouter transport adapter.DNSTransport strategy C.DomainStrategy fallbackDelay time.Duration } func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer { return &legacyTransportDialer{ dialer, dnsRouter, transport, strategy, fallbackDelay, } } func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if destination.IsIP() { return d.dialer.DialContext(ctx, network, destination) } addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{ Transport: d.transport, Strategy: d.strategy, }) if err != nil { return nil, err } return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay) } func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if destination.IsIP() { return d.dialer.ListenPacket(ctx, destination) } addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{ Transport: d.transport, Strategy: d.strategy, }) if err != nil { return nil, err } conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses) return conn, err } func (d *legacyTransportDialer) Upstream() any { return d.dialer } ================================================ 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.3 * Fixes and improvements #### 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-servers). 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-servers). 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 [Address Filter Fields](/configuration/dns/rule#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 [Address filter DNS rule items](/configuration/dns/rule/#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 [Address filter DNS rule items](/configuration/dns/rule/#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 [Address Filter Fields](/configuration/dns/rule#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: | / | | `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: | / | | `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/delete-clock --- !!! failure "Deprecated in sing-box 1.12.0" Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers). ### 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_address IPv6 address range for FakeIP. ================================================ FILE: docs/configuration/dns/fakeip.zh.md ================================================ --- icon: material/delete-clock --- !!! failure "已在 sing-box 1.12.0 废弃" 旧的 fake-ip 配置已废弃且将在 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.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, "reverse_mapping": false, "client_subnet": "", "fakeip": {} } } ``` ### Fields | Key | Format | |----------|---------------------------------| | `server` | List of [DNS Server](./server/) | | `rules` | List of [DNS Rule](./rule/) | | `fakeip` | [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. #### disable_expire Disable dns cache expire. #### independent_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. #### 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 overrides by `servers.[].client_subnet` or `rules.[].client_subnet`. ================================================ FILE: docs/configuration/dns/index.zh.md ================================================ --- icon: material/alert-decagram --- !!! 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, "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 缓存。 #### disable_expire 禁用 DNS 缓存过期。 #### independent_cache 使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 #### cache_capacity !!! question "自 sing-box 1.11.0 起" LRU 缓存容量。 小于 1024 的值将被忽略。 #### 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 [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) !!! 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, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "ip_is_private": false, "ip_accept_any": 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" ], "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" ], "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, "rule_set_ip_cidr_accept_empty": false, "invert": false, "outbound": [ "direct" ], "action": "route", "server": "local", // Deprecated "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, included rule-sets can be considered merged rather than as a single rule sub-item. #### inbound Tags of [Inbound](/configuration/inbound/). #### ip_version 4 (A DNS query) or 6 (AAAA DNS query). Not limited if empty. #### query_type 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. #### 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. #### 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. #### 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). ### Address Filter Fields 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. #### ip_is_private !!! question "Since sing-box 1.9.0" Match private IP with query response. #### rule_set_ip_cidr_accept_empty !!! question "Since sing-box 1.10.0" Make `ip_cidr` rules in rule-sets accept empty query response. #### ip_accept_any !!! question "Since sing-box 1.12.0" Match any IP with query response. ### 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) !!! 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, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "ip_is_private": false, "ip_accept_any": 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" ], "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" ], "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, "rule_set_ip_cidr_accept_empty": false, "invert": false, "outbound": [ "direct" ], "action": "route", "server": "local", // 已弃用 "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` 另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。 #### inbound [入站](/zh/configuration/inbound/) 标签. #### ip_version 4 (A DNS 查询) 或 6 (AAAA DNS 查询)。 默认不限制。 #### query_type 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 应用包名。 #### 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 租约获取的主机名。 #### 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。 #### 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). ### 地址筛选字段 仅对地址请求 (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。 #### ip_is_private !!! question "自 sing-box 1.9.0 起" 与查询响应匹配非公开 IP。 #### ip_accept_any !!! question "自 sing-box 1.12.0 起" 匹配任意 IP。 #### rule_set_ip_cidr_accept_empty !!! question "自 sing-box 1.10.0 起" 使规则集中的 `ip_cidr` 规则接受空查询响应。 ### 逻辑字段 #### type `logical` #### mode ==必填== `and` 或 `or` #### rules ==必填== 包括的规则。 ================================================ FILE: docs/configuration/dns/rule_action.md ================================================ --- icon: material/new-box --- !!! 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, "rewrite_ttl": null, "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" 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. #### rewrite_ttl Rewrite TTL in DNS responses. #### client_subnet Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. Will overrides `dns.client_subnet`. ### route-options ```json { "action": "route-options", "disable_cache": false, "rewrite_ttl": null, "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.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, "rewrite_ttl": null, "client_subnet": null } ``` `route` 继承了将 DNS 请求 路由到指定服务器的经典规则动作。 #### server ==必填== 目标 DNS 服务器的标签。 #### strategy !!! question "自 sing-box 1.12.0 起" 为此查询设置域名策略。 可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 #### disable_cache 在此查询中禁用缓存。 #### rewrite_ttl 重写 DNS 回应中的 TTL。 #### client_subnet 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 将覆盖 `dns.client_subnet`. ### route-options ```json { "action": "route-options", "disable_cache": false, "rewrite_ttl": null, "client_subnet": null } ``` `route-options` 为路由设置选项。 ### reject ```json { "action": "reject", "method": "", "no_drop": false } ``` `reject` 拒绝 DNS 请求。 #### method - `default`: 返回 REFUSED。 - `drop`: 丢弃请求。 默认使用 `defualt`。 #### 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" ```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" ```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.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) | [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/) | | `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.12.0 中的更改" :material-plus: [type](#type) # DNS Server ### 结构 ```json { "dns": { "servers": [ { "type": "", "tag": "" } ] } } ``` #### type DNS 服务器的类型。 | 类型 | 格式 | |-----------------|---------------------------| | empty (default) | [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/) | | `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) | | `resolved` | [Resolved](./resolved/) | #### tag DNS 服务器的标签。 ================================================ FILE: docs/configuration/dns/server/legacy.md ================================================ --- icon: material/delete-clock --- !!! failure "Deprecated in sing-box 1.12.0" Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers). !!! 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 overrides by `rules.[].client_subnet`. Will overrides `dns.client_subnet`. ================================================ FILE: docs/configuration/dns/server/legacy.zh.md ================================================ --- icon: material/delete-clock --- !!! failure "Deprecated in sing-box 1.12.0" 旧的 DNS 服务器配置已废弃且将在 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.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 // 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`. ### 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.13.0 中的更改" :material-plus: [prefer_go](#prefer_go) !!! question "自 sing-box 1.12.0 起" # Local ### 结构 ```json { "dns": { "servers": [ { "type": "local", "tag": "", "prefer_go": false, // 拨号字段 } ] } } ``` !!! 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` 禁用。 ### 拨号字段 参阅 [拨号字段](/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" ```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" ```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 --- !!! question "Since sing-box 1.12.0" # Tailscale ### Structure ```json { "dns": { "servers": [ { "type": "tailscale", "tag": "", "endpoint": "ts-ep", "accept_default_resolvers": 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. ### Examples === "MagicDNS only" ```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 --- !!! question "自 sing-box 1.12.0 起" # Tailscale ### 结构 ```json { "dns": { "servers": [ { "type": "tailscale", "tag": "", "endpoint": "ts-ep", "accept_default_resolvers": false } ] } } ``` ### 字段 #### endpoint ==必填== [Tailscale 端点](/zh/configuration/endpoint/tailscale) 的标签。 #### accept_default_resolvers 指示是否除了 MagicDNS 外,还应接受默认 DNS 解析器以进行回退查询。 如果未启用,对于非 Tailscale 域名查询将返回 `NXDOMAIN`。 ### 示例 === "仅 MagicDNS" ```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.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": "", "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. ### Dial Fields !!! note Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections. See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/endpoint/tailscale.zh.md ================================================ --- icon: material/new-box --- !!! 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": "", "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`。 ### 拨号字段 !!! note Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。 参阅 [拨号字段](/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.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": "" } ``` ### 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 Store rejected DNS response cache in the cache file The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) will be cached until expiration. #### rdrc_timeout Timeout of rejected DNS response cache. `7d` is used by default. ================================================ FILE: docs/configuration/experimental/cache-file.zh.md ================================================ !!! question "自 sing-box 1.8.0 起" !!! 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": "" } ``` ### 字段 #### enabled 启用缓存文件。 #### path 缓存文件路径,默认使用`cache.db`。 #### cache_id 缓存文件中的标识符。 如果不为空,配置特定的数据将使用由其键控的单独存储。 #### store_fakeip 将 fakeip 存储在缓存文件中。 #### store_rdrc 将拒绝的 DNS 响应缓存存储在缓存文件中。 [地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。 #### rdrc_timeout 拒绝的 DNS 响应缓存超时。 默认使用 `7d`。 ================================================ 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/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" } ], "recv_window_conn": 0, "recv_window_client": 0, "max_conn_client": 0, "disable_mtu_discovery": false, "tls": {} } ``` ### 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. #### recv_window_conn The QUIC stream-level flow control window for receiving data. `15728640 (15 MB/s)` will be used if empty. #### recv_window_client The QUIC connection-level flow control window for receiving data. `67108864 (64 MB/s)` will be used if empty. #### max_conn_client The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open. `1024` will be used if empty. #### disable_mtu_discovery Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size. Force enabled on for systems other than Linux and Windows (according to upstream). #### tls ==Required== TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ================================================ 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" } ], "recv_window_conn": 0, "recv_window_client": 0, "max_conn_client": 0, "disable_mtu_discovery": false, "tls": {} } ``` ### 监听字段 参阅 [监听字段](/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 认证密码。 #### recv_window_conn 用于接收数据的 QUIC 流级流控制窗口。 默认 `15728640 (15 MB/s)`。 #### recv_window_client 用于接收数据的 QUIC 连接级流控制窗口。 默认 `67108864 (64 MB/s)`。 #### max_conn_client 允许对等点打开的 QUIC 并发双向流的最大数量。 默认 `1024`。 #### disable_mtu_discovery 禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。 强制为 Linux 和 Windows 以外的系统启用(根据上游)。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ================================================ FILE: docs/configuration/inbound/hysteria2.md ================================================ --- icon: material/alert-decagram --- !!! 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": {}, "masquerade": "", // or {} "brutal_debug": false } ``` !!! 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). #### 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. #### brutal_debug Enable debug information logging for Hysteria Brutal CC. ================================================ FILE: docs/configuration/inbound/hysteria2.zh.md ================================================ --- icon: material/alert-decagram --- !!! 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": {}, "masquerade": "", // 或 {} "brutal_debug": false } ``` !!! 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/#入站)。 #### 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 固定响应内容。 #### brutal_debug 启用 Hysteria Brutal CC 的调试信息日志记录。 ================================================ 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: | #### 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: | #### 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": {} } ``` ### 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). ================================================ 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": {} } ``` ### 监听字段 参阅 [监听字段](/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/#入站)。 ================================================ 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) !!! 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, "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. #### 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) !!! 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, "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 最大传输单元。 #### 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": {}, "endpoints": [], "inbounds": [], "outbounds": [], "route": {}, "services": [], "experimental": {} } ``` ### Fields | Key | Format | |----------------|---------------------------------| | `log` | [Log](./log/) | | `dns` | [DNS](./dns/) | | `ntp` | [NTP](./ntp/) | | `certificate` | [Certificate](./certificate/) | | `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": {}, "endpoints": [], "inbounds": [], "outbounds": [], "route": {}, "services": [], "experimental": {} } ``` ### 字段 | Key | Format | |----------------|------------------------| | `log` | [日志](./log/) | | `dns` | [DNS](./dns/) | | `ntp` | [NTP](./ntp/) | | `certificate` | [证书](./certificate/) | | `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", "recv_window_conn": 0, "recv_window": 0, "disable_mtu_discovery": false, "network": "tcp", "tls": {}, ... // Dial Fields } ``` ### 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. #### recv_window_conn The QUIC stream-level flow control window for receiving data. `15728640 (15 MB/s)` will be used if empty. #### recv_window The QUIC connection-level flow control window for receiving data. `67108864 (64 MB/s)` will be used if empty. #### disable_mtu_discovery Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size. Force enabled on for systems other than Linux and Windows (according to upstream). #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### tls ==Required== TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ 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", "recv_window_conn": 0, "recv_window": 0, "disable_mtu_discovery": false, "network": "tcp", "tls": {}, ... // 拨号字段 } ``` ### 字段 #### 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 认证密码。 #### recv_window_conn 用于接收数据的 QUIC 流级流控制窗口。 默认 `15728640 (15 MB/s)`。 #### recv_window 用于接收数据的 QUIC 连接级流控制窗口。 默认 `67108864 (64 MB/s)`。 #### disable_mtu_discovery 禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。 强制为 Linux 和 Windows 以外的系统启用(根据上游)。 #### network 启用的网络协议。 `tcp` 或 `udp`。 默认所有。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/hysteria2.md ================================================ !!! 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": "", "up_mbps": 100, "down_mbps": 100, "obfs": { "type": "salamander", "password": "cry_me_a_r1ver" }, "password": "goofy_ahh_password", "network": "tcp", "tls": {}, "brutal_debug": false, ... // 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. #### server_port ==Required== The server port. Ignored if `server_ports` is set. #### server_ports !!! question "Since sing-box 1.11.0" Server port range list. Conflicts with `server_port`. #### hop_interval !!! question "Since sing-box 1.11.0" Port hopping interval. `30s` is used by default. #### 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). #### brutal_debug Enable debug information logging for Hysteria Brutal CC. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/hysteria2.zh.md ================================================ !!! 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": "", "up_mbps": 100, "down_mbps": 100, "obfs": { "type": "salamander", "password": "cry_me_a_r1ver" }, "password": "goofy_ahh_password", "network": "tcp", "tls": {}, "brutal_debug": false, ... // 拨号字段 } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, 本质上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 如果设置了 `server_ports`,则忽略此项。 #### server_ports !!! question "自 sing-box 1.11.0 起" 服务器端口范围列表。 与 `server_port` 冲突。 #### hop_interval !!! question "自 sing-box 1.11.0 起" 端口跳跃间隔。 默认使用 `30s`。 #### 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/#出站)。 #### brutal_debug 启用 Hysteria Brutal CC 的调试信息日志记录。 ### 拨号字段 参阅 [拨号字段](/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 ================================================ ### 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", ... // 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. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/ssh.zh.md ================================================ ### 结构 ```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", ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port 服务器端口,默认使用 22。 #### user SSH 用户, 默认使用 root。 #### password 密码。 #### private_key 密钥。 #### private_key_path 密钥路径。 #### private_key_passphrase 密钥密码。 #### host_key 主机密钥,留空接受所有。 #### host_key_algorithms 主机密钥算法。 #### client_version 客户端版本,默认使用随机值。 ### 拨号字段 参阅 [拨号字段](/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": {}, ... // 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). ### 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": {}, ... // 拨号字段 } ``` ### 字段 #### 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/#出站)。 ### 拨号字段 参阅 [拨号字段](/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: [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_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_domain_resolver !!! question "Since sing-box 1.12.0" See [Dial Fields](/configuration/shared/dial/#domain_resolver) for details. Can be overrides 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 overrides 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: [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_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_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) !!! 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" ], "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, included rule-sets can be considered merged rather than as a single rule sub-item. #### 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. #### 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) !!! 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" ], "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` 另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。 #### 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 应用包名。 #### 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.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": "" } ``` `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. ### 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, "rewrite_ttl": null, "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. #### rewrite_ttl !!! question "Since sing-box 1.12.0" Rewrite TTL in DNS responses. #### 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 overrides `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.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": "" } ``` !!! 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 记录来绕过防火墙检测。 ### sniff ```json { "action": "sniff", "sniffer": [], "timeout": "" } ``` `sniff` 对连接执行协议嗅探。 对于已弃用的 `inbound.sniff` 选项,被视为在路由之前执行的 `sniff`。 #### sniffer 启用的探测器。 默认启用所有探测器。 可用的协议值可以在 [协议嗅探](../sniff/) 中找到。 #### timeout 探测超时时间。 默认使用 300ms。 ### resolve ```json { "action": "resolve", "server": "", "strategy": "", "disable_cache": false, "rewrite_ttl": null, "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 起" 在此查询中禁用缓存。 #### rewrite_ttl !!! question "自 sing-box 1.12.0 起" 重写 DNS 回应中的 TTL。 #### 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.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" ], "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 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. #### 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.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" ], "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 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 应用包名。 #### 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.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": "", "download_detour": "", // optional "update_interval": "" // optional } ``` ### 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. #### download_detour Tag of the outbound to download rule-set. Default outbound will be used if empty. #### update_interval Update interval of rule-set. `1d` will be used if empty. ================================================ FILE: docs/configuration/rule-set/index.zh.md ================================================ !!! 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": "", "download_detour": "", // 可选 "update_interval": "" // 可选 } ``` ### 字段 #### 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。 #### download_detour 用于下载规则集的出站的标签。 如果为空,将使用默认出站。 #### update_interval 规则集的更新间隔。 默认使用 `1d`。 ================================================ FILE: docs/configuration/rule-set/source-format.md ================================================ --- icon: material/new-box --- !!! 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. #### 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.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` 规则项。 #### 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": "https://my-headscale.com/verify", ... // Dial 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": "https://my-headscale.com/verify", ... // 拨号字段 } ``` 将数组值设置为字符串 `__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/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) | | `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) | | `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/dial.md ================================================ --- icon: material/new-box --- !!! 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.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.13.0" :material-plus: [alidns.security_token](#security_token) :material-plus: [cloudflare.zone_token](#zone_token) :material-plus: [acmedns](#acmedns) ### Structure ```json { "provider": "", ... // Provider 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.13.0 中的更改" :material-plus: [alidns.security_token](#security_token) :material-plus: [cloudflare.zone_token](#zone_token) :material-plus: [acmedns](#acmedns) ### 结构 ```json { "provider": "", ... // 提供商字段 } ``` ### 提供商字段 #### 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/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. 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 地址和主机名。 当这些规则项存在时,邻居解析自动启用。 使用 [`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/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.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, "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, "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, "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. #### 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. ## 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" 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. 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` has nothing to do with ECH, was added by mistake, has been deprecated and no longer works. 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. ### ACME Fields #### 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.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, "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, "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, "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 #### 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 接收支持。 ## 自定义 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 废弃" ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案,因此 `pq_signature_schemes_enabled` 已被弃用且不再工作。 启用对后量子对等证书签名方案的支持。 #### dynamic_record_sizing_disabled !!! failure "已在 sing-box 1.12.0 废弃" `dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。 禁用 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 记录以绕过防火墙。 ### 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.12.0 #### Legacy DNS server formats DNS servers are refactored, check [Migration](../migration/#migrate-to-new-dns-servers). Compatibility for old formats will be 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 will be 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 will be 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 will be 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). #### WireGuard outbound WireGuard outbound is deprecated and can be replaced by endpoint, check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint). Old outbound will be removed in sing-box 1.13.0. #### GSO option in TUN GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN. Old fields will be removed in sing-box 1.13.0. ## 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 will be removed in sing-box 1.12.0. #### Match source rule items are renamed `rule_set_ipcidr_match_source` route and DNS rule items are renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. #### 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 will be 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 will be 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 --- # 废弃功能列表 #### 旧的 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.11.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/). | | `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/)。 | | `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.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.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) ## Commercial Sponsors > [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents. [![](https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png)](https://go.warp.dev/sing-box) ## 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/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "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), } cacheIDDefault = []byte("default") ) var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { ctx context.Context path string cacheID []byte storeFakeIP bool storeRDRC bool rdrcTimeout time.Duration DB *bbolt.DB resetAccess sync.Mutex saveMetadataTimer *time.Timer saveFakeIPAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr saveAddress6 map[string]netip.Addr saveRDRCAccess sync.RWMutex saveRDRC map[saveRDRCCacheKey]bool } type saveRDRCCacheKey struct { TransportName string QuestionName string QType uint16 } func New(ctx context.Context, 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)...) } 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, path: filemanager.BasePath(ctx, path), cacheID: cacheIDBytes, storeFakeIP: options.StoreFakeIP, storeRDRC: options.StoreRDRC, rdrcTimeout: rdrcTimeout, saveDomain: make(map[netip.Addr]string), saveAddress4: make(map[string]netip.Addr), saveAddress6: make(map[string]netip.Addr), saveRDRC: make(map[saveRDRCCacheKey]bool), } } func (c *CacheFile) Name() string { return "cache-file" } func (c *CacheFile) Dependencies() []string { return nil } func (c *CacheFile) Start(stage adapter.StartStage) error { if stage != adapter.StartStateInitialize { return nil } const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( db *bbolt.DB err error ) for i := 0; i < 10; i++ { 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/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[saveRDRCCacheKey{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 { 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) expiresAt := buf.Get(8) defer buf.Put(expiresAt) binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix())) return bucket.Put(key, expiresAt) }) } func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) { saveKey := saveRDRCCacheKey{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, router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler { r := chi.NewRouter() r.Get("/", getConnections(ctx, trafficManager)) r.Delete("/", closeAllConnections(router, 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(router adapter.Router, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { snapshot := trafficManager.Snapshot() for _, c := range snapshot.Connections { c.Close() } router.ResetNetwork() 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" 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 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 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, 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, } 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.router, 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 i := 0; i < 3; i++ { 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, ) } 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) ResetStatistic() { m.uploadTotal.Store(0) m.downloadTotal.Store(0) } 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 t.Metadata.ProcessInfo.AndroidPackageName != "" { processPath = t.Metadata.ProcessInfo.AndroidPackageName } 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 OptionLegacyDNSTransport = Note{ Name: "legacy-dns-transport", Description: "legacy DNS servers", DeprecatedVersion: "1.12.0", ScheduledVersion: "1.14.0", EnvName: "LEGACY_DNS_SERVERS", MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats", } var OptionLegacyDNSFakeIPOptions = Note{ Name: "legacy-dns-fakeip-options", Description: "legacy DNS fakeip options", DeprecatedVersion: "1.12.0", ScheduledVersion: "1.14.0", EnvName: "LEGACY_DNS_FAKEIP_OPTIONS", MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats", } var OptionOutboundDNSRuleItem = Note{ Name: "outbound-dns-rule-item", Description: "outbound DNS rule item", 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 Options = []Note{ OptionLegacyDNSTransport, OptionLegacyDNSFakeIPOptions, OptionOutboundDNSRuleItem, OptionMissingDomainResolver, OptionLegacyDomainStrategyOptions, } ================================================ 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" "github.com/sagernet/sing/common/logger" ) type stderrManager struct { 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) { 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 ) ================================================ 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/credentials/insecure" "google.golang.org/grpc/metadata" "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) 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, err } 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 := 0; attempt < commandClientDialAttempts; attempt++ { 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, err } 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, lastError } 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() 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, err } 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(err.Error()) return } defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{}) if err != nil { c.handler.Disconnected(err.Error()) return } c.handler.SetDefaultLogLevel(int32(defaultLogLevel.Level)) for { logMessage, err := stream.Recv() if err != nil { c.handler.Disconnected(err.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(err.Error()) return } for { status, err := stream.Recv() if err != nil { c.handler.Disconnected(err.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(err.Error()) return } for { groups, err := stream.Recv() if err != nil { c.handler.Disconnected(err.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(err.Error()) return } if sFixAndroidStack { go func() { c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode) if len(modeStatus.ModeList) == 0 { c.handler.Disconnected(os.ErrInvalid.Error()) } }() } else { c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode) if len(modeStatus.ModeList) == 0 { c.handler.Disconnected(os.ErrInvalid.Error()) return } } if len(modeStatus.ModeList) == 0 { return } stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{}) if err != nil { c.handler.Disconnected(err.Error()) return } for { mode, err := stream.Recv() if err != nil { c.handler.Disconnected(err.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(err.Error()) return } for { events, err := stream.Recv() if err != nil { c.handler.Disconnected(err.Error()) return } libboxEvents := connectionEventsFromGRPC(events) c.handler.WriteConnectionEvents(libboxEvents) } } 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, }) }) return err } 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, }) }) return err } 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, }) }) return err } 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, }) }) return err } func (c *CommandClient) CloseConnections() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.CloseAllConnections(context.Background(), &emptypb.Empty{}) }) return err } func (c *CommandClient) ServiceReload() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.ReloadService(context.Background(), &emptypb.Empty{}) }) return err } func (c *CommandClient) ServiceClose() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.StopService(context.Background(), &emptypb.Empty{}) }) return err } func (c *CommandClient) ClearLogs() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.ClearLogs(context.Background(), &emptypb.Empty{}) }) return err } func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) { return callWithResult(c, func(client daemon.StartedServiceClient) (*SystemProxyStatus, error) { status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{}) if err != nil { return nil, err } return 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, }) }) return err } func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) { return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) { warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{}) if err != nil { return nil, err } var notes []*DeprecatedNote for _, warning := range warnings.Warnings { notes = append(notes, &DeprecatedNote{ Description: warning.Message, 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, err } 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, }) }) return err } ================================================ 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 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, OOMKiller: memoryLimitEnabled, // 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 i := 0; i < 30; i++ { 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 { return s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{ AutoRedirect: options.AutoRedirect, IncludePackage: iteratorToArray(options.IncludePackage), ExcludePackage: iteratorToArray(options.ExcludePackage), }) } 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().Router().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, err } 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) 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 PackageName string } 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 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, PackageName: conn.ProcessInfo.PackageName, } } 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, } } func systemProxyStatusToGRPC(status *SystemProxyStatus) *daemon.SystemProxyStatus { if status == nil { return nil } return &daemon.SystemProxyStatus{ Available: status.Available, Enabled: status.Enabled, } } ================================================ FILE: experimental/libbox/config.go ================================================ package libbox import ( "bytes" "context" "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" 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" ) 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) return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry()) } 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) 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/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 } 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)) } ================================================ 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 package libbox import ( "os" "runtime" "runtime/debug" ) var crashOutputFile *os.File func RedirectStderr(path string) error { if stats, err := os.Stat(path); err == nil && stats.Size() > 0 { _ = os.Rename(path, path+".old") } outputFile, err := os.Create(path) if err != nil { return err } if runtime.GOOS != "android" { 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 } crashOutputFile = outputFile return nil } ================================================ FILE: experimental/libbox/memory.go ================================================ package libbox import ( "math" runtimeDebug "runtime/debug" C "github.com/sagernet/sing-box/constant" ) var memoryLimitEnabled bool func SetMemoryLimit(enabled bool) { memoryLimitEnabled = enabled const memoryLimitGo = 45 * 1024 * 1024 if enabled { runtimeDebug.SetGCPercent(10) if C.IsIos { runtimeDebug.SetMemoryLimit(memoryLimitGo) } } else { runtimeDebug.SetGCPercent(100) if C.IsIos { runtimeDebug.SetMemoryLimit(math.MaxInt64) } } } ================================================ 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 element *list.Element[tun.NetworkUpdateCallback] 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 import ( "net" "net/netip" ) 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) } 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/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/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" "github.com/sagernet/sing-box/option" ) 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 AndroidPackageName string } 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 } type onDemandRule struct { option.OnDemandRule } func (r *onDemandRule) Target() int32 { if r.OnDemandRule.Action == nil { return -1 } return int32(*r.OnDemandRule.Action) } func (r *onDemandRule) DNSSearchDomainMatch() StringIterator { return newIterator(r.OnDemandRule.DNSSearchDomainMatch) } func (r *onDemandRule) DNSServerAddressMatch() StringIterator { return newIterator(r.OnDemandRule.DNSServerAddressMatch) } func (r *onDemandRule) InterfaceTypeMatch() int32 { if r.OnDemandRule.InterfaceTypeMatch == nil { return -1 } return int32(*r.OnDemandRule.InterfaceTypeMatch) } func (r *onDemandRule) SSIDMatch() StringIterator { return newIterator(r.OnDemandRule.SSIDMatch) } func (r *onDemandRule) ProbeURL() string { return r.OnDemandRule.ProbeURL } ================================================ 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/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 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.iif.RegisterMyInterface(options.Name) return tun.New(*options) } 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) { if netInterface.Name == w.myTunName { continue } w.defaultInterfaceAccess.Lock() // (GOOS=windows) SA4006: this value of `isDefault` is never used // Why not used? //nolint:staticcheck isDefault := 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, AndroidPackageName: result.AndroidPackageName, }, 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 ( "os" "runtime/debug" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/locale" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/byteformats" ) var ( sBasePath string sWorkingPath string sTempPath string sUserID int sGroupID int sFixAndroidStack bool sCommandServerListenPort uint16 sCommandServerSecret string sLogMaxLines int sDebug bool ) 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 } func Setup(options *SetupOptions) error { 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 os.MkdirAll(sWorkingPath, 0o777) os.MkdirAll(sTempPath, 0o777) return nil } func SetLocale(localeId string) { locale.Set(localeId) } func Version() string { return C.Version } 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 ProxyDisplayType(proxyType string) string { return C.ProxyDisplayName(proxyType) } ================================================ 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" E "github.com/sagernet/sing/common/exceptions" ) type TunOptions interface { GetInet4Address() RoutePrefixIterator GetInet6Address() RoutePrefixIterator GetDNSServerAddress() (*StringBox, 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) GetDNSServerAddress() (*StringBox, error) { if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 { return nil, E.New("need one more IPv4 address for DNS hijacking") } return wrapString(o.Inet4Address[0].Addr().Next().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 := 0; fd < 1024; fd++ { 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.2 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/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-20260309100020-c128886ff3fc github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc github.com/sagernet/fswatch v0.1.1 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.3-0.20260315153529-ed51f65fbfde github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.0 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.4-0.20260315091454-bbe21100c226 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.6.0.20260311131347-f88b27eeb76e 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/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/caddyserver/zerossl v0.1.5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/database64128/netx-go v0.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect github.com/ebitengine/purego v0.9.1 // indirect github.com/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-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/libdns/libdns v1.1.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect 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-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // 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/sync v0.19.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.11.0 // indirect 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 ) ================================================ 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.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= github.com/caddyserver/certmagic v0.25.2/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/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.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/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.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.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/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/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-20260309100020-c128886ff3fc h1:YK7PwJT0irRAEui9ASdXSxcE2BOVQipWMF/A1Ogt+7c= github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc h1:EJPHOqk23IuBsTjXK9OXqkNxPbKOBWKRmviQoCcriAs= github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc/go.mod h1:8aty0RW96DrJSMWXO6bRPMBJEjuqq5JWiOIi4bCRzFA= github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA= github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8= github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 h1:Y7lWrZwEhC/HX8Pb5C92CrQihuaE7hrHmWB2ykst3iQ= github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:3Ggy5wiyjA6t+aVVPnXlSEIVj9zkxd4ybH3NsvsNefs= github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:DuFTCnZloblY+7olXiZoRdueWfxi34EV5UheTFKM2rA= github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:x/6T2gjpLw9yNdCVR6xBlzMUzED9fxNFNt6U6A6SOh8= github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Lx9PExM70rg8aNxPm0JPeSr5SWC3yFiCz4wIq86ugx8= github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:BTEpw7/vKR9BNBsHebfpiGHDCPpjVJ3vLIbHNU3VUfM= github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:hdEph9nQXRnKwc/lIDwo15rmzbC6znXF5jJWHPN1Fiw= github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Iq++oYV7dtRJHTpu8yclHJdn+1oj2t1e84/YpdXYWW8= github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 h1:Y43fuLL8cgwRHpEKwxh0O3vYp7g/SZGvbkJj3cQ6USA= github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:bX2GJmF0VCC+tBrVAa49YEsmJ4A9dLmwoA6DJUxRtCY= github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:gQTR/2azUCInE0r3kmesZT9xu+x801+BmtDY0d0Tw9Y= github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 h1:X4mP3jlYvxgrKpZLOKMmc/O8T5/zP83/23pgfQOc3tY= github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:c6xj2nXr/65EDiRFddUKQIBQ/b/lAPoH8WFYlgadaPc= github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:ahbl7yjOvGVVNUwk9TcQk+xejVfoYAYFRlhWnby0/YM= github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 h1:JC5Zv5+J85da6g5G56VhdaK53fmo6Os2q/wWi5QlxOw= github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 h1:4bt7Go588BoM4VjNYMxx0MrvbwlFQn3DdRDCM7BmkRo= github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:E1z0BeLUh8EZfCjIyS9BrfCocZrt+0KPS0bzop3Sxf4= github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 h1:d8ejxRHO7Vi9JqR/6DxR7RyI/swA2JfDWATR4T7otBw= github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 h1:iUDVEVu3RxL5ArPIY72BesbuX5zQ1la/ZFwKpQcGc5c= github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 h1:xB6ikOC/R3n3hjy68EJ0sbZhH4vwEhd6JM9jZ1U2SVY= github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 h1:mBOuLCPOOMMq8N1+dUM5FqZclqga1+u6fAbPqQcbIhc= github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:cwPyDfj+ZNFE7kvcWbayQJyeC/KQA16HTXOxgHphL0w= github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Zk9zG8kt3mXAboclUXQlvvxKQuhnI8u5NdDEl8uotNY= github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:Lu05srGqddQRMnl1MZtGAReln2yJljeGx9b1IadlMJ8= github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Tk9bDywUmOtc0iMjjCVIwMlAQNsxCy+bK+bTNA0OaBE= github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:tQqDQw3tEHdQpt7NTdAwF3UvZ3CjNIj/IJKMRFmm388= github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:biUIbI2YxUrcQikEfS/bwPA8NsHp/WO+VZUG4morUmE= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9/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/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-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.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.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c= github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/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 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM= github.com/sagernet/sing-quic v0.6.0/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.4-0.20260315091454-bbe21100c226 h1:Shy/fsm+pqVq6OkBAWPaOmOiPT/AwoRxQLiV1357Y0Y= github.com/sagernet/sing-tun v0.8.4-0.20260315091454-bbe21100c226/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= 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.0.20260311131347-f88b27eeb76e h1:Sv1qUhJIidjSTc24XEknovDZnbmVSlAXj8wNVgIfgGo= github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e/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/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= ================================================ 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/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/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) } ================================================ 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/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 }) } ================================================ 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/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/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" "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()) } 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) 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) 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) registerDERPService(registry) registerCCMService(registry) registerOCMService(registry) registerOOMKillerService(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/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 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/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 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" "github.com/sagernet/sing/common/random" ) func init() { random.InitializeSeed() } 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 - 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 - 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 - 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 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: 拨号字段 DNS01 Challenge Fields: DNS01 验证字段 Multiplex: 多路复用 V2Ray Transport: V2Ray 传输层 Wi-Fi State: Wi-Fi 状态 Endpoint: 端点 Inbound: 入站 Outbound: 出站 Manual: 手册 reconfigure_material: true reconfigure_search: true ================================================ 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/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" "net/url" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/service" "github.com/miekg/dns" ) type RawDNSOptions struct { Servers []DNSServerOptions `json:"servers,omitempty"` Rules []DNSRule `json:"rules,omitempty"` Final string `json:"final,omitempty"` ReverseMapping bool `json:"reverse_mapping,omitempty"` DNSClientOptions } type LegacyDNSOptions struct { FakeIP *LegacyDNSFakeIPOptions `json:"fakeip,omitempty"` } type DNSOptions struct { RawDNSOptions LegacyDNSOptions } type contextKeyDontUpgrade struct{} func ContextWithDontUpgrade(ctx context.Context) context.Context { return context.WithValue(ctx, (*contextKeyDontUpgrade)(nil), true) } func dontUpgradeFromContext(ctx context.Context) bool { return ctx.Value((*contextKeyDontUpgrade)(nil)) == true } func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { err := json.UnmarshalContext(ctx, content, &o.LegacyDNSOptions) if err != nil { return err } dontUpgrade := dontUpgradeFromContext(ctx) legacyOptions := o.LegacyDNSOptions if !dontUpgrade { if o.FakeIP != nil && o.FakeIP.Enabled { deprecated.Report(ctx, deprecated.OptionLegacyDNSFakeIPOptions) ctx = context.WithValue(ctx, (*LegacyDNSFakeIPOptions)(nil), o.FakeIP) } o.LegacyDNSOptions = LegacyDNSOptions{} } err = badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions) if err != nil { return err } if !dontUpgrade { rcodeMap := make(map[string]int) o.Servers = common.Filter(o.Servers, func(it DNSServerOptions) bool { if it.Type == C.DNSTypeLegacyRcode { rcodeMap[it.Tag] = it.Options.(int) return false } return true }) if len(rcodeMap) > 0 { for i := 0; i < len(o.Rules); i++ { rewriteRcode(rcodeMap, &o.Rules[i]) } } } return nil } func rewriteRcode(rcodeMap map[string]int, rule *DNSRule) { switch rule.Type { case C.RuleTypeDefault: rewriteRcodeAction(rcodeMap, &rule.DefaultOptions.DNSRuleAction) case C.RuleTypeLogical: rewriteRcodeAction(rcodeMap, &rule.LogicalOptions.DNSRuleAction) } } func rewriteRcodeAction(rcodeMap map[string]int, ruleAction *DNSRuleAction) { if ruleAction.Action != C.RuleActionTypeRoute { return } rcode, loaded := rcodeMap[ruleAction.RouteOptions.Server] if !loaded { return } ruleAction.Action = C.RuleActionTypePredefined ruleAction.PredefinedOptions.Rcode = common.Ptr(DNSRCode(rcode)) } type DNSClientOptions struct { Strategy DomainStrategy `json:"strategy,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"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type LegacyDNSFakeIPOptions struct { Enabled bool `json:"enabled,omitempty"` Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"` Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"` } type DNSTransportOptionsRegistry interface { CreateOptions(transportType string) (any, bool) } 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) { switch o.Type { case C.DNSTypeLegacy: o.Type = "" } 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: o.Type = C.DNSTypeLegacy options = new(LegacyDNSServerOptions) deprecated.Report(ctx, deprecated.OptionLegacyDNSTransport) 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 if o.Type == C.DNSTypeLegacy && !dontUpgradeFromContext(ctx) { err = o.Upgrade(ctx) if err != nil { return err } } return nil } func (o *DNSServerOptions) Upgrade(ctx context.Context) error { if o.Type != C.DNSTypeLegacy { return nil } options := o.Options.(*LegacyDNSServerOptions) serverURL, _ := url.Parse(options.Address) var serverType string if serverURL != nil && serverURL.Scheme != "" { serverType = serverURL.Scheme } else { switch options.Address { case "local", "fakeip": serverType = options.Address default: serverType = C.DNSTypeUDP } } remoteOptions := RemoteDNSServerOptions{ RawLocalDNSServerOptions: RawLocalDNSServerOptions{ DialerOptions: DialerOptions{ Detour: options.Detour, DomainResolver: &DomainResolveOptions{ Server: options.AddressResolver, Strategy: options.AddressStrategy, }, FallbackDelay: options.AddressFallbackDelay, }, Legacy: true, LegacyStrategy: options.Strategy, LegacyDefaultDialer: options.Detour == "", LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), }, LegacyAddressResolver: options.AddressResolver, LegacyAddressStrategy: options.AddressStrategy, LegacyAddressFallbackDelay: options.AddressFallbackDelay, } switch serverType { case C.DNSTypeLocal: o.Type = C.DNSTypeLocal o.Options = &LocalDNSServerOptions{ RawLocalDNSServerOptions: remoteOptions.RawLocalDNSServerOptions, } case C.DNSTypeUDP: o.Type = C.DNSTypeUDP o.Options = &remoteOptions var serverAddr M.Socksaddr if serverURL == nil || serverURL.Scheme == "" { serverAddr = M.ParseSocksaddr(options.Address) } else { serverAddr = M.ParseSocksaddr(serverURL.Host) } if !serverAddr.IsValid() { return E.New("invalid server address") } remoteOptions.Server = serverAddr.AddrString() if serverAddr.Port != 0 && serverAddr.Port != 53 { remoteOptions.ServerPort = serverAddr.Port } case C.DNSTypeTCP: o.Type = C.DNSTypeTCP o.Options = &remoteOptions if serverURL == nil { return E.New("invalid server address") } serverAddr := M.ParseSocksaddr(serverURL.Host) if !serverAddr.IsValid() { return E.New("invalid server address") } remoteOptions.Server = serverAddr.AddrString() if serverAddr.Port != 0 && serverAddr.Port != 53 { remoteOptions.ServerPort = serverAddr.Port } case C.DNSTypeTLS, C.DNSTypeQUIC: o.Type = serverType if serverURL == nil { return E.New("invalid server address") } serverAddr := M.ParseSocksaddr(serverURL.Host) if !serverAddr.IsValid() { return E.New("invalid server address") } remoteOptions.Server = serverAddr.AddrString() if serverAddr.Port != 0 && serverAddr.Port != 853 { remoteOptions.ServerPort = serverAddr.Port } o.Options = &RemoteTLSDNSServerOptions{ RemoteDNSServerOptions: remoteOptions, } case C.DNSTypeHTTPS, C.DNSTypeHTTP3: o.Type = serverType httpsOptions := RemoteHTTPSDNSServerOptions{ RemoteTLSDNSServerOptions: RemoteTLSDNSServerOptions{ RemoteDNSServerOptions: remoteOptions, }, } o.Options = &httpsOptions if serverURL == nil { return E.New("invalid server address") } serverAddr := M.ParseSocksaddr(serverURL.Host) if !serverAddr.IsValid() { return E.New("invalid server address") } httpsOptions.Server = serverAddr.AddrString() if serverAddr.Port != 0 && serverAddr.Port != 443 { httpsOptions.ServerPort = serverAddr.Port } if serverURL.Path != "/dns-query" { httpsOptions.Path = serverURL.Path } case "rcode": var rcode int if serverURL == nil { return E.New("invalid server address") } switch serverURL.Host { case "success": rcode = dns.RcodeSuccess case "format_error": rcode = dns.RcodeFormatError case "server_failure": rcode = dns.RcodeServerFailure case "name_error": rcode = dns.RcodeNameError case "not_implemented": rcode = dns.RcodeNotImplemented case "refused": rcode = dns.RcodeRefused default: return E.New("unknown rcode: ", serverURL.Host) } o.Type = C.DNSTypeLegacyRcode o.Options = rcode case C.DNSTypeDHCP: o.Type = C.DNSTypeDHCP dhcpOptions := DHCPDNSServerOptions{} if serverURL == nil { return E.New("invalid server address") } if serverURL.Host != "" && serverURL.Host != "auto" { dhcpOptions.Interface = serverURL.Host } o.Options = &dhcpOptions case C.DNSTypeFakeIP: o.Type = C.DNSTypeFakeIP fakeipOptions := FakeIPDNSServerOptions{} if legacyOptions, loaded := ctx.Value((*LegacyDNSFakeIPOptions)(nil)).(*LegacyDNSFakeIPOptions); loaded { fakeipOptions.Inet4Range = legacyOptions.Inet4Range fakeipOptions.Inet6Range = legacyOptions.Inet6Range } o.Options = &fakeipOptions default: return E.New("unsupported DNS server scheme: ", serverType) } return nil } 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 LegacyDNSServerOptions struct { Address string `json:"address"` AddressResolver string `json:"address_resolver,omitempty"` AddressStrategy DomainStrategy `json:"address_strategy,omitempty"` AddressFallbackDelay badoption.Duration `json:"address_fallback_delay,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"` Detour string `json:"detour,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type HostsDNSServerOptions struct { Path badoption.Listable[string] `json:"path,omitempty"` Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"` } type RawLocalDNSServerOptions struct { DialerOptions Legacy bool `json:"-"` LegacyStrategy DomainStrategy `json:"-"` LegacyDefaultDialer bool `json:"-"` LegacyClientSubnet netip.Prefix `json:"-"` } type LocalDNSServerOptions struct { RawLocalDNSServerOptions PreferGo bool `json:"prefer_go,omitempty"` } type RemoteDNSServerOptions struct { RawLocalDNSServerOptions DNSServerAddressOptions LegacyAddressResolver string `json:"-"` LegacyAddressStrategy DomainStrategy `json:"-"` LegacyAddressFallbackDelay badoption.Duration `json:"-"` } 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"` } ================================================ FILE: option/dns_record.go ================================================ package option import ( "encoding/base64" "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" ) 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 := dns.NewRR(stringValue) if err != nil { return err } if a, isA := record.(*dns.A); isA { a.A = M.AddrFromIP(a.A).Unmap().AsSlice() } o.RR = record return nil } 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 } ================================================ 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"` } 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/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"` ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"` MaxConnClient int `json:"max_conn_client,omitempty"` DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` InboundTLSOptionsContainer } 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"` ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` ReceiveWindow uint64 `json:"recv_window,omitempty"` DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer } ================================================ 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 Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` BrutalDebug bool `json:"brutal_debug,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"` UpMbps int `json:"up_mbps,omitempty"` DownMbps int `json:"down_mbps,omitempty"` Obfs *Hysteria2Obfs `json:"obfs,omitempty"` Password string `json:"password,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer BrutalDebug bool `json:"brutal_debug,omitempty"` } ================================================ 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 } 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"` ChecksBeforeLimit int `json:"checks_before_limit,omitempty"` } ================================================ 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:"-"` Schema string `json:"$schema,omitempty"` Log *LogOptions `json:"log,omitempty"` DNS *DNSOptions `json:"dns,omitempty"` NTP *NTPOptions `json:"ntp,omitempty"` Certificate *CertificateOptions `json:"certificate,omitempty"` Endpoints []Endpoint `json:"endpoints,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"` Route *RouteOptions `json:"route,omitempty"` Services []Service `json:"services,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"` } type Options _Options 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) } 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 } 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/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"` Strategy DomainStrategy `json:"strategy,omitempty"` DisableCache bool `json:"disable_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.DisableCache && 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"` } 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 ( "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) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_Rule)(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, (*_Rule)(r), v) if err != nil { return err } return nil } 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"` 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 (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.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) } 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.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) } 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"` } 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"` Strategy DomainStrategy `json:"strategy,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type _DNSRouteOptionsActionOptions struct { Strategy DomainStrategy `json:"strategy,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } 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"` Strategy DomainStrategy `json:"strategy,omitempty"` DisableCache bool `json:"disable_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_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.Unmarshal(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"` Geosite badoption.Listable[string] `json:"geosite,omitempty"` SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` GeoIP badoption.Listable[string] `json:"geoip,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPIsPrivate bool `json:"ip_is_private,omitempty"` IPAcceptAny bool `json:"ip_accept_any,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` 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"` 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"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` 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 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 { err := json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule) if err != nil { return err } return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction) } 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 { err := json.Unmarshal(data, &r.RawLogicalDNSRule) if err != nil { return err } return badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction) } func (r *LogicalDNSRule) IsValid() bool { return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } ================================================ 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"` DownloadDetour string `json:"download_detour,omitempty"` UpdateInterval badoption.Duration `json:"update_interval,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"` 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: 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: 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: 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"` } ================================================ 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" "reflect" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" ) type TailscaleEndpointOptions struct { DialerOptions StateDirectory string `json:"state_directory,omitempty"` AuthKey string `json:"auth_key,omitempty"` ControlURL string `json:"control_url,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"` } 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 _DERPVerifyClientURLOptions struct { URL string `json:"url,omitempty"` DialerOptions } 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 reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) { return json.Marshal(d.URL) } else { return json.Marshal(_DERPVerifyClientURLOptions(d)) } } func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error { var stringValue string err := json.Unmarshal(bytes, &stringValue) if err == nil { d.URL = stringValue return nil } return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d)) } 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"` 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"` 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"` KernelTx bool `json:"kernel_tx,omitempty"` KernelRx bool `json:"kernel_rx,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"` } 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 } 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 } ================================================ 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"` 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: break 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) NewConnectionEx(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/direct/inbound.go ================================================ package direct import ( "context" "net" "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) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { i.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, i.listener.UDPAddr(), nil) } func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { 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) } ================================================ FILE: protocol/direct/loopback_detect.go ================================================ package direct import ( "net" "net/netip" "sync" "github.com/sagernet/sing-box/adapter" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type loopBackDetector struct { networkManager adapter.NetworkManager connAccess sync.RWMutex packetConnAccess sync.RWMutex connMap map[netip.AddrPort]netip.AddrPort packetConnMap map[uint16]uint16 } func newLoopBackDetector(networkManager adapter.NetworkManager) *loopBackDetector { return &loopBackDetector{ networkManager: networkManager, connMap: make(map[netip.AddrPort]netip.AddrPort), packetConnMap: make(map[uint16]uint16), } } func (l *loopBackDetector) NewConn(conn net.Conn) net.Conn { source := M.AddrPortFromNet(conn.LocalAddr()) if !source.IsValid() { return conn } if udpConn, isUDPConn := conn.(abstractUDPConn); isUDPConn { if !source.Addr().IsLoopback() { _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) if err != nil { return conn } } if !N.IsPublicAddr(source.Addr()) { return conn } l.packetConnAccess.Lock() l.packetConnMap[source.Port()] = M.AddrPortFromNet(conn.RemoteAddr()).Port() l.packetConnAccess.Unlock() return &loopBackDetectUDPWrapper{abstractUDPConn: udpConn, detector: l, connPort: source.Port()} } else { l.connAccess.Lock() l.connMap[source] = M.AddrPortFromNet(conn.RemoteAddr()) l.connAccess.Unlock() return &loopBackDetectWrapper{Conn: conn, detector: l, connAddr: source} } } func (l *loopBackDetector) NewPacketConn(conn N.NetPacketConn, destination M.Socksaddr) N.NetPacketConn { source := M.AddrPortFromNet(conn.LocalAddr()) if !source.IsValid() { return conn } if !source.Addr().IsLoopback() { _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) if err != nil { return conn } } l.packetConnAccess.Lock() l.packetConnMap[source.Port()] = destination.AddrPort().Port() l.packetConnAccess.Unlock() return &loopBackDetectPacketWrapper{NetPacketConn: conn, detector: l, connPort: source.Port()} } func (l *loopBackDetector) CheckConn(source netip.AddrPort, local netip.AddrPort) bool { l.connAccess.RLock() defer l.connAccess.RUnlock() destination, loaded := l.connMap[source] return loaded && destination != local } func (l *loopBackDetector) CheckPacketConn(source netip.AddrPort, local netip.AddrPort) bool { if !source.IsValid() { return false } if !source.Addr().IsLoopback() { _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) if err != nil { return false } } if N.IsPublicAddr(source.Addr()) { return false } l.packetConnAccess.RLock() defer l.packetConnAccess.RUnlock() destinationPort, loaded := l.packetConnMap[source.Port()] return loaded && destinationPort != local.Port() } type loopBackDetectWrapper struct { net.Conn detector *loopBackDetector connAddr netip.AddrPort closeOnce sync.Once } func (w *loopBackDetectWrapper) Close() error { w.closeOnce.Do(func() { w.detector.connAccess.Lock() delete(w.detector.connMap, w.connAddr) w.detector.connAccess.Unlock() }) return w.Conn.Close() } func (w *loopBackDetectWrapper) ReaderReplaceable() bool { return true } func (w *loopBackDetectWrapper) WriterReplaceable() bool { return true } func (w *loopBackDetectWrapper) Upstream() any { return w.Conn } type loopBackDetectPacketWrapper struct { N.NetPacketConn detector *loopBackDetector connPort uint16 closeOnce sync.Once } func (w *loopBackDetectPacketWrapper) Close() error { w.closeOnce.Do(func() { w.detector.packetConnAccess.Lock() delete(w.detector.packetConnMap, w.connPort) w.detector.packetConnAccess.Unlock() }) return w.NetPacketConn.Close() } func (w *loopBackDetectPacketWrapper) ReaderReplaceable() bool { return true } func (w *loopBackDetectPacketWrapper) WriterReplaceable() bool { return true } func (w *loopBackDetectPacketWrapper) Upstream() any { return w.NetPacketConn } type abstractUDPConn interface { net.Conn net.PacketConn } type loopBackDetectUDPWrapper struct { abstractUDPConn detector *loopBackDetector connPort uint16 closeOnce sync.Once } func (w *loopBackDetectUDPWrapper) Close() error { w.closeOnce.Do(func() { w.detector.packetConnAccess.Lock() delete(w.detector.packetConnMap, w.connPort) w.detector.packetConnAccess.Unlock() }) return w.abstractUDPConn.Close() } func (w *loopBackDetectUDPWrapper) ReaderReplaceable() bool { return true } func (w *loopBackDetectUDPWrapper) WriterReplaceable() bool { return true } func (w *loopBackDetectUDPWrapper) Upstream() any { return w.abstractUDPConn } ================================================ 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 // loopBack *loopBackDetector } 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}), // loopBack: newLoopBackDetector(router), } //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) } /*conn, err := h.dialer.DialContext(ctx, network, destination) if err != nil { return nil, err } return h.loopBack.NewConn(conn), nil*/ 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 } // conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination) 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 } /*func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if h.loopBack.CheckConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) { return E.New("reject loopback connection to ", metadata.Destination) } return NewConnection(ctx, h, conn, metadata) } func (h *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { if h.loopBack.CheckPacketConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) { return E.New("reject loopback packet connection to ", metadata.Destination) } return NewPacketConnection(ctx, h, conn, metadata) } */ ================================================ 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 := common.ContextWithCancelCause(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 := common.ContextWithCancelCause(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) NewConnectionEx(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) NewPacketConnectionEx(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.ConnectionHandlerEx = (*Selector)(nil) _ adapter.PacketConnectionHandlerEx = (*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) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) selected := s.selected.Load() if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler { outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) } else { s.connection.NewConnection(ctx, selected, conn, metadata, onClose) } } func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) selected := s.selected.Load() if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler { outboundHandler.NewPacketConnectionEx(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 router adapter.Router 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, router: router, 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) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) s.connection.NewConnection(ctx, s, conn, metadata, onClose) } func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { 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 router adapter.Router 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 } g.ticker = time.NewTicker(g.interval) go g.loopCheck() g.pauseCallback = pause.RegisterTicker(g.pause, g.ticker, g.interval, nil) } func (g *URLTestGroup) Close() error { g.access.Lock() defer g.access.Unlock() if g.ticker == nil { return nil } g.ticker.Stop() g.pause.UnregisterCallback(g.pauseCallback) 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() { if time.Since(g.lastActive.Load()) > g.interval { g.lastActive.Store(time.Now()) g.CheckOutbounds(false) } for { select { case <-g.close: return case <-g.ticker.C: } if time.Since(g.lastActive.Load()) > g.idleTimeout { g.access.Lock() 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) NewConnectionEx(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.NewUpstreamHandlerEx(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, UDPTimeout: udpTimeout, Handler: inbound, // Legacy options ConnReceiveWindow: options.ReceiveWindowConn, StreamReceiveWindow: options.ReceiveWindowClient, MaxIncomingStreams: int64(options.MaxConnClient), DisableMTUDiscovery: options.DisableMTUDiscovery, }) if err != nil { return nil, err } 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, UDPDisabled: !common.Contains(networkList, N.NetworkUDP), ConnReceiveWindow: options.ReceiveWindowConn, StreamReceiveWindow: options.ReceiveWindow, DisableMTUDiscovery: options.DisableMTUDiscovery, }) 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/hysteria2/inbound.go ================================================ package hysteria2 import ( "context" "net" "net/http" "net/http/httputil" "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" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" "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" ) 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 } service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ Context: ctx, Logger: logger, BrutalDebug: options.BrutalDebug, SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), SalamanderPassword: salamanderPassword, TLSConfig: tlsConfig, IgnoreClientBandwidth: options.IgnoreClientBandwidth, UDPTimeout: udpTimeout, Handler: inbound, MasqueradeHandler: masqueradeHandler, }) 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) } 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/hysteria2/outbound.go ================================================ package hysteria2 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-quic/hysteria2" "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.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 } tlsConfig, err := tls.NewClient(ctx, logger, options.Server, 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) } } outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } 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), SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), SalamanderPassword: salamanderPassword, Password: options.Password, TLSConfig: tlsConfig, UDPDisabled: !common.Contains(networkList, N.NetworkUDP), }) if err != nil { return nil, err } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria2, 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) } func (h *Outbound) InterfaceUpdated() { h.client.CloseWithError(E.New("network changed")) } func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } ================================================ 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) NewConnectionEx(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.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) default: return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) } } 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) 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 := 0; i < 16; i++ { 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)) buffer.Extend(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) buffer.Extend(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, baderror.WrapH2(err) } func (c *naiveConn) Write(p []byte) (n int, err error) { n, err = c.writeChunked(c.Conn, p) return n, baderror.WrapH2(err) } func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() err := c.writeBufferWithPadding(c.Conn, buffer) return baderror.WrapH2(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, baderror.WrapH2(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, baderror.WrapH2(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 baderror.WrapH2(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 } } ================================================ 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) NewConnectionEx(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) NewConnectionEx(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) NewPacketEx(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.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) case common.Contains(shadowaead.List, options.Method): inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) case common.Contains(shadowaead_2022.List, options.Method): inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) 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) NewConnectionEx(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) NewPacketEx(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.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx), ) } else if common.Contains(shadowaead.List, options.Method) { service, err = shadowaead.NewMultiService[int]( options.Method, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ) } 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) NewConnectionEx(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) NewPacketEx(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.NewUpstreamHandler(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) NewConnectionEx(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) NewPacketEx(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) NewConnectionEx(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) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) 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 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, 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)) }, } 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/dns_transport.go ================================================ //go:build with_gvisor package tailscale import ( "context" "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/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 dnsRouter adapter.DNSRouter endpointManager adapter.EndpointManager endpoint *Endpoint routePrefixes []netip.Prefix routes map[string][]adapter.DNSTransport hosts map[string][]netip.Addr 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, 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() { } 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 { t.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 } 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.routes = routes t.hosts = hosts t.defaultResolvers = defaultResolvers if len(defaultResolvers) > 0 { t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, 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") } 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 { return nil } func (t *DNSTransport) Raw() bool { return true } func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if len(message.Question) != 1 { return nil, os.ErrInvalid } question := message.Question[0] addresses, hostsLoaded := t.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 t.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 t.acceptDefaultResolvers { if len(t.defaultResolvers) > 0 { var lastErr error for _, resolver := range t.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 } 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") } for _, prefix := range d.transport.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") } for _, prefix := range d.transport.routePrefixes { if prefix.Contains(destination.Addr) { return d.transport.endpoint.ListenPacket(ctx, destination) } } return d.fallbackDialer.ListenPacket(ctx, destination) } ================================================ FILE: protocol/tailscale/endpoint.go ================================================ //go:build with_gvisor package tailscale import ( "context" "crypto/tls" "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/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/common/ntp" "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 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 } 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) 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: &http.Client{ Transport: &http.Transport{ ForceAttemptHTTP2: true, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address)) }, TLSClientConfig: &tls.Config{ RootCAs: adapter.RootPoolFromContext(ctx), Time: ntp.TimeFuncFromContext(ctx), }, }, }, } return &Endpoint{ Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), 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 { if stage != adapter.StartStateStart { return nil } 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)) }) }) } err := t.server.Start() if err != nil { if t.systemTun != nil { _ = t.systemTun.Close() } return err } 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() 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 { netmon.RegisterInterfaceGetter(nil) netns.SetControlFunc(nil) if t.fallbackTCPCloser != nil { t.fallbackTCPCloser() t.fallbackTCPCloser = nil } err := common.Close(common.PtrOrNil(t.server)) 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 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.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) { 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) { 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/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" "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 := 0; i < n; i++ { 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 { common.ClearArray(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" 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 *ProxyListener 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 } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTor, tag, []string{N.NetworkTCP}, options.DialerOptions), ctx: ctx, logger: logger, proxy: NewProxyListener(ctx, logger, outboundDialer), 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() err = t.proxy.Start() if err != nil { return err } proxyPort := "127.0.0.1:" + F.ToString(t.proxy.Port()) proxyUsername := t.proxy.Username() proxyPassword := t.proxy.Password() 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/tor/proxy.go ================================================ package tor import ( std_bufio "bufio" "context" "crypto/rand" "encoding/hex" "net" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/service" ) type ProxyListener struct { ctx context.Context logger log.ContextLogger dialer N.Dialer connection adapter.ConnectionManager tcpListener *net.TCPListener username string password string authenticator *auth.Authenticator } func NewProxyListener(ctx context.Context, logger log.ContextLogger, dialer N.Dialer) *ProxyListener { var usernameB [64]byte var passwordB [64]byte rand.Read(usernameB[:]) rand.Read(passwordB[:]) username := hex.EncodeToString(usernameB[:]) password := hex.EncodeToString(passwordB[:]) return &ProxyListener{ ctx: ctx, logger: logger, dialer: dialer, connection: service.FromContext[adapter.ConnectionManager](ctx), authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}), username: username, password: password, } } func (l *ProxyListener) Start() error { tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), }) if err != nil { return err } l.tcpListener = tcpListener go l.acceptLoop() return nil } func (l *ProxyListener) Port() uint16 { if l.tcpListener == nil { panic("start listener first") } return M.SocksaddrFromNet(l.tcpListener.Addr()).Port } func (l *ProxyListener) Username() string { return l.username } func (l *ProxyListener) Password() string { return l.password } func (l *ProxyListener) Close() error { return common.Close(l.tcpListener) } func (l *ProxyListener) acceptLoop() { for { tcpConn, err := l.tcpListener.AcceptTCP() if err != nil { return } ctx := log.ContextWithNewID(l.ctx) go func() { hErr := l.accept(ctx, tcpConn) if hErr != nil { if E.IsClosedOrCanceled(hErr) { l.logger.DebugContext(ctx, E.Cause(hErr, "proxy connection closed")) return } l.logger.ErrorContext(ctx, E.Cause(hErr, "proxy")) } }() } } func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, l, nil, 0, M.SocksaddrFromNet(conn.RemoteAddr()), nil) } func (l *ProxyListener) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Source = source metadata.Destination = destination metadata.Network = N.NetworkTCP l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination) l.connection.NewConnection(ctx, l.dialer, conn, metadata, onClose) } func (l *ProxyListener) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Source = source metadata.Destination = destination metadata.Network = N.NetworkUDP l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination) l.connection.NewPacketConnection(ctx, l.dialer, conn, metadata, onClose) } ================================================ 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.NewUpstreamContextHandlerEx(inbound.fallbackConnection, nil) } service := trojan.NewService[int](adapter.NewUpstreamContextHandlerEx(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) NewConnectionEx(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).NewConnectionEx(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" "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, 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" "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, 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/hook.go ================================================ package tun var HookBeforeCreatePlatformInterface func() ================================================ 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 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") } 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, 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.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 { if HookBeforeCreatePlatformInterface != nil { HookBeforeCreatePlatformInterface() } 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 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 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 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.NewUpstreamContextHandlerEx(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) NewConnectionEx(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).NewConnectionEx(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.NewUpstreamContextHandlerEx(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) NewConnectionEx(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).NewConnectionEx(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" "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 } 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: return w.endpoint.Start(true) } return nil } func (w *Endpoint) Close() error { 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) { 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 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 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 { return w.endpoint.Lookup(address) != nil } func (w *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { 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_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,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_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/tlsfragment" 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) } var done atomic.Bool if m.kickWriteHandshake(ctx, conn, remoteConn, false, &done, onClose) { return } if m.kickWriteHandshake(ctx, remoteConn, conn, 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, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) bool { if !N.NeedHandshakeForWrite(destination) { return false } var ( cachedBuffer *buf.Buffer wrotePayload bool ) sourceReader, readCounters := N.UnwrapCountReader(source, nil) destinationWriter, writeCounters := N.UnwrapCountWriter(destination, nil) if cachedReader, ok := sourceReader.(N.CachedReader); ok { cachedBuffer = cachedReader.ReadCached() } var err error if cachedBuffer != nil { wrotePayload = true dataLen := cachedBuffer.Len() _, err = destinationWriter.Write(cachedBuffer.Bytes()) cachedBuffer.Release() if err == nil { for _, counter := range readCounters { counter(int64(dataLen)) } for _, counter := range writeCounters { counter(int64(dataLen)) } } } else { _ = 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) 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_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 strings.HasPrefix(line, "hardware ethernet ") { macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";") parsed, macErr := net.ParseMAC(macString) if macErr == nil { currentMAC = parsed } } else if strings.HasPrefix(line, "client-hostname ") { hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";") hostname = strings.Trim(hostname, "\"") if hostname != "" { currentHostname = hostname } } else if strings.HasPrefix(line, "binding state ") { state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";") currentActive = state == "active" } } } func parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { scanner := bufio.NewScanner(file) firstLine := true for scanner.Scan() { if firstLine { firstLine = false continue } fields := strings.Split(scanner.Text(), ",") if len(fields) < 10 { continue } if fields[9] != "0" { continue } address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) if !addrOK { continue } address = address.Unmap() mac, macErr := net.ParseMAC(fields[1]) if macErr != nil { continue } ipToMAC[address] = mac hostname := "" if len(fields) > 8 { hostname = fields[8] } if hostname != "" { ipToHostname[address] = hostname macToHostname[mac.String()] = hostname } } } func parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { scanner := bufio.NewScanner(file) firstLine := true for scanner.Scan() { if firstLine { firstLine = false continue } fields := strings.Split(scanner.Text(), ",") if len(fields) < 14 { continue } if fields[13] != "0" { continue } address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) if !addrOK { continue } address = address.Unmap() var mac net.HardwareAddr if fields[12] != "" { mac, _ = net.ParseMAC(fields[12]) } if mac == nil { duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", "")) if duidErr == nil { mac, _ = extractMACFromDUID(duid) } } hostname := "" if len(fields) > 11 { hostname = fields[11] } if mac != nil { ipToMAC[address] = mac } if hostname != "" { ipToHostname[address] = hostname if mac != nil { macToHostname[mac.String()] = hostname } } } } func parseBootpdLeases(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { now := time.Now().Unix() scanner := bufio.NewScanner(file) var currentName string var currentIP netip.Addr var currentMAC net.HardwareAddr var currentLease int64 var inBlock bool for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "{" { inBlock = true currentName = "" currentIP = netip.Addr{} currentMAC = nil currentLease = 0 continue } if line == "}" && inBlock { if currentMAC != nil && currentIP.IsValid() { if currentLease == 0 || currentLease >= now { ipToMAC[currentIP] = currentMAC if currentName != "" { ipToHostname[currentIP] = currentName macToHostname[currentMAC.String()] = currentName } } } inBlock = false continue } if !inBlock { continue } key, value, found := strings.Cut(line, "=") if !found { continue } switch key { case "name": currentName = value case "ip_address": parsed, addrOK := netip.AddrFromSlice(net.ParseIP(value)) if addrOK { currentIP = parsed.Unmap() } case "hw_address": typeAndMAC, hasSep := strings.CutPrefix(value, "1,") if hasSep { mac, macErr := net.ParseMAC(typeAndMAC) if macErr == nil { currentMAC = mac } } case "lease": leaseHex := strings.TrimPrefix(value, "0x") parsed, parseErr := strconv.ParseInt(leaseHex, 16, 64) if parseErr == nil { currentLease = parsed } } } } ================================================ 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) 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) 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 { logger logger.ContextLogger 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{ 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), DisableCache: defaultDomainResolver.DisableCache, 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: 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) { 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() } } } 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) } ================================================ 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/process" "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 interface{}) 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.NewConnectionEx(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 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.ConnectionHandlerEx); isHandler { outboundHandler.NewConnectionEx(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 interface{}) 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.NewPacketConnectionEx(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)) }*/ 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.PacketConnectionHandlerEx); isHandler { outboundHandler.NewPacketConnectionEx(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, ) { if r.processSearcher != nil && metadata.ProcessInfo == nil { var originDestination netip.AddrPort if metadata.OriginDestination.IsValid() { originDestination = metadata.OriginDestination.AddrPort() } else if metadata.Destination.IsIP() { originDestination = metadata.Destination.AddrPort() } processInfo, fErr := process.FindProcessInfo(r.processSearcher, ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) if fErr != nil { r.logger.InfoContext(ctx, "failed to search process: ", fErr) } else { 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) } } else if processInfo.AndroidPackageName != "" { r.logger.InfoContext(ctx, "found package name: ", processInfo.AndroidPackageName) } else 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) } } metadata.ProcessInfo = processInfo } } 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 } 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 } } 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, RewriteTTL: action.RewriteTTL, 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" "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" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/task" "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 rules []adapter.Rule needFindProcess bool needFindNeighbor bool leaseFiles []string ruleSets []adapter.RuleSet ruleSetMap map[string]adapter.RuleSet processSearcher process.Searcher 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), rules: make([]adapter.Rule, 0, len(options.Rules)), ruleSetMap: make(map[string]adapter.RuleSet), needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, needFindNeighbor: hasRule(options.Rules, isNeighborRule) || hasDNSRule(dnsOptions.Rules, isNeighborDNSRule) || options.FindNeighbor, leaseFiles: options.DHCPLeaseFiles, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), } } func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) error { for i, options := range rules { 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.StartStateStart: var cacheContext *adapter.HTTPStartContext if len(r.ruleSets) > 0 { monitor.Start("initialize rule-set") cacheContext = adapter.NewHTTPStartContext(r.ctx) var ruleSetStartGroup task.Group for i, ruleSet := range r.ruleSets { ruleSetInPlace := ruleSet ruleSetStartGroup.Append0(func(ctx context.Context) error { err := ruleSetInPlace.StartContext(ctx, cacheContext) 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 cacheContext != nil { cacheContext.Close() } r.network.Initialize(r.ruleSets) needFindProcess := r.needFindProcess needFindNeighbor := r.needFindNeighbor 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 } } } r.needFindNeighbor = needFindNeighbor if needFindNeighbor { if r.platformInterface != nil && r.platformInterface.UsePlatformNeighborResolver() { monitor.Start("initialize neighbor resolver") resolver := newPlatformNeighborResolver(r.logger, r.platformInterface) err := resolver.Start() monitor.Finish() if err != nil { r.logger.Error(E.Cause(err, "start neighbor resolver")) } else { r.neighborResolver = resolver } } else { monitor.Start("initialize neighbor resolver") resolver, err := newNeighborResolver(r.logger, r.leaseFiles) monitor.Finish() if err != nil { if err != os.ErrInvalid { r.logger.Error(E.Cause(err, "create neighbor resolver")) } } else { err = resolver.Start() if err != nil { r.logger.Error(E.Cause(err, "start neighbor resolver")) } else { r.neighborResolver = resolver } } } } case adapter.StartStatePostStart: for i, rule := range r.rules { monitor.Start("initialize rule[", i, "]") 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() } 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.network.ResetNetwork() r.dns.ResetNetwork() } ================================================ FILE: route/rule/rule_abstract.go ================================================ package rule import ( "io" "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" ) type abstractDefaultRule struct { items []RuleItem sourceAddressItems []RuleItem sourcePortItems []RuleItem destinationAddressItems []RuleItem destinationIPCIDRItems []RuleItem destinationPortItems []RuleItem allItems []RuleItem ruleSetItem RuleItem invert bool action adapter.RuleAction } func (r *abstractDefaultRule) Type() string { return C.RuleTypeDefault } func (r *abstractDefaultRule) Start() error { for _, item := range r.allItems { if starter, isStarter := item.(interface { Start() error }); isStarter { err := starter.Start() if err != nil { return err } } } return nil } func (r *abstractDefaultRule) Close() error { for _, item := range r.allItems { err := common.Close(item) if err != nil { return err } } return nil } func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { if len(r.allItems) == 0 { return true } if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { metadata.DidMatch = true for _, item := range r.sourceAddressItems { if item.Match(metadata) { metadata.SourceAddressMatch = true break } } } if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { metadata.DidMatch = true for _, item := range r.sourcePortItems { if item.Match(metadata) { metadata.SourcePortMatch = true break } } } if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { metadata.DidMatch = true for _, item := range r.destinationAddressItems { if item.Match(metadata) { metadata.DestinationAddressMatch = true break } } } if !metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0 && !metadata.DestinationAddressMatch { metadata.DidMatch = true for _, item := range r.destinationIPCIDRItems { if item.Match(metadata) { metadata.DestinationAddressMatch = true break } } } if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { metadata.DidMatch = true for _, item := range r.destinationPortItems { if item.Match(metadata) { metadata.DestinationPortMatch = true break } } } for _, item := range r.items { metadata.DidMatch = true if !item.Match(metadata) { return r.invert } } if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { return r.invert } if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { return r.invert } if ((!metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0) || len(r.destinationAddressItems) > 0) && !metadata.DestinationAddressMatch { return r.invert } if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { return r.invert } if !metadata.DidMatch { return true } return !r.invert } 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 { if r.mode == C.LogicalTypeAnd { return common.All(r.rules, func(it adapter.HeadlessRule) bool { metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } else { return common.Any(r.rules, func(it adapter.HeadlessRule) bool { metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } } 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+" ") + ")" } } ================================================ 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{ items: []RuleItem{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 { testCase := testCase 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" 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 NewRuleAction(ctx context.Context, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { switch action.Action { case "": return nil, nil case C.RuleActionTypeRoute: return &RuleActionRoute{ Outbound: action.RouteOptions.Outbound, RuleActionRouteOptions: RuleActionRouteOptions{ OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0), OverridePort: action.RouteOptions.OverridePort, NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptions.NetworkStrategy), FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, UDPConnect: action.RouteOptions.UDPConnect, TLSFragment: action.RouteOptions.TLSFragment, TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay), TLSRecordFragment: action.RouteOptions.TLSRecordFragment, }, }, nil case C.RuleActionTypeRouteOptions: return &RuleActionRouteOptions{ OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptionsOptions.OverrideAddress, 0), OverridePort: action.RouteOptionsOptions.OverridePort, NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptionsOptions.NetworkStrategy), FallbackDelay: time.Duration(action.RouteOptionsOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, UDPConnect: action.RouteOptionsOptions.UDPConnect, UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout), TLSFragment: action.RouteOptionsOptions.TLSFragment, TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay), TLSRecordFragment: action.RouteOptionsOptions.TLSRecordFragment, }, nil case C.RuleActionTypeBypass: return &RuleActionBypass{ Outbound: action.BypassOptions.Outbound, RuleActionRouteOptions: RuleActionRouteOptions{ OverrideAddress: M.ParseSocksaddrHostPort(action.BypassOptions.OverrideAddress, 0), OverridePort: action.BypassOptions.OverridePort, NetworkStrategy: (*C.NetworkStrategy)(action.BypassOptions.NetworkStrategy), FallbackDelay: time.Duration(action.BypassOptions.FallbackDelay), UDPDisableDomainUnmapping: action.BypassOptions.UDPDisableDomainUnmapping, UDPConnect: action.BypassOptions.UDPConnect, TLSFragment: action.BypassOptions.TLSFragment, TLSFragmentFallbackDelay: time.Duration(action.BypassOptions.TLSFragmentFallbackDelay), TLSRecordFragment: action.BypassOptions.TLSRecordFragment, }, }, 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, Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), DisableCache: action.ResolveOptions.DisableCache, 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), DisableCache: action.RouteOptions.DisableCache, RewriteTTL: action.RouteOptions.RewriteTTL, ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), }, } case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), DisableCache: action.RouteOptionsOptions.DisableCache, RewriteTTL: action.RouteOptionsOptions.RewriteTTL, ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), } 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 } 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.NetworkType, 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") } return descriptions } type RuleActionDNSRoute struct { Server string RuleActionDNSRouteOptions } func (r *RuleActionDNSRoute) Type() string { return C.RuleActionTypeRoute } func (r *RuleActionDNSRoute) String() string { var descriptions []string descriptions = append(descriptions, r.Server) if r.DisableCache { descriptions = append(descriptions, "disable-cache") } if r.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) } if r.ClientSubnet.IsValid() { descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) } return F.ToString("route(", strings.Join(descriptions, ","), ")") } type RuleActionDNSRouteOptions struct { Strategy C.DomainStrategy DisableCache 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.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) } 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 Strategy C.DomainStrategy DisableCache 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.RewriteTTL != nil { options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL)) } 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 } 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.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.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } return rule, nil } var _ adapter.Rule = (*LogicalRule)(nil) type LogicalRule struct { abstractLogicalRule } 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 { 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/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 NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, 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.Server == "" && checkServer { return nil, E.New("missing server field") } } return NewDefaultDNSRule(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.Server == "" && checkServer { return nil, E.New("missing server field") } } return NewLogicalDNSRule(ctx, logger, options.LogicalOptions) default: return nil, E.New("unknown rule type: ", options.Type) } } var _ adapter.DNSRule = (*DefaultDNSRule)(nil) type DefaultDNSRule struct { abstractDefaultRule } func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { rule := &DefaultDNSRule{ abstractDefaultRule: abstractDefaultRule{ invert: options.Invert, action: NewDNSRuleAction(logger, options.DNSRuleAction), }, } 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 { 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 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.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.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) rule.items = append(rule.items, 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 } for _, rawRule := range r.items { ruleSet, isRuleSet := rawRule.(*RuleSetItem) if !isRuleSet { continue } if ruleSet.ContainsDestinationIPCIDRRule() { return true } } return false } func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool { metadata.IgnoreDestinationIPCIDRMatch = true defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() return r.abstractDefaultRule.Match(metadata) } func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { return r.abstractDefaultRule.Match(metadata) } var _ adapter.DNSRule = (*LogicalDNSRule)(nil) type LogicalDNSRule struct { abstractLogicalRule } func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*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 { rule, err := NewDNSRule(ctx, logger, subRule, false) 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 { if r.mode == C.LogicalTypeAnd { return common.All(r.rules, func(it adapter.HeadlessRule) bool { metadata.ResetRuleCache() return it.(adapter.DNSRule).Match(metadata) }) != r.invert } else { return common.Any(r.rules, func(it adapter.HeadlessRule) bool { metadata.ResetRuleCache() return it.(adapter.DNSRule).Match(metadata) }) != r.invert } } func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { return common.All(r.rules, func(it adapter.HeadlessRule) bool { metadata.ResetRuleCache() return it.(adapter.DNSRule).MatchAddressLimit(metadata) }) != r.invert } else { return common.Any(r.rules, func(it adapter.HeadlessRule) bool { metadata.ResetRuleCache() return it.(adapter.DNSRule).MatchAddressLimit(metadata) }) != r.invert } } ================================================ 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 NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { networkManager := service.FromContext[adapter.NetworkManager](ctx) rule := &DefaultHeadlessRule{ abstractDefaultRule{ invert: options.Invert, }, } 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 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 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" "strings" "github.com/sagernet/sing-box/adapter" 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.Destination.IsIP() { return r.ipSet.Contains(metadata.Destination.Addr) } if len(metadata.DestinationAddresses) > 0 { for _, address := range metadata.DestinationAddresses { if r.ipSet.Contains(address) { return true } } return false } 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 ( "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) { for _, domainItem := range domains { if domainItem == "" { return nil, E.New("domain: empty item is not allowed") } } for _, domainSuffixItem := range domainSuffixes { if domainSuffixItem == "" { 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 { 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 ( "net/netip" "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 { var destination netip.Addr if r.isSource { destination = metadata.Source.Addr } else { destination = metadata.Destination.Addr } if destination.IsValid() { return !N.IsPublicAddr(destination) } if !r.isSource { 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 || metadata.ProcessInfo.AndroidPackageName == "" { return false } return r.packageMap[metadata.ProcessInfo.AndroidPackageName] } 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_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_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" ) 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 || metadata.ProcessInfo.ProcessPath == "" { return false } return r.processMap[metadata.ProcessInfo.ProcessPath] } 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_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 { for _, tag := range r.tagList { ruleSet, loaded := r.router.RuleSet(tag) if !loaded { return E.New("rule-set not found: ", tag) } ruleSet.IncRef() r.setList = append(r.setList, ruleSet) } return nil } func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { metadata.IPCIDRMatchSource = r.ipCidrMatchSource metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty for _, ruleSet := range r.setList { if ruleSet.Match(metadata) { return true } } return false } 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_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[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_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() match: for ifType, addresses := range r.interfaceAddresses { for _, networkInterface := range interfaces { 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" "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" "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), nil 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 } 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 } ================================================ 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, "]") } } var metadata adapter.RuleSetMetadata metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule) metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule) metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) 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 { for _, rule := range s.rules { if rule.Match(metadata) { return true } } return false } ================================================ FILE: route/rule/rule_set_remote.go ================================================ package rule import ( "bytes" "context" "crypto/tls" "io" "net" "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/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" "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 dialer N.Dialer 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 { 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), } } 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) var dialer N.Dialer if s.options.RemoteOptions.DownloadDetour != "" { outbound, loaded := s.outbound.Outbound(s.options.RemoteOptions.DownloadDetour) if !loaded { return E.New("download detour not found: ", s.options.RemoteOptions.DownloadDetour) } dialer = outbound } else { dialer = s.outbound.Default() } s.dialer = dialer 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, startContext) 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, "]") } } s.access.Lock() s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.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 { err := s.fetch(s.ctx, nil) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) } else if s.refs.Load() == 0 { s.rules = nil } } for { runtime.GC() select { case <-s.ctx.Done(): return case <-s.updateTicker.C: s.updateOnce() } } } func (s *RemoteRuleSet) updateOnce() { err := s.fetch(s.ctx, nil) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) } else if s.refs.Load() == 0 { s.rules = nil } } func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPStartContext) error { s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) var httpClient *http.Client if startContext != nil { httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer) } else { httpClient = &http.Client{ Transport: &http.Transport{ ForceAttemptHTTP2: true, TLSHandshakeTimeout: C.TCPTimeout, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, TLSClientConfig: &tls.Config{ Time: ntp.TimeFuncFromContext(s.ctx), RootCAs: adapter.RootPoolFromContext(s.ctx), }, }, } } request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) if err != nil { return err } if s.lastEtag != "" { request.Header.Set("If-None-Match", s.lastEtag) } response, err := httpClient.Do(request.WithContext(ctx)) if err != nil { return err } 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 { response.Body.Close() return err } err = s.loadBytes(content) if err != nil { response.Body.Close() return err } response.Body.Close() eTagHeader := response.Header.Get("Etag") if eTagHeader != "" { s.lastEtag = eTagHeader } 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) Close() error { s.rules = nil s.cancel() if s.updateTicker != nil { s.updateTicker.Stop() } return nil } func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { for _, rule := range s.rules { if rule.Match(metadata) { return true } } return false } ================================================ 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.User) > 0 || len(rule.UserID) > 0 } func isProcessDNSRule(rule option.DefaultDNSRule) bool { return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 } 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 } ================================================ 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 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 trackingGroup sync.WaitGroup shuttingDown bool } 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.Split(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 bytes.HasPrefix(line, []byte("data: ")) { eventData := bytes.TrimPrefix(line, []byte("data: ")) 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" stdTLS "crypto/tls" "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" "github.com/sagernet/sing/common/ntp" 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 for index, options := range d.verifyClientURL { verifyDialer, createErr := dialer.NewWithOptions(dialer.Options{ Context: d.ctx, Options: options.DialerOptions, RemoteIsDomain: options.ServerIsDomain(), NewDialer: true, }) if createErr != nil { return E.Cause(createErr, "verify_client_url[", index, "]") } httpClients = append(httpClients, &http.Client{ Transport: &http.Transport{ ForceAttemptHTTP2: true, TLSClientConfig: &stdTLS.Config{ RootCAs: adapter.RootPoolFromContext(d.ctx), Time: ntp.TimeFuncFromContext(d.ctx), }, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, }, }) urls = append(urls, options.URL) } 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, common.PtrValueOrDefault(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 { return common.Close( common.PtrOrNil(d.listener), d.tlsConfig, ) } 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 bytes.HasPrefix(line, []byte("data: ")) { eventData := bytes.TrimPrefix(line, []byte("data: ")) 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 := stats.InputTokens - stats.CachedTokens if regularInputTokens < 0 { regularInputTokens = 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/config.go ================================================ package oomkiller import ( "time" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, useAvailable bool) (timerConfig, error) { safetyMargin := uint64(defaultSafetyMargin) if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 { safetyMargin = options.SafetyMargin.Value() } minInterval := defaultMinInterval if options.MinInterval != 0 { minInterval = time.Duration(options.MinInterval.Build()) if minInterval <= 0 { return timerConfig{}, E.New("min_interval must be greater than 0") } } maxInterval := defaultMaxInterval if options.MaxInterval != 0 { maxInterval = time.Duration(options.MaxInterval.Build()) if maxInterval <= 0 { return timerConfig{}, E.New("max_interval must be greater than 0") } } if maxInterval < minInterval { return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval") } checksBeforeLimit := defaultChecksBeforeLimit if options.ChecksBeforeLimit != 0 { checksBeforeLimit = options.ChecksBeforeLimit if checksBeforeLimit <= 0 { return timerConfig{}, E.New("checks_before_limit must be greater than 0") } } return timerConfig{ memoryLimit: memoryLimit, safetyMargin: safetyMargin, minInterval: minInterval, maxInterval: maxInterval, checksBeforeLimit: checksBeforeLimit, useAvailable: useAvailable, }, nil } ================================================ FILE: service/oomkiller/service.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_WARN | DISPATCH_MEMORYPRESSURE_CRITICAL, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) ); dispatch_source_set_event_handler(memoryPressureSource, ^{ unsigned long status = dispatch_source_get_data(memoryPressureSource); goMemoryPressureCallback(status); }); dispatch_activate(memoryPressureSource); } static void stopMemoryPressureMonitor() { if (memoryPressureSource) { dispatch_source_cancel(memoryPressureSource); memoryPressureSource = NULL; } } */ import "C" import ( "context" runtimeDebug "runtime/debug" "sync" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" boxConstant "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/service" ) func RegisterService(registry *boxService.Registry) { boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) } var ( globalAccess sync.Mutex globalServices []*Service ) type Service struct { boxService.Adapter logger log.ContextLogger router adapter.Router memoryLimit uint64 hasTimerMode bool useAvailable bool timerConfig timerConfig adaptiveTimer *adaptiveTimer } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { s := &Service{ Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), logger: logger, router: service.FromContext[adapter.Router](ctx), } if options.MemoryLimit != nil { s.memoryLimit = options.MemoryLimit.Value() if s.memoryLimit > 0 { s.hasTimerMode = true } } config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable) if err != nil { return nil, err } s.timerConfig = config return s, nil } func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if s.hasTimerMode { s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig) if s.memoryLimit > 0 { s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB") } else { s.logger.Info("started memory monitor with available memory detection") } } else { s.logger.Info("started memory pressure monitor") } globalAccess.Lock() isFirst := len(globalServices) == 0 globalServices = append(globalServices, s) globalAccess.Unlock() if isFirst { C.startMemoryPressureMonitor() } return nil } func (s *Service) Close() error { if s.adaptiveTimer != nil { s.adaptiveTimer.stop() } globalAccess.Lock() for i, svc := range globalServices { if svc == s { globalServices = append(globalServices[:i], globalServices[i+1:]...) break } } isLast := len(globalServices) == 0 globalAccess.Unlock() if isLast { C.stopMemoryPressureMonitor() } return nil } //export goMemoryPressureCallback func goMemoryPressureCallback(status C.ulong) { globalAccess.Lock() services := make([]*Service, len(globalServices)) copy(services, globalServices) globalAccess.Unlock() if len(services) == 0 { return } criticalFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_CRITICAL) warnFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_WARN) isCritical := status&criticalFlag != 0 isWarning := status&warnFlag != 0 var level string switch { case isCritical: level = "critical" case isWarning: level = "warning" default: level = "normal" } var freeOSMemory bool for _, s := range services { usage := memory.Total() if s.hasTimerMode { if isCritical { s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") if s.adaptiveTimer != nil { s.adaptiveTimer.startNow() } } else if isWarning { s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") } else { s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") if s.adaptiveTimer != nil { s.adaptiveTimer.stop() } } } else { if isCritical { s.logger.Error("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB, resetting network") s.router.ResetNetwork() freeOSMemory = true } else if isWarning { s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") } else { s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") } } } if freeOSMemory { runtimeDebug.FreeOSMemory() } } ================================================ FILE: service/oomkiller/service_stub.go ================================================ //go:build !darwin || !cgo package oomkiller import ( "context" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" boxConstant "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/service" ) func RegisterService(registry *boxService.Registry) { boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) } type Service struct { boxService.Adapter logger log.ContextLogger router adapter.Router adaptiveTimer *adaptiveTimer timerConfig timerConfig hasTimerMode bool useAvailable bool memoryLimit uint64 } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { s := &Service{ Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), logger: logger, router: service.FromContext[adapter.Router](ctx), } if options.MemoryLimit != nil { s.memoryLimit = options.MemoryLimit.Value() } if s.memoryLimit > 0 { s.hasTimerMode = true } else if memory.AvailableSupported() { s.useAvailable = true s.hasTimerMode = true } config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable) if err != nil { return nil, err } s.timerConfig = config return s, nil } func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if !s.hasTimerMode { return E.New("memory pressure monitoring is not available on this platform without memory_limit") } s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig) s.adaptiveTimer.start(0) if s.useAvailable { s.logger.Info("started memory monitor with available memory detection") } else { s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB") } return nil } func (s *Service) Close() error { if s.adaptiveTimer != nil { s.adaptiveTimer.stop() } return nil } ================================================ FILE: service/oomkiller/service_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/common/memory" ) const ( defaultChecksBeforeLimit = 4 defaultMinInterval = 500 * time.Millisecond defaultMaxInterval = 10 * time.Second defaultSafetyMargin = 5 * 1024 * 1024 ) type adaptiveTimer struct { logger log.ContextLogger router adapter.Router memoryLimit uint64 safetyMargin uint64 minInterval time.Duration maxInterval time.Duration checksBeforeLimit int useAvailable bool access sync.Mutex timer *time.Timer previousUsage uint64 lastInterval time.Duration } type timerConfig struct { memoryLimit uint64 safetyMargin uint64 minInterval time.Duration maxInterval time.Duration checksBeforeLimit int useAvailable bool } func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig) *adaptiveTimer { return &adaptiveTimer{ logger: logger, router: router, memoryLimit: config.memoryLimit, safetyMargin: config.safetyMargin, minInterval: config.minInterval, maxInterval: config.maxInterval, checksBeforeLimit: config.checksBeforeLimit, useAvailable: config.useAvailable, } } func (t *adaptiveTimer) start(_ uint64) { t.access.Lock() defer t.access.Unlock() t.startLocked() } func (t *adaptiveTimer) startNow() { t.access.Lock() t.startLocked() t.access.Unlock() t.poll() } func (t *adaptiveTimer) startLocked() { if t.timer != nil { return } t.previousUsage = memory.Total() t.lastInterval = t.minInterval t.timer = time.AfterFunc(t.minInterval, t.poll) } func (t *adaptiveTimer) stop() { t.access.Lock() defer t.access.Unlock() t.stopLocked() } func (t *adaptiveTimer) stopLocked() { if t.timer != nil { t.timer.Stop() t.timer = nil } } func (t *adaptiveTimer) running() bool { t.access.Lock() defer t.access.Unlock() return t.timer != nil } func (t *adaptiveTimer) poll() { t.access.Lock() defer t.access.Unlock() if t.timer == nil { return } usage := memory.Total() delta := int64(usage) - int64(t.previousUsage) t.previousUsage = usage var remaining uint64 var triggered bool if t.memoryLimit > 0 { if usage >= t.memoryLimit { remaining = 0 triggered = true } else { remaining = t.memoryLimit - usage } } else if t.useAvailable { available := memory.Available() if available <= t.safetyMargin { remaining = 0 triggered = true } else { remaining = available - t.safetyMargin } } else { remaining = 0 } if triggered { t.logger.Error("memory threshold reached, usage: ", usage/(1024*1024), " MiB, resetting network") t.router.ResetNetwork() runtimeDebug.FreeOSMemory() } var interval time.Duration if triggered { interval = t.maxInterval } else if delta <= 0 { interval = t.maxInterval } else if t.checksBeforeLimit <= 0 { interval = t.maxInterval } else { timeToLimit := time.Duration(float64(remaining) / float64(delta) * float64(t.lastInterval)) interval = timeToLimit / time.Duration(t.checksBeforeLimit) if interval < t.minInterval { interval = t.minInterval } if interval > t.maxInterval { interval = t.maxInterval } } t.lastInterval = interval t.timer.Reset(interval) } ================================================ FILE: service/resolved/resolve1.go ================================================ //go:build linux package resolved import ( "context" "errors" "fmt" "net/netip" "os" "os/user" "path/filepath" "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.Split(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 i := len(address) - 1; i >= 0; i-- { b := address[i] 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) NewConnectionEx(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) NewPacketEx(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) 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) 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 := uint32(0); j < sLen; j++ { 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 } // Escape backslashes and all the bytes that are in set. func backslashEscape(s string, set []byte) string { var buf bytes.Buffer for _, b := range []byte(s) { if b == '\\' || bytes.IndexByte(set, b) != -1 { buf.WriteByte('\\') } buf.WriteByte(b) } return buf.String() } ================================================ 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" "github.com/sagernet/sing/common" 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 := common.ContextWithCancelCause(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 interface{}) context.Context { return context.WithValue(ctx, requestInfoKey{}, ri) } // RequestInfoFromContext extracts the RequestInfo from ctx. func RequestInfoFromContext(ctx context.Context) interface{} { 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) interface{} { return ctx.Value(clientHandshakeInfoKey{}) } // NewClientHandshakeInfoContext creates a context with chi. func NewClientHandshakeInfoContext(ctx context.Context, chi interface{}) 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" ) const alpnProtoStrH2 = "h2" // AppendH2ToNextProtos appends h2 to next protos. func AppendH2ToNextProtos(ps []string) []string { for _, p := range ps { if p == 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.Split(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] common.ClearArray(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" "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{} 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() { networkProtocol = header.IPv4ProtocolNumber bind.Addr = tun.AddressFromAddr(w.inet4Address) } else { networkProtocol = header.IPv6ProtocolNumber bind.Addr = tun.AddressFromAddr(w.inet4Address) } 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 { 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 { 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 { common.ClearArray(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 { close(w.events) return w.device.Close() } 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" "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" "github.com/sagernet/sing/common" 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 } 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(bufs, offset) } } else { for _, packet := range bufs { if !w.writeStack(packet[offset:]) { if tun.PacketOffset > 0 { common.ClearArray(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 { close(w.endpoint.done) w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() } w.stack.Wait() return w.systemDevice.Close() } 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 ...interface{}) { e.options.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) }, Errorf: func(format string, args ...interface{}) { 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) ipcConf := e.ipcConf for _, peer := range e.peers { ipcConf += peer.GenerateIpcLines() } err = wgDevice.IpcSet(ipcConf) if err != nil { return E.Cause(err, "setup wireguard: \n", ipcConf) } 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.device != nil { e.device.Close() } if e.pauseCallback != nil { e.pause.UnregisterCallback(e.pauseCallback) } 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 { ipcLines := "\npublic_key=" + c.publicKeyHex if c.endpoint.IsValid() { ipcLines += "\nendpoint=" + c.endpoint.String() } if c.preSharedKeyHex != "" { ipcLines += "\npreshared_key=" + c.preSharedKeyHex } for _, allowedIP := range c.allowedIPs { ipcLines += "\nallowed_ip=" + allowedIP.String() } if c.keepalive > 0 { ipcLines += "\npersistent_keepalive_interval=" + F.ToString(c.keepalive) } return ipcLines } ================================================ 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 }