Repository: appshubcc/Bettbox Branch: main Commit: 715d8a0a4ae8 Files: 1770 Total size: 7.6 MB Directory structure: gitextract_09h7tnhn/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── release_template.md │ ├── release_template_pre.md │ └── workflows/ │ └── build.yaml ├── .gitignore ├── .metadata ├── LICENSE ├── Makefile ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── appshub/ │ │ │ │ └── bettbox/ │ │ │ │ ├── BettboxApplication.kt │ │ │ │ ├── FilesProvider.kt │ │ │ │ ├── GlobalState.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── TempActivity.kt │ │ │ │ ├── extensions/ │ │ │ │ │ └── Ext.kt │ │ │ │ ├── models/ │ │ │ │ │ ├── Package.kt │ │ │ │ │ ├── Process.kt │ │ │ │ │ └── Props.kt │ │ │ │ ├── modules/ │ │ │ │ │ └── SuspendModule.kt │ │ │ │ ├── plugins/ │ │ │ │ │ ├── AppPlugin.kt │ │ │ │ │ ├── ServicePlugin.kt │ │ │ │ │ ├── TilePlugin.kt │ │ │ │ │ └── VpnPlugin.kt │ │ │ │ ├── receivers/ │ │ │ │ │ ├── BootReceiver.kt │ │ │ │ │ └── PackageReplacedReceiver.kt │ │ │ │ └── services/ │ │ │ │ ├── BaseServiceInterface.kt │ │ │ │ ├── BettboxService.kt │ │ │ │ ├── BettboxTileService.kt │ │ │ │ └── BettboxVpnService.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ ├── ic_launcher_background_dark.xml │ │ │ │ ├── ic_launcher_background_light.xml │ │ │ │ ├── ic_launcher_foreground_dark.xml │ │ │ │ ├── ic_launcher_foreground_light.xml │ │ │ │ ├── ic_notification_dark.xml │ │ │ │ ├── ic_notification_light.xml │ │ │ │ ├── ic_tile.xml │ │ │ │ ├── launch_background.xml │ │ │ │ └── tv_banner.xml │ │ │ ├── drawable-night/ │ │ │ │ ├── ic_tile.xml │ │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ ├── ic_launcher_light.xml │ │ │ │ ├── ic_launcher_round.xml │ │ │ │ └── ic_launcher_round_light.xml │ │ │ ├── values/ │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── values-night/ │ │ │ │ └── styles.xml │ │ │ ├── values-night-v27/ │ │ │ │ └── styles.xml │ │ │ ├── values-ru/ │ │ │ │ └── strings.xml │ │ │ ├── values-v27/ │ │ │ │ └── styles.xml │ │ │ ├── values-zh-rCN/ │ │ │ │ └── strings.xml │ │ │ ├── values-zh-rTW/ │ │ │ │ └── strings.xml │ │ │ └── xml/ │ │ │ ├── file_paths.xml │ │ │ └── network_security_config.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle.kts │ ├── core/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── cpp/ │ │ │ ├── CMakeLists.txt │ │ │ ├── core.cpp │ │ │ ├── jni_helper.cpp │ │ │ └── jni_helper.h │ │ └── java/ │ │ └── com/ │ │ └── appshub/ │ │ └── bettbox/ │ │ └── core/ │ │ ├── Core.kt │ │ └── TunInterface.kt │ ├── gradle.properties │ └── settings.gradle.kts ├── arb/ │ ├── intl_en.arb │ ├── intl_ru.arb │ ├── intl_zh_CN.arb │ └── intl_zh_TC.arb ├── assets/ │ └── data/ │ ├── ASN.mmdb │ └── geoip.metadb ├── build.yaml ├── core/ │ ├── Clash.Meta/ │ │ ├── .github/ │ │ │ ├── ISSUE_TEMPLATE/ │ │ │ │ ├── bug_report.yml │ │ │ │ ├── config.yml │ │ │ │ └── feature_request.yml │ │ │ ├── genReleaseNote.sh │ │ │ ├── patch/ │ │ │ │ ├── go1.21.patch │ │ │ │ ├── go1.22.patch │ │ │ │ ├── go1.23.patch │ │ │ │ ├── go1.24.patch │ │ │ │ ├── go1.25.patch │ │ │ │ ├── go1.26.patch │ │ │ │ ├── issue77731.patch │ │ │ │ ├── issue77930.patch │ │ │ │ └── issue77975.patch │ │ │ ├── release/ │ │ │ │ ├── .fpm_systemd │ │ │ │ ├── config.yaml │ │ │ │ ├── mihomo.service │ │ │ │ └── mihomo@.service │ │ │ ├── release.sh │ │ │ ├── rename-cgo.sh │ │ │ ├── rename-go120.sh │ │ │ └── workflows/ │ │ │ ├── build.yml │ │ │ ├── test.yml │ │ │ └── trigger-cmfa-update.yml │ │ ├── .gitignore │ │ ├── .golangci.yaml │ │ ├── Dockerfile │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── adapter/ │ │ │ ├── adapter.go │ │ │ ├── inbound/ │ │ │ │ ├── addition.go │ │ │ │ ├── auth.go │ │ │ │ ├── http.go │ │ │ │ ├── https.go │ │ │ │ ├── ipfilter.go │ │ │ │ ├── listen.go │ │ │ │ ├── listen_notwindows.go │ │ │ │ ├── listen_windows.go │ │ │ │ ├── packet.go │ │ │ │ ├── socket.go │ │ │ │ └── util.go │ │ │ ├── outbound/ │ │ │ │ ├── anytls.go │ │ │ │ ├── base.go │ │ │ │ ├── direct.go │ │ │ │ ├── dns.go │ │ │ │ ├── ech.go │ │ │ │ ├── http.go │ │ │ │ ├── hysteria.go │ │ │ │ ├── hysteria2.go │ │ │ │ ├── masque.go │ │ │ │ ├── mieru.go │ │ │ │ ├── mieru_test.go │ │ │ │ ├── reality.go │ │ │ │ ├── reject.go │ │ │ │ ├── shadowsocks.go │ │ │ │ ├── shadowsocksr.go │ │ │ │ ├── singmux.go │ │ │ │ ├── snell.go │ │ │ │ ├── socks5.go │ │ │ │ ├── ssh.go │ │ │ │ ├── sudoku.go │ │ │ │ ├── trojan.go │ │ │ │ ├── trusttunnel.go │ │ │ │ ├── tuic.go │ │ │ │ ├── util.go │ │ │ │ ├── vless.go │ │ │ │ ├── vmess.go │ │ │ │ └── wireguard.go │ │ │ ├── outboundgroup/ │ │ │ │ ├── fallback.go │ │ │ │ ├── groupbase.go │ │ │ │ ├── loadbalance.go │ │ │ │ ├── parser.go │ │ │ │ ├── selector.go │ │ │ │ ├── urltest.go │ │ │ │ └── util.go │ │ │ ├── parser.go │ │ │ ├── patch.go │ │ │ └── provider/ │ │ │ ├── healthcheck.go │ │ │ ├── override.go │ │ │ ├── parser.go │ │ │ ├── patch.go │ │ │ ├── provider.go │ │ │ └── subscription_info.go │ │ ├── android_tz.go │ │ ├── check_amd64.sh │ │ ├── common/ │ │ │ ├── arc/ │ │ │ │ ├── arc.go │ │ │ │ ├── arc_test.go │ │ │ │ └── entry.go │ │ │ ├── atomic/ │ │ │ │ ├── enum.go │ │ │ │ ├── type.go │ │ │ │ ├── value.go │ │ │ │ └── value_test.go │ │ │ ├── batch/ │ │ │ │ ├── batch.go │ │ │ │ └── batch_test.go │ │ │ ├── buf/ │ │ │ │ └── sing.go │ │ │ ├── callback/ │ │ │ │ ├── callback.go │ │ │ │ └── close_callback.go │ │ │ ├── cmd/ │ │ │ │ ├── cmd.go │ │ │ │ ├── cmd_other.go │ │ │ │ ├── cmd_test.go │ │ │ │ └── cmd_windows.go │ │ │ ├── contextutils/ │ │ │ │ ├── afterfunc_compact.go │ │ │ │ ├── afterfunc_go120.go │ │ │ │ ├── afterfunc_go121.go │ │ │ │ ├── afterfunc_test.go │ │ │ │ ├── withoutcancel_compact.go │ │ │ │ ├── withoutcancel_go120.go │ │ │ │ └── withoutcancel_go121.go │ │ │ ├── convert/ │ │ │ │ ├── base64.go │ │ │ │ ├── converter.go │ │ │ │ ├── converter_test.go │ │ │ │ ├── util.go │ │ │ │ └── v.go │ │ │ ├── deque/ │ │ │ │ └── deque.go │ │ │ ├── httputils/ │ │ │ │ ├── addr.go │ │ │ │ ├── force_close.go │ │ │ │ └── h2_transport_close.go │ │ │ ├── lru/ │ │ │ │ ├── lrucache.go │ │ │ │ └── lrucache_test.go │ │ │ ├── maphash/ │ │ │ │ ├── common.go │ │ │ │ ├── comparable_go120.go │ │ │ │ ├── comparable_go124.go │ │ │ │ └── maphash_test.go │ │ │ ├── murmur3/ │ │ │ │ ├── murmur.go │ │ │ │ └── murmur32.go │ │ │ ├── net/ │ │ │ │ ├── addr.go │ │ │ │ ├── bind.go │ │ │ │ ├── bufconn.go │ │ │ │ ├── bufconn_unsafe.go │ │ │ │ ├── cached.go │ │ │ │ ├── context.go │ │ │ │ ├── context_test.go │ │ │ │ ├── deadline/ │ │ │ │ │ ├── conn.go │ │ │ │ │ ├── packet.go │ │ │ │ │ ├── packet_enhance.go │ │ │ │ │ ├── packet_sing.go │ │ │ │ │ ├── pipe.go │ │ │ │ │ └── pipe_sing.go │ │ │ │ ├── earlyconn.go │ │ │ │ ├── io.go │ │ │ │ ├── listener.go │ │ │ │ ├── packet/ │ │ │ │ │ ├── packet.go │ │ │ │ │ ├── packet_posix.go │ │ │ │ │ ├── packet_sing.go │ │ │ │ │ ├── packet_windows.go │ │ │ │ │ ├── ref.go │ │ │ │ │ ├── ref_sing.go │ │ │ │ │ ├── thread.go │ │ │ │ │ └── thread_sing.go │ │ │ │ ├── packet.go │ │ │ │ ├── refconn.go │ │ │ │ ├── relay.go │ │ │ │ ├── sing.go │ │ │ │ ├── tcpip.go │ │ │ │ └── websocket.go │ │ │ ├── observable/ │ │ │ │ ├── iterable.go │ │ │ │ ├── observable.go │ │ │ │ ├── observable_test.go │ │ │ │ └── subscriber.go │ │ │ ├── once/ │ │ │ │ ├── once_go120.go │ │ │ │ ├── once_go122.go │ │ │ │ └── oncefunc.go │ │ │ ├── orderedmap/ │ │ │ │ ├── doc.go │ │ │ │ ├── json.go │ │ │ │ ├── json_fuzz_test.go │ │ │ │ ├── json_test.go │ │ │ │ ├── orderedmap.go │ │ │ │ ├── orderedmap_test.go │ │ │ │ ├── utils_test.go │ │ │ │ ├── yaml.go │ │ │ │ ├── yaml_fuzz_test.go │ │ │ │ └── yaml_test.go │ │ │ ├── picker/ │ │ │ │ ├── picker.go │ │ │ │ └── picker_test.go │ │ │ ├── pool/ │ │ │ │ ├── alloc.go │ │ │ │ ├── alloc_test.go │ │ │ │ ├── buffer.go │ │ │ │ ├── buffer_low_memory.go │ │ │ │ ├── buffer_standard.go │ │ │ │ ├── pool.go │ │ │ │ └── sing.go │ │ │ ├── queue/ │ │ │ │ ├── queue.go │ │ │ │ └── queue_test.go │ │ │ ├── singledo/ │ │ │ │ ├── singledo.go │ │ │ │ └── singledo_test.go │ │ │ ├── singleflight/ │ │ │ │ └── singleflight.go │ │ │ ├── sockopt/ │ │ │ │ ├── reuse_common.go │ │ │ │ ├── reuse_other.go │ │ │ │ ├── reuse_unix.go │ │ │ │ └── reuse_windows.go │ │ │ ├── structure/ │ │ │ │ ├── structure.go │ │ │ │ └── structure_test.go │ │ │ ├── utils/ │ │ │ │ ├── callback.go │ │ │ │ ├── global_id.go │ │ │ │ ├── hash.go │ │ │ │ ├── manipulation.go │ │ │ │ ├── must.go │ │ │ │ ├── range.go │ │ │ │ ├── ranges.go │ │ │ │ ├── ranges_test.go │ │ │ │ ├── slice.go │ │ │ │ ├── string_unsafe.go │ │ │ │ ├── strings.go │ │ │ │ ├── uuid.go │ │ │ │ └── uuid_test.go │ │ │ ├── xsync/ │ │ │ │ ├── map.go │ │ │ │ ├── map_extra.go │ │ │ │ ├── map_extra_test.go │ │ │ │ └── map_test.go │ │ │ └── yaml/ │ │ │ └── yaml.go │ │ ├── component/ │ │ │ ├── auth/ │ │ │ │ └── auth.go │ │ │ ├── ca/ │ │ │ │ ├── auth.go │ │ │ │ ├── ca-certificates.crt │ │ │ │ ├── config.go │ │ │ │ ├── fingerprint.go │ │ │ │ ├── fingerprint_test.go │ │ │ │ ├── fix_windows.go │ │ │ │ └── keypair.go │ │ │ ├── cidr/ │ │ │ │ ├── ipcidr_set.go │ │ │ │ ├── ipcidr_set_bin.go │ │ │ │ └── ipcidr_set_test.go │ │ │ ├── dhcp/ │ │ │ │ ├── conn.go │ │ │ │ └── dhcp.go │ │ │ ├── dialer/ │ │ │ │ ├── bind.go │ │ │ │ ├── bind_darwin.go │ │ │ │ ├── bind_linux.go │ │ │ │ ├── bind_others.go │ │ │ │ ├── bind_windows.go │ │ │ │ ├── control.go │ │ │ │ ├── dialer.go │ │ │ │ ├── error.go │ │ │ │ ├── mark_linux.go │ │ │ │ ├── mark_nonlinux.go │ │ │ │ ├── options.go │ │ │ │ ├── reuse.go │ │ │ │ ├── socket_hook.go │ │ │ │ ├── tfo.go │ │ │ │ └── tfo_windows.go │ │ │ ├── ech/ │ │ │ │ ├── ech.go │ │ │ │ ├── echparser/ │ │ │ │ │ └── echparser.go │ │ │ │ ├── key.go │ │ │ │ └── key_test.go │ │ │ ├── fakeip/ │ │ │ │ ├── cachefile.go │ │ │ │ ├── memory.go │ │ │ │ ├── pool.go │ │ │ │ ├── pool_test.go │ │ │ │ ├── skipper.go │ │ │ │ └── skipper_test.go │ │ │ ├── generator/ │ │ │ │ ├── cmd.go │ │ │ │ └── x25519.go │ │ │ ├── geodata/ │ │ │ │ ├── attr.go │ │ │ │ ├── geodata.go │ │ │ │ ├── geodataproto.go │ │ │ │ ├── init.go │ │ │ │ ├── memconservative/ │ │ │ │ │ ├── cache.go │ │ │ │ │ ├── decode.go │ │ │ │ │ └── memc.go │ │ │ │ ├── package_info.go │ │ │ │ ├── router/ │ │ │ │ │ ├── condition.go │ │ │ │ │ ├── config.pb.go │ │ │ │ │ └── config.proto │ │ │ │ ├── standard/ │ │ │ │ │ └── standard.go │ │ │ │ ├── strmatcher/ │ │ │ │ │ ├── ac_automaton_matcher.go │ │ │ │ │ ├── matchers.go │ │ │ │ │ ├── mph_matcher.go │ │ │ │ │ ├── package_info.go │ │ │ │ │ └── strmatcher.go │ │ │ │ └── utils.go │ │ │ ├── http/ │ │ │ │ └── http.go │ │ │ ├── iface/ │ │ │ │ └── iface.go │ │ │ ├── keepalive/ │ │ │ │ ├── tcp_keepalive.go │ │ │ │ ├── tcp_keepalive_go122.go │ │ │ │ ├── tcp_keepalive_go123.go │ │ │ │ ├── tcp_keepalive_go123_unix.go │ │ │ │ └── tcp_keepalive_go123_windows.go │ │ │ ├── loopback/ │ │ │ │ └── detector.go │ │ │ ├── memory/ │ │ │ │ ├── memory.go │ │ │ │ ├── memory_darwin.go │ │ │ │ ├── memory_darwin_amd64.s │ │ │ │ ├── memory_darwin_arm64.s │ │ │ │ ├── memory_falllback.go │ │ │ │ ├── memory_freebsd.go │ │ │ │ ├── memory_freebsd_386.go │ │ │ │ ├── memory_freebsd_amd64.go │ │ │ │ ├── memory_freebsd_arm.go │ │ │ │ ├── memory_freebsd_arm64.go │ │ │ │ ├── memory_linux.go │ │ │ │ ├── memory_openbsd.go │ │ │ │ ├── memory_openbsd_386.go │ │ │ │ ├── memory_openbsd_amd64.go │ │ │ │ ├── memory_openbsd_arm.go │ │ │ │ ├── memory_openbsd_arm64.go │ │ │ │ ├── memory_openbsd_riscv64.go │ │ │ │ ├── memory_test.go │ │ │ │ └── memory_windows.go │ │ │ ├── mmdb/ │ │ │ │ ├── mmdb.go │ │ │ │ └── reader.go │ │ │ ├── mptcp/ │ │ │ │ ├── mptcp_go120.go │ │ │ │ └── mptcp_go121.go │ │ │ ├── nat/ │ │ │ │ ├── proxy.go │ │ │ │ └── table.go │ │ │ ├── pool/ │ │ │ │ ├── pool.go │ │ │ │ └── pool_test.go │ │ │ ├── power/ │ │ │ │ ├── event.go │ │ │ │ ├── event_other.go │ │ │ │ └── event_windows.go │ │ │ ├── process/ │ │ │ │ ├── find_process_mode.go │ │ │ │ ├── process.go │ │ │ │ ├── process_darwin.go │ │ │ │ ├── process_freebsd_amd64.go │ │ │ │ ├── process_linux.go │ │ │ │ ├── process_other.go │ │ │ │ └── process_windows.go │ │ │ ├── profile/ │ │ │ │ ├── cachefile/ │ │ │ │ │ ├── cache.go │ │ │ │ │ ├── etag.go │ │ │ │ │ ├── fakeip.go │ │ │ │ │ ├── storage.go │ │ │ │ │ └── subscriptioninfo.go │ │ │ │ └── profile.go │ │ │ ├── proxydialer/ │ │ │ │ ├── byname.go │ │ │ │ ├── proxydialer.go │ │ │ │ ├── sing.go │ │ │ │ ├── slowdown.go │ │ │ │ └── slowdown_sing.go │ │ │ ├── resolver/ │ │ │ │ ├── enhancer.go │ │ │ │ ├── host.go │ │ │ │ ├── hosts/ │ │ │ │ │ ├── hosts.go │ │ │ │ │ └── hosts_windows.go │ │ │ │ ├── ip4p.go │ │ │ │ ├── relay.go │ │ │ │ ├── resolver.go │ │ │ │ ├── service.go │ │ │ │ └── system.go │ │ │ ├── resource/ │ │ │ │ ├── fetcher.go │ │ │ │ └── vehicle.go │ │ │ ├── slowdown/ │ │ │ │ ├── backoff.go │ │ │ │ └── slowdown.go │ │ │ ├── sniffer/ │ │ │ │ ├── base_sniffer.go │ │ │ │ ├── dispatcher.go │ │ │ │ ├── http_sniffer.go │ │ │ │ ├── quic_sniffer.go │ │ │ │ ├── sniff_test.go │ │ │ │ └── tls_sniffer.go │ │ │ ├── tls/ │ │ │ │ ├── httpserver.go │ │ │ │ ├── reality.go │ │ │ │ └── utls.go │ │ │ ├── trie/ │ │ │ │ ├── domain.go │ │ │ │ ├── domain_set.go │ │ │ │ ├── domain_set_bin.go │ │ │ │ ├── domain_set_test.go │ │ │ │ ├── domain_test.go │ │ │ │ ├── ipcidr_node.go │ │ │ │ ├── ipcidr_trie.go │ │ │ │ ├── node.go │ │ │ │ └── trie_test.go │ │ │ ├── updater/ │ │ │ │ ├── patch.go │ │ │ │ ├── update_core.go │ │ │ │ ├── update_core_test.go │ │ │ │ ├── update_geo.go │ │ │ │ ├── update_ui.go │ │ │ │ └── utils.go │ │ │ └── wildcard/ │ │ │ ├── wildcard.go │ │ │ └── wildcard_test.go │ │ ├── config/ │ │ │ ├── config.go │ │ │ ├── initial.go │ │ │ ├── utils.go │ │ │ └── utils_test.go │ │ ├── constant/ │ │ │ ├── adapters.go │ │ │ ├── context.go │ │ │ ├── dns.go │ │ │ ├── features/ │ │ │ │ ├── android.go │ │ │ │ ├── android_stub.go │ │ │ │ ├── goflags.go │ │ │ │ ├── low_memory.go │ │ │ │ ├── low_memory_stub.go │ │ │ │ ├── no_fake_tcp.go │ │ │ │ ├── no_fake_tcp_stub.go │ │ │ │ ├── tags.go │ │ │ │ ├── version.go │ │ │ │ ├── version_windows.go │ │ │ │ ├── with_gvisor.go │ │ │ │ └── with_gvisor_stub.go │ │ │ ├── listener.go │ │ │ ├── matcher.go │ │ │ ├── metadata.go │ │ │ ├── path.go │ │ │ ├── path_test.go │ │ │ ├── provider/ │ │ │ │ └── interface.go │ │ │ ├── rule.go │ │ │ ├── sniffer/ │ │ │ │ └── sniffer.go │ │ │ ├── tun.go │ │ │ ├── tunnel.go │ │ │ └── version.go │ │ ├── context/ │ │ │ ├── conn.go │ │ │ ├── dns.go │ │ │ └── packetconn.go │ │ ├── dns/ │ │ │ ├── client.go │ │ │ ├── dhcp.go │ │ │ ├── dialer.go │ │ │ ├── doh.go │ │ │ ├── doq.go │ │ │ ├── dot.go │ │ │ ├── edns0_subnet.go │ │ │ ├── enhancer.go │ │ │ ├── middleware.go │ │ │ ├── patch_android.go │ │ │ ├── policy.go │ │ │ ├── rcode.go │ │ │ ├── resolver.go │ │ │ ├── server.go │ │ │ ├── service.go │ │ │ ├── system.go │ │ │ ├── system_common.go │ │ │ ├── system_posix.go │ │ │ ├── system_windows.go │ │ │ └── util.go │ │ ├── docker/ │ │ │ └── file-name.sh │ │ ├── docs/ │ │ │ └── config.yaml │ │ ├── flake.nix │ │ ├── go.mod │ │ ├── go.sum │ │ ├── hub/ │ │ │ ├── executor/ │ │ │ │ ├── concurrent_load_limit.go │ │ │ │ ├── concurrent_load_single.go │ │ │ │ ├── concurrent_load_unlimit.go │ │ │ │ ├── executor.go │ │ │ │ └── patch.go │ │ │ ├── hub.go │ │ │ └── route/ │ │ │ ├── cache.go │ │ │ ├── common.go │ │ │ ├── configs.go │ │ │ ├── connections.go │ │ │ ├── ctxkeys.go │ │ │ ├── dns.go │ │ │ ├── doh.go │ │ │ ├── errors.go │ │ │ ├── external.go │ │ │ ├── groups.go │ │ │ ├── patch_android.go │ │ │ ├── provider.go │ │ │ ├── proxies.go │ │ │ ├── restart.go │ │ │ ├── rules.go │ │ │ ├── server.go │ │ │ ├── storage.go │ │ │ └── upgrade.go │ │ ├── listener/ │ │ │ ├── anytls/ │ │ │ │ └── server.go │ │ │ ├── auth/ │ │ │ │ └── auth.go │ │ │ ├── config/ │ │ │ │ ├── anytls.go │ │ │ │ ├── auth.go │ │ │ │ ├── hysteria2.go │ │ │ │ ├── kcptun.go │ │ │ │ ├── shadowsocks.go │ │ │ │ ├── shadowtls.go │ │ │ │ ├── sudoku.go │ │ │ │ ├── trojan.go │ │ │ │ ├── trusttunnel.go │ │ │ │ ├── tuic.go │ │ │ │ ├── tun.go │ │ │ │ ├── tunnel.go │ │ │ │ ├── vless.go │ │ │ │ └── vmess.go │ │ │ ├── http/ │ │ │ │ ├── client.go │ │ │ │ ├── hack.go │ │ │ │ ├── patch_android.go │ │ │ │ ├── proxy.go │ │ │ │ ├── server.go │ │ │ │ ├── upgrade.go │ │ │ │ └── utils.go │ │ │ ├── inbound/ │ │ │ │ ├── anytls.go │ │ │ │ ├── anytls_test.go │ │ │ │ ├── auth.go │ │ │ │ ├── base.go │ │ │ │ ├── common_test.go │ │ │ │ ├── http.go │ │ │ │ ├── hysteria2.go │ │ │ │ ├── hysteria2_test.go │ │ │ │ ├── kcptun.go │ │ │ │ ├── mieru.go │ │ │ │ ├── mieru_test.go │ │ │ │ ├── mixed.go │ │ │ │ ├── mux.go │ │ │ │ ├── mux_test.go │ │ │ │ ├── reality.go │ │ │ │ ├── redir.go │ │ │ │ ├── shadowsocks.go │ │ │ │ ├── shadowsocks_test.go │ │ │ │ ├── shadowtls.go │ │ │ │ ├── socks.go │ │ │ │ ├── sudoku.go │ │ │ │ ├── sudoku_test.go │ │ │ │ ├── tproxy.go │ │ │ │ ├── trojan.go │ │ │ │ ├── trojan_test.go │ │ │ │ ├── trusttunnel.go │ │ │ │ ├── trusttunnel_test.go │ │ │ │ ├── tuic.go │ │ │ │ ├── tuic_test.go │ │ │ │ ├── tun.go │ │ │ │ ├── tunnel.go │ │ │ │ ├── vless.go │ │ │ │ ├── vless_test.go │ │ │ │ ├── vmess.go │ │ │ │ └── vmess_test.go │ │ │ ├── inner/ │ │ │ │ └── tcp.go │ │ │ ├── listener.go │ │ │ ├── mieru/ │ │ │ │ └── server.go │ │ │ ├── mixed/ │ │ │ │ └── mixed.go │ │ │ ├── parse.go │ │ │ ├── patch.go │ │ │ ├── reality/ │ │ │ │ └── reality.go │ │ │ ├── redir/ │ │ │ │ ├── tcp.go │ │ │ │ ├── tcp_darwin.go │ │ │ │ ├── tcp_freebsd.go │ │ │ │ ├── tcp_linux.go │ │ │ │ ├── tcp_linux_386.go │ │ │ │ ├── tcp_linux_other.go │ │ │ │ └── tcp_other.go │ │ │ ├── shadowsocks/ │ │ │ │ ├── tcp.go │ │ │ │ ├── udp.go │ │ │ │ ├── utils.go │ │ │ │ └── utils_test.go │ │ │ ├── sing/ │ │ │ │ ├── context.go │ │ │ │ ├── dialer.go │ │ │ │ ├── sing.go │ │ │ │ └── util.go │ │ │ ├── sing_hysteria2/ │ │ │ │ └── server.go │ │ │ ├── sing_shadowsocks/ │ │ │ │ └── server.go │ │ │ ├── sing_tun/ │ │ │ │ ├── dns.go │ │ │ │ ├── iface.go │ │ │ │ ├── prepare.go │ │ │ │ ├── redirect_linux.go │ │ │ │ ├── redirect_stub.go │ │ │ │ ├── server.go │ │ │ │ ├── server_notwindows.go │ │ │ │ ├── server_windows.go │ │ │ │ ├── tun_name_darwin.go │ │ │ │ ├── tun_name_linux.go │ │ │ │ └── tun_name_other.go │ │ │ ├── sing_vless/ │ │ │ │ ├── server.go │ │ │ │ └── service.go │ │ │ ├── sing_vmess/ │ │ │ │ ├── server.go │ │ │ │ └── server_test.go │ │ │ ├── socks/ │ │ │ │ ├── tcp.go │ │ │ │ ├── udp.go │ │ │ │ └── utils.go │ │ │ ├── sudoku/ │ │ │ │ └── server.go │ │ │ ├── tproxy/ │ │ │ │ ├── packet.go │ │ │ │ ├── setsockopt_linux.go │ │ │ │ ├── setsockopt_other.go │ │ │ │ ├── tproxy.go │ │ │ │ ├── tproxy_iptables.go │ │ │ │ ├── udp.go │ │ │ │ ├── udp_linux.go │ │ │ │ └── udp_other.go │ │ │ ├── trojan/ │ │ │ │ ├── packet.go │ │ │ │ └── server.go │ │ │ ├── trusttunnel/ │ │ │ │ └── server.go │ │ │ ├── tuic/ │ │ │ │ └── server.go │ │ │ └── tunnel/ │ │ │ ├── packet.go │ │ │ ├── tcp.go │ │ │ └── udp.go │ │ ├── log/ │ │ │ ├── level.go │ │ │ ├── log.go │ │ │ └── sing.go │ │ ├── main.go │ │ ├── ntp/ │ │ │ ├── ntp/ │ │ │ │ ├── service.go │ │ │ │ ├── time_stub.go │ │ │ │ ├── time_unix.go │ │ │ │ └── time_windows.go │ │ │ └── time.go │ │ ├── rules/ │ │ │ ├── common/ │ │ │ │ ├── base.go │ │ │ │ ├── domain.go │ │ │ │ ├── domain_keyword.go │ │ │ │ ├── domain_regex.go │ │ │ │ ├── domain_suffix.go │ │ │ │ ├── domain_wildcard.go │ │ │ │ ├── dscp.go │ │ │ │ ├── final.go │ │ │ │ ├── geoip.go │ │ │ │ ├── geosite.go │ │ │ │ ├── in_name.go │ │ │ │ ├── in_type.go │ │ │ │ ├── in_user.go │ │ │ │ ├── ipasn.go │ │ │ │ ├── ipcidr.go │ │ │ │ ├── ipsuffix.go │ │ │ │ ├── network_type.go │ │ │ │ ├── port.go │ │ │ │ ├── process.go │ │ │ │ └── uid.go │ │ │ ├── logic/ │ │ │ │ └── logic.go │ │ │ ├── logic_test/ │ │ │ │ └── logic_test.go │ │ │ ├── parser.go │ │ │ ├── provider/ │ │ │ │ ├── classical_strategy.go │ │ │ │ ├── domain_strategy.go │ │ │ │ ├── ipcidr_strategy.go │ │ │ │ ├── mrs_converter.go │ │ │ │ ├── mrs_reader.go │ │ │ │ ├── parse.go │ │ │ │ ├── patch_android.go │ │ │ │ ├── provider.go │ │ │ │ └── rule_set.go │ │ │ └── wrapper/ │ │ │ └── wrapper.go │ │ ├── test/ │ │ │ ├── .golangci.yaml │ │ │ ├── Makefile │ │ │ ├── clash_test.go │ │ │ ├── config/ │ │ │ │ ├── example.org-key.pem │ │ │ │ ├── example.org.pem │ │ │ │ ├── hysteria.json │ │ │ │ ├── snell-http.conf │ │ │ │ ├── snell-tls.conf │ │ │ │ ├── snell.conf │ │ │ │ ├── trojan-grpc.json │ │ │ │ ├── trojan-ws.json │ │ │ │ ├── trojan-xtls.json │ │ │ │ ├── trojan.json │ │ │ │ ├── vless-tls.json │ │ │ │ ├── vless-ws.json │ │ │ │ ├── vless-xtls.json │ │ │ │ ├── vmess-grpc.json │ │ │ │ ├── vmess-http.json │ │ │ │ ├── vmess-http2.json │ │ │ │ ├── vmess-tls.json │ │ │ │ ├── vmess-ws-0rtt.json │ │ │ │ ├── vmess-ws-tls.json │ │ │ │ ├── vmess-ws.json │ │ │ │ ├── vmess.json │ │ │ │ └── xray-shadowsocks.json │ │ │ ├── dns_test.go │ │ │ ├── docker_test.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── hysteria_test.go │ │ │ ├── snell_test.go │ │ │ ├── ss_test.go │ │ │ ├── trojan_test.go │ │ │ ├── util.go │ │ │ ├── util_darwin_test.go │ │ │ ├── util_other_test.go │ │ │ ├── vless_test.go │ │ │ └── vmess_test.go │ │ ├── transport/ │ │ │ ├── anytls/ │ │ │ │ ├── client.go │ │ │ │ ├── padding/ │ │ │ │ │ └── padding.go │ │ │ │ ├── pipe/ │ │ │ │ │ ├── deadline.go │ │ │ │ │ └── io_pipe.go │ │ │ │ ├── session/ │ │ │ │ │ ├── client.go │ │ │ │ │ ├── frame.go │ │ │ │ │ ├── session.go │ │ │ │ │ └── stream.go │ │ │ │ ├── skiplist/ │ │ │ │ │ ├── contianer.go │ │ │ │ │ ├── skiplist.go │ │ │ │ │ ├── skiplist_newnode.go │ │ │ │ │ └── types.go │ │ │ │ └── util/ │ │ │ │ ├── deadline.go │ │ │ │ ├── routine.go │ │ │ │ ├── string_map.go │ │ │ │ └── type.go │ │ │ ├── gost-plugin/ │ │ │ │ └── websocket.go │ │ │ ├── gun/ │ │ │ │ ├── gun.go │ │ │ │ ├── server.go │ │ │ │ └── utils.go │ │ │ ├── hysteria/ │ │ │ │ ├── congestion/ │ │ │ │ │ ├── brutal.go │ │ │ │ │ └── pacer.go │ │ │ │ ├── conns/ │ │ │ │ │ ├── faketcp/ │ │ │ │ │ │ ├── LICENSE │ │ │ │ │ │ ├── obfs.go │ │ │ │ │ │ ├── tcp_linux.go │ │ │ │ │ │ └── tcp_stub.go │ │ │ │ │ ├── udp/ │ │ │ │ │ │ ├── hop.go │ │ │ │ │ │ └── obfs.go │ │ │ │ │ └── wechat/ │ │ │ │ │ └── obfs.go │ │ │ │ ├── core/ │ │ │ │ │ ├── client.go │ │ │ │ │ ├── frag.go │ │ │ │ │ ├── frag_test.go │ │ │ │ │ ├── protocol.go │ │ │ │ │ └── stream.go │ │ │ │ ├── obfs/ │ │ │ │ │ ├── dummy.go │ │ │ │ │ ├── obfs.go │ │ │ │ │ ├── xplus.go │ │ │ │ │ └── xplus_test.go │ │ │ │ ├── pmtud_fix/ │ │ │ │ │ ├── avail.go │ │ │ │ │ └── unavail.go │ │ │ │ ├── transport/ │ │ │ │ │ └── client.go │ │ │ │ └── utils/ │ │ │ │ └── misc.go │ │ │ ├── kcptun/ │ │ │ │ ├── client.go │ │ │ │ ├── common.go │ │ │ │ ├── comp.go │ │ │ │ ├── doc.go │ │ │ │ └── server.go │ │ │ ├── masque/ │ │ │ │ ├── client_h2.go │ │ │ │ └── masque.go │ │ │ ├── restls/ │ │ │ │ └── restls.go │ │ │ ├── shadowsocks/ │ │ │ │ ├── core/ │ │ │ │ │ └── cipher.go │ │ │ │ ├── shadowaead/ │ │ │ │ │ ├── cipher.go │ │ │ │ │ ├── packet.go │ │ │ │ │ └── stream.go │ │ │ │ └── shadowstream/ │ │ │ │ ├── chacha20.go │ │ │ │ ├── cipher.go │ │ │ │ ├── packet.go │ │ │ │ └── stream.go │ │ │ ├── shadowtls/ │ │ │ │ └── shadowtls.go │ │ │ ├── simple-obfs/ │ │ │ │ ├── http.go │ │ │ │ └── tls.go │ │ │ ├── sing-shadowtls/ │ │ │ │ └── shadowtls.go │ │ │ ├── snell/ │ │ │ │ ├── cipher.go │ │ │ │ ├── pool.go │ │ │ │ └── snell.go │ │ │ ├── socks4/ │ │ │ │ └── socks4.go │ │ │ ├── socks5/ │ │ │ │ └── socks5.go │ │ │ ├── ssr/ │ │ │ │ ├── obfs/ │ │ │ │ │ ├── base.go │ │ │ │ │ ├── http_post.go │ │ │ │ │ ├── http_simple.go │ │ │ │ │ ├── obfs.go │ │ │ │ │ ├── plain.go │ │ │ │ │ ├── random_head.go │ │ │ │ │ └── tls1.2_ticket_auth.go │ │ │ │ ├── protocol/ │ │ │ │ │ ├── auth_aes128_md5.go │ │ │ │ │ ├── auth_aes128_sha1.go │ │ │ │ │ ├── auth_chain_a.go │ │ │ │ │ ├── auth_chain_b.go │ │ │ │ │ ├── auth_sha1_v4.go │ │ │ │ │ ├── base.go │ │ │ │ │ ├── origin.go │ │ │ │ │ ├── packet.go │ │ │ │ │ ├── protocol.go │ │ │ │ │ └── stream.go │ │ │ │ └── tools/ │ │ │ │ ├── bufPool.go │ │ │ │ ├── crypto.go │ │ │ │ └── random.go │ │ │ ├── sudoku/ │ │ │ │ ├── address.go │ │ │ │ ├── config.go │ │ │ │ ├── crypto/ │ │ │ │ │ ├── aead.go │ │ │ │ │ ├── ed25519.go │ │ │ │ │ ├── record_conn.go │ │ │ │ │ └── record_conn_test.go │ │ │ │ ├── directional_hint_test.go │ │ │ │ ├── early_handshake.go │ │ │ │ ├── features_test.go │ │ │ │ ├── handshake.go │ │ │ │ ├── handshake_kip.go │ │ │ │ ├── handshake_test.go │ │ │ │ ├── httpmask_tunnel.go │ │ │ │ ├── httpmask_tunnel_test.go │ │ │ │ ├── init.go │ │ │ │ ├── init_test.go │ │ │ │ ├── kip.go │ │ │ │ ├── kip_test.go │ │ │ │ ├── multiplex/ │ │ │ │ │ ├── session.go │ │ │ │ │ └── write_chunks.go │ │ │ │ ├── multiplex.go │ │ │ │ ├── multiplex_test.go │ │ │ │ ├── obfs/ │ │ │ │ │ ├── httpmask/ │ │ │ │ │ │ ├── auth.go │ │ │ │ │ │ ├── early_handshake.go │ │ │ │ │ │ ├── halfpipe.go │ │ │ │ │ │ ├── masker.go │ │ │ │ │ │ ├── pathroot.go │ │ │ │ │ │ ├── tunnel.go │ │ │ │ │ │ ├── tunnel_ws.go │ │ │ │ │ │ ├── tunnel_ws_server.go │ │ │ │ │ │ └── ws_stream_conn.go │ │ │ │ │ └── sudoku/ │ │ │ │ │ ├── ascii_mode.go │ │ │ │ │ ├── ascii_mode_test.go │ │ │ │ │ ├── conn.go │ │ │ │ │ ├── encode.go │ │ │ │ │ ├── grid.go │ │ │ │ │ ├── layout.go │ │ │ │ │ ├── packed.go │ │ │ │ │ ├── packed_prefix_test.go │ │ │ │ │ ├── padding_prob.go │ │ │ │ │ ├── pending.go │ │ │ │ │ ├── rand.go │ │ │ │ │ ├── table.go │ │ │ │ │ └── table_set.go │ │ │ │ ├── replay.go │ │ │ │ ├── session_keys.go │ │ │ │ ├── table_probe.go │ │ │ │ ├── tables.go │ │ │ │ ├── tables_directional_test.go │ │ │ │ ├── uot.go │ │ │ │ └── write_chunks.go │ │ │ ├── trojan/ │ │ │ │ └── trojan.go │ │ │ ├── trusttunnel/ │ │ │ │ ├── client.go │ │ │ │ ├── doc.go │ │ │ │ ├── icmp.go │ │ │ │ ├── packet.go │ │ │ │ ├── protocol.go │ │ │ │ ├── quic.go │ │ │ │ └── service.go │ │ │ ├── tuic/ │ │ │ │ ├── common/ │ │ │ │ │ ├── congestion.go │ │ │ │ │ └── dial.go │ │ │ │ ├── congestion/ │ │ │ │ │ ├── bandwidth.go │ │ │ │ │ ├── bandwidth_sampler.go │ │ │ │ │ ├── bbr_sender.go │ │ │ │ │ ├── cubic.go │ │ │ │ │ ├── cubic_sender.go │ │ │ │ │ ├── hybrid_slow_start.go │ │ │ │ │ ├── minmax.go │ │ │ │ │ ├── minmax_go120.go │ │ │ │ │ ├── minmax_go121.go │ │ │ │ │ ├── pacer.go │ │ │ │ │ └── windowed_filter.go │ │ │ │ ├── congestion_v2/ │ │ │ │ │ ├── bandwidth.go │ │ │ │ │ ├── bandwidth_sampler.go │ │ │ │ │ ├── bbr_sender.go │ │ │ │ │ ├── minmax_go120.go │ │ │ │ │ ├── minmax_go121.go │ │ │ │ │ ├── pacer.go │ │ │ │ │ ├── packet_number_indexed_queue.go │ │ │ │ │ ├── ringbuffer.go │ │ │ │ │ └── windowed_filter.go │ │ │ │ ├── pool_client.go │ │ │ │ ├── server.go │ │ │ │ ├── tuic.go │ │ │ │ ├── types/ │ │ │ │ │ ├── stream.go │ │ │ │ │ └── type.go │ │ │ │ ├── v4/ │ │ │ │ │ ├── client.go │ │ │ │ │ ├── packet.go │ │ │ │ │ ├── protocol.go │ │ │ │ │ └── server.go │ │ │ │ └── v5/ │ │ │ │ ├── client.go │ │ │ │ ├── frag.go │ │ │ │ ├── packet.go │ │ │ │ ├── protocol.go │ │ │ │ └── server.go │ │ │ ├── v2ray-plugin/ │ │ │ │ ├── mux.go │ │ │ │ └── websocket.go │ │ │ ├── vless/ │ │ │ │ ├── addons.go │ │ │ │ ├── addons_test.go │ │ │ │ ├── config.pb.go │ │ │ │ ├── config.proto │ │ │ │ ├── conn.go │ │ │ │ ├── encryption/ │ │ │ │ │ ├── client.go │ │ │ │ │ ├── client_test.go │ │ │ │ │ ├── common.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── factory.go │ │ │ │ │ ├── key.go │ │ │ │ │ ├── server.go │ │ │ │ │ └── xor.go │ │ │ │ ├── packet.go │ │ │ │ ├── vision/ │ │ │ │ │ ├── conn.go │ │ │ │ │ ├── filter.go │ │ │ │ │ ├── padding.go │ │ │ │ │ └── vision.go │ │ │ │ └── vless.go │ │ │ ├── vmess/ │ │ │ │ ├── aead.go │ │ │ │ ├── chunk.go │ │ │ │ ├── conn.go │ │ │ │ ├── h2.go │ │ │ │ ├── header.go │ │ │ │ ├── http.go │ │ │ │ ├── tls.go │ │ │ │ ├── user.go │ │ │ │ ├── vmess.go │ │ │ │ ├── websocket.go │ │ │ │ ├── websocket_go120.go │ │ │ │ └── websocket_go121.go │ │ │ └── xhttp/ │ │ │ ├── browser.go │ │ │ ├── client.go │ │ │ ├── config.go │ │ │ ├── conn.go │ │ │ ├── reuse.go │ │ │ ├── reuse_test.go │ │ │ ├── server.go │ │ │ ├── server_test.go │ │ │ ├── upload_queue.go │ │ │ ├── upload_queue_test.go │ │ │ └── xpadding.go │ │ └── tunnel/ │ │ ├── connection.go │ │ ├── dns_dialer.go │ │ ├── mode.go │ │ ├── statistic/ │ │ │ ├── manager.go │ │ │ ├── patch.go │ │ │ ├── patch_android.go │ │ │ └── tracker.go │ │ ├── status.go │ │ └── tunnel.go │ ├── action.go │ ├── android_bride.go │ ├── common.go │ ├── constant.go │ ├── dart-bridge/ │ │ ├── include/ │ │ │ ├── dart_api.h │ │ │ ├── dart_api_dl.c │ │ │ ├── dart_api_dl.h │ │ │ ├── dart_native_api.h │ │ │ ├── dart_tools_api.h │ │ │ ├── dart_version.h │ │ │ └── internal/ │ │ │ └── dart_api_dl_impl.h │ │ ├── lib.go │ │ └── lib_common.go │ ├── go.mod │ ├── go.sum │ ├── hub.go │ ├── lib.go │ ├── lib_android.go │ ├── lib_no_android.go │ ├── main.go │ ├── main_cgo.go │ ├── platform/ │ │ ├── limit.go │ │ └── procfs.go │ ├── server.go │ ├── state/ │ │ └── state.go │ └── tun/ │ └── tun.go ├── devtools_options.yaml ├── distribute_options.yaml ├── lib/ │ ├── application.dart │ ├── clash/ │ │ ├── clash.dart │ │ ├── core.dart │ │ ├── generated/ │ │ │ └── clash_ffi.dart │ │ ├── interface.dart │ │ ├── lib.dart │ │ ├── message.dart │ │ └── service.dart │ ├── common/ │ │ ├── android.dart │ │ ├── app_localizations.dart │ │ ├── archive.dart │ │ ├── color.dart │ │ ├── common.dart │ │ ├── constant.dart │ │ ├── context.dart │ │ ├── converter.dart │ │ ├── datetime.dart │ │ ├── dav_client.dart │ │ ├── fixed.dart │ │ ├── flclash_database_extractor.dart │ │ ├── function.dart │ │ ├── future.dart │ │ ├── helper_auth.dart │ │ ├── http.dart │ │ ├── icons.dart │ │ ├── iterable.dart │ │ ├── js_runtime_manager.dart │ │ ├── keyboard.dart │ │ ├── launch.dart │ │ ├── link.dart │ │ ├── lock.dart │ │ ├── measure.dart │ │ ├── mixin.dart │ │ ├── navigation.dart │ │ ├── navigator.dart │ │ ├── network.dart │ │ ├── network_matcher.dart │ │ ├── num.dart │ │ ├── package.dart │ │ ├── path.dart │ │ ├── picker.dart │ │ ├── preferences.dart │ │ ├── print.dart │ │ ├── protocol.dart │ │ ├── proxy.dart │ │ ├── render.dart │ │ ├── request.dart │ │ ├── scroll.dart │ │ ├── state.dart │ │ ├── string.dart │ │ ├── system.dart │ │ ├── task.dart │ │ ├── text.dart │ │ ├── theme.dart │ │ ├── tray.dart │ │ ├── ui_manager.dart │ │ ├── utils.dart │ │ └── window.dart │ ├── controller.dart │ ├── enum/ │ │ └── enum.dart │ ├── l10n/ │ │ ├── intl/ │ │ │ ├── messages_all.dart │ │ │ ├── messages_en.dart │ │ │ ├── messages_ru.dart │ │ │ ├── messages_zh_CN.dart │ │ │ └── messages_zh_TC.dart │ │ └── l10n.dart │ ├── main.dart │ ├── manager/ │ │ ├── android_manager.dart │ │ ├── app_manager.dart │ │ ├── clash_manager.dart │ │ ├── connectivity_manager.dart │ │ ├── hotkey_manager.dart │ │ ├── manager.dart │ │ ├── message_manager.dart │ │ ├── proxy_manager.dart │ │ ├── smart_auto_stop_manager.dart │ │ ├── theme_manager.dart │ │ ├── tile_manager.dart │ │ ├── tray_manager.dart │ │ ├── vpn_manager.dart │ │ └── window_manager.dart │ ├── models/ │ │ ├── app.dart │ │ ├── clash_config.dart │ │ ├── common.dart │ │ ├── config.dart │ │ ├── core.dart │ │ ├── generated/ │ │ │ ├── app.freezed.dart │ │ │ ├── clash_config.freezed.dart │ │ │ ├── clash_config.g.dart │ │ │ ├── common.freezed.dart │ │ │ ├── common.g.dart │ │ │ ├── config.freezed.dart │ │ │ ├── config.g.dart │ │ │ ├── core.freezed.dart │ │ │ ├── core.g.dart │ │ │ ├── profile.freezed.dart │ │ │ ├── profile.g.dart │ │ │ ├── selector.freezed.dart │ │ │ └── widget.freezed.dart │ │ ├── models.dart │ │ ├── profile.dart │ │ ├── selector.dart │ │ └── widget.dart │ ├── pages/ │ │ ├── editor.dart │ │ ├── home.dart │ │ ├── pages.dart │ │ └── scan.dart │ ├── plugins/ │ │ ├── app.dart │ │ ├── service.dart │ │ ├── tile.dart │ │ └── vpn.dart │ ├── providers/ │ │ ├── app.dart │ │ ├── config.dart │ │ ├── generated/ │ │ │ ├── app.g.dart │ │ │ ├── config.g.dart │ │ │ └── state.g.dart │ │ ├── providers.dart │ │ └── state.dart │ ├── state.dart │ ├── views/ │ │ ├── about.dart │ │ ├── access.dart │ │ ├── application_setting.dart │ │ ├── backup_and_recovery.dart │ │ ├── config/ │ │ │ ├── config.dart │ │ │ ├── dns.dart │ │ │ ├── experimental.dart │ │ │ ├── general.dart │ │ │ ├── network.dart │ │ │ ├── ntp.dart │ │ │ ├── sniffer.dart │ │ │ └── tunnel.dart │ │ ├── connection/ │ │ │ ├── connections.dart │ │ │ ├── item.dart │ │ │ └── requests.dart │ │ ├── dashboard/ │ │ │ ├── dashboard.dart │ │ │ └── widgets/ │ │ │ ├── connections_count.dart │ │ │ ├── dns_override.dart │ │ │ ├── fcm_status.dart │ │ │ ├── intranet_ip.dart │ │ │ ├── ipv6_switch.dart │ │ │ ├── memory_info.dart │ │ │ ├── network_detection.dart │ │ │ ├── network_speed.dart │ │ │ ├── network_speed_small.dart │ │ │ ├── ntp_override.dart │ │ │ ├── online_panel.dart │ │ │ ├── outbound_mode.dart │ │ │ ├── providers_info.dart │ │ │ ├── quick_options.dart │ │ │ ├── sniffer_override.dart │ │ │ ├── start_button.dart │ │ │ ├── traffic_usage.dart │ │ │ ├── wakelock_switch.dart │ │ │ └── widgets.dart │ │ ├── developer.dart │ │ ├── hotkey.dart │ │ ├── logs.dart │ │ ├── other_setting.dart │ │ ├── profiles/ │ │ │ ├── add_profile.dart │ │ │ ├── edit_profile.dart │ │ │ ├── override_profile.dart │ │ │ ├── profiles.dart │ │ │ └── scripts.dart │ │ ├── proxies/ │ │ │ ├── advanced_settings.dart │ │ │ ├── card.dart │ │ │ ├── common.dart │ │ │ ├── list.dart │ │ │ ├── providers.dart │ │ │ ├── proxies.dart │ │ │ ├── setting.dart │ │ │ └── tab.dart │ │ ├── resources.dart │ │ ├── theme.dart │ │ ├── tools.dart │ │ └── views.dart │ └── widgets/ │ ├── activate_box.dart │ ├── animate_grid.dart │ ├── bar_chart.dart │ ├── builder.dart │ ├── card.dart │ ├── chip.dart │ ├── color_scheme_box.dart │ ├── container.dart │ ├── dialog.dart │ ├── disabled_mask.dart │ ├── donut_chart.dart │ ├── effect.dart │ ├── fade_box.dart │ ├── float_layout.dart │ ├── google_bottom_nav_bar.dart │ ├── grid.dart │ ├── icon.dart │ ├── input.dart │ ├── keep_scope.dart │ ├── line_chart.dart │ ├── list.dart │ ├── notification.dart │ ├── null_status.dart │ ├── open_container.dart │ ├── palette.dart │ ├── pop_scope.dart │ ├── popup.dart │ ├── scaffold.dart │ ├── scroll.dart │ ├── setting.dart │ ├── sheet.dart │ ├── side_sheet.dart │ ├── subscription_info_view.dart │ ├── super_grid.dart │ ├── tab.dart │ ├── text.dart │ ├── view.dart │ ├── wave.dart │ └── widgets.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ ├── my_application.h │ └── packaging/ │ ├── appimage/ │ │ └── make_config.yaml │ ├── deb/ │ │ └── make_config.yaml │ └── rpm/ │ └── make_config.yaml ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ ├── RunnerTests/ │ │ └── RunnerTests.swift │ └── packaging/ │ └── dmg/ │ └── make_config.yaml ├── plugins/ │ ├── flutter_distributor/ │ │ ├── .all-contributorsrc │ │ ├── .github/ │ │ │ ├── FUNDING.yml │ │ │ └── workflows/ │ │ │ ├── hello_world_build.yml │ │ │ ├── lint.yml │ │ │ └── test.yml │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── analysis_options.yaml │ │ ├── examples/ │ │ │ ├── custom_binary_name/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .metadata │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── distribute_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ └── main.dart │ │ │ │ ├── linux/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── CMakeLists.txt │ │ │ │ │ ├── flutter/ │ │ │ │ │ │ ├── CMakeLists.txt │ │ │ │ │ │ ├── generated_plugin_registrant.cc │ │ │ │ │ │ ├── generated_plugin_registrant.h │ │ │ │ │ │ └── generated_plugins.cmake │ │ │ │ │ ├── main.cc │ │ │ │ │ ├── my_application.cc │ │ │ │ │ ├── my_application.h │ │ │ │ │ └── packaging/ │ │ │ │ │ ├── appimage/ │ │ │ │ │ │ └── make_config.yaml │ │ │ │ │ └── deb/ │ │ │ │ │ └── make_config.yaml │ │ │ │ ├── macos/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── Flutter/ │ │ │ │ │ │ ├── Flutter-Debug.xcconfig │ │ │ │ │ │ ├── Flutter-Release.xcconfig │ │ │ │ │ │ └── GeneratedPluginRegistrant.swift │ │ │ │ │ ├── Runner/ │ │ │ │ │ │ ├── AppDelegate.swift │ │ │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── Base.lproj/ │ │ │ │ │ │ │ └── MainMenu.xib │ │ │ │ │ │ ├── Configs/ │ │ │ │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ │ │ │ ├── Debug.xcconfig │ │ │ │ │ │ │ ├── Release.xcconfig │ │ │ │ │ │ │ └── Warnings.xcconfig │ │ │ │ │ │ ├── DebugProfile.entitlements │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ ├── MainFlutterWindow.swift │ │ │ │ │ │ └── Release.entitlements │ │ │ │ │ ├── Runner.xcodeproj/ │ │ │ │ │ │ ├── project.pbxproj │ │ │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ └── xcschemes/ │ │ │ │ │ │ └── Runner.xcscheme │ │ │ │ │ ├── Runner.xcworkspace/ │ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ │ ├── RunnerTests/ │ │ │ │ │ │ └── RunnerTests.swift │ │ │ │ │ └── packaging/ │ │ │ │ │ └── dmg/ │ │ │ │ │ └── make_config.yaml │ │ │ │ ├── pubspec.yaml │ │ │ │ ├── release.sh │ │ │ │ └── test/ │ │ │ │ └── widget_test.dart │ │ │ ├── hello_world/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .metadata │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── android/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── build.gradle │ │ │ │ │ │ └── src/ │ │ │ │ │ │ ├── debug/ │ │ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ │ │ ├── main/ │ │ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ │ │ ├── kotlin/ │ │ │ │ │ │ │ │ └── org/ │ │ │ │ │ │ │ │ └── leanflutter/ │ │ │ │ │ │ │ │ └── examples/ │ │ │ │ │ │ │ │ └── hello_world/ │ │ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ │ │ └── res/ │ │ │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ │ ├── drawable-v21/ │ │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ │ ├── values/ │ │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ │ └── values-night/ │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ └── profile/ │ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ │ ├── build.gradle │ │ │ │ │ ├── gradle/ │ │ │ │ │ │ └── wrapper/ │ │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ │ ├── gradle.properties │ │ │ │ │ └── settings.gradle │ │ │ │ ├── distribute_options.yaml │ │ │ │ ├── env.json │ │ │ │ ├── ios/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── ExportOptions.plist │ │ │ │ │ ├── Flutter/ │ │ │ │ │ │ ├── AppFrameworkInfo.plist │ │ │ │ │ │ ├── Debug.xcconfig │ │ │ │ │ │ └── Release.xcconfig │ │ │ │ │ ├── Podfile │ │ │ │ │ ├── Runner/ │ │ │ │ │ │ ├── AppDelegate.swift │ │ │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── Base.lproj/ │ │ │ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ │ │ │ └── Main.storyboard │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ └── Runner-Bridging-Header.h │ │ │ │ │ ├── Runner.xcodeproj/ │ │ │ │ │ │ ├── project.pbxproj │ │ │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ └── xcschemes/ │ │ │ │ │ │ └── Runner.xcscheme │ │ │ │ │ └── Runner.xcworkspace/ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ │ ├── lib/ │ │ │ │ │ └── main.dart │ │ │ │ ├── linux/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── CMakeLists.txt │ │ │ │ │ ├── flutter/ │ │ │ │ │ │ ├── CMakeLists.txt │ │ │ │ │ │ ├── generated_plugin_registrant.cc │ │ │ │ │ │ ├── generated_plugin_registrant.h │ │ │ │ │ │ └── generated_plugins.cmake │ │ │ │ │ ├── main.cc │ │ │ │ │ ├── my_application.cc │ │ │ │ │ ├── my_application.h │ │ │ │ │ └── packaging/ │ │ │ │ │ ├── appimage/ │ │ │ │ │ │ └── make_config.yaml │ │ │ │ │ ├── deb/ │ │ │ │ │ │ └── make_config.yaml │ │ │ │ │ ├── helloworld.appdata.xml │ │ │ │ │ ├── pacman/ │ │ │ │ │ │ └── make_config.yaml │ │ │ │ │ └── rpm/ │ │ │ │ │ └── make_config.yaml │ │ │ │ ├── macos/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── Flutter/ │ │ │ │ │ │ ├── Flutter-Debug.xcconfig │ │ │ │ │ │ ├── Flutter-Release.xcconfig │ │ │ │ │ │ └── GeneratedPluginRegistrant.swift │ │ │ │ │ ├── Podfile │ │ │ │ │ ├── Runner/ │ │ │ │ │ │ ├── AppDelegate.swift │ │ │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── Base.lproj/ │ │ │ │ │ │ │ └── MainMenu.xib │ │ │ │ │ │ ├── Configs/ │ │ │ │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ │ │ │ ├── Debug.xcconfig │ │ │ │ │ │ │ ├── Release.xcconfig │ │ │ │ │ │ │ └── Warnings.xcconfig │ │ │ │ │ │ ├── DebugProfile.entitlements │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ ├── MainFlutterWindow.swift │ │ │ │ │ │ └── Release.entitlements │ │ │ │ │ ├── Runner.xcodeproj/ │ │ │ │ │ │ ├── project.pbxproj │ │ │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ └── xcschemes/ │ │ │ │ │ │ └── Runner.xcscheme │ │ │ │ │ ├── Runner.xcworkspace/ │ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ │ └── packaging/ │ │ │ │ │ ├── dmg/ │ │ │ │ │ │ └── make_config.yaml │ │ │ │ │ └── pkg/ │ │ │ │ │ └── make_config.yaml │ │ │ │ ├── ohos/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── AppScope/ │ │ │ │ │ │ ├── app.json5 │ │ │ │ │ │ └── resources/ │ │ │ │ │ │ └── base/ │ │ │ │ │ │ └── element/ │ │ │ │ │ │ └── string.json │ │ │ │ │ ├── build-profile.json5 │ │ │ │ │ ├── entry/ │ │ │ │ │ │ ├── .gitignore │ │ │ │ │ │ ├── build-profile.json5 │ │ │ │ │ │ ├── hvigorfile.ts │ │ │ │ │ │ ├── oh-package.json5 │ │ │ │ │ │ └── src/ │ │ │ │ │ │ ├── main/ │ │ │ │ │ │ │ ├── ets/ │ │ │ │ │ │ │ │ ├── entryability/ │ │ │ │ │ │ │ │ │ └── EntryAbility.ets │ │ │ │ │ │ │ │ ├── pages/ │ │ │ │ │ │ │ │ │ └── Index.ets │ │ │ │ │ │ │ │ └── plugins/ │ │ │ │ │ │ │ │ └── GeneratedPluginRegistrant.ets │ │ │ │ │ │ │ ├── module.json5 │ │ │ │ │ │ │ └── resources/ │ │ │ │ │ │ │ ├── base/ │ │ │ │ │ │ │ │ ├── element/ │ │ │ │ │ │ │ │ │ ├── color.json │ │ │ │ │ │ │ │ │ └── string.json │ │ │ │ │ │ │ │ └── profile/ │ │ │ │ │ │ │ │ ├── buildinfo.json5 │ │ │ │ │ │ │ │ └── main_pages.json │ │ │ │ │ │ │ ├── en_US/ │ │ │ │ │ │ │ │ └── element/ │ │ │ │ │ │ │ │ └── string.json │ │ │ │ │ │ │ └── zh_CN/ │ │ │ │ │ │ │ └── element/ │ │ │ │ │ │ │ └── string.json │ │ │ │ │ │ └── ohosTest/ │ │ │ │ │ │ ├── ets/ │ │ │ │ │ │ │ ├── test/ │ │ │ │ │ │ │ │ ├── Ability.test.ets │ │ │ │ │ │ │ │ └── List.test.ets │ │ │ │ │ │ │ ├── testability/ │ │ │ │ │ │ │ │ ├── TestAbility.ets │ │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ │ └── Index.ets │ │ │ │ │ │ │ └── testrunner/ │ │ │ │ │ │ │ └── OpenHarmonyTestRunner.ts │ │ │ │ │ │ ├── module.json5 │ │ │ │ │ │ └── resources/ │ │ │ │ │ │ └── base/ │ │ │ │ │ │ ├── element/ │ │ │ │ │ │ │ ├── color.json │ │ │ │ │ │ │ └── string.json │ │ │ │ │ │ └── profile/ │ │ │ │ │ │ └── test_pages.json │ │ │ │ │ ├── hvigor/ │ │ │ │ │ │ └── hvigor-config.json5 │ │ │ │ │ ├── hvigorfile.ts │ │ │ │ │ └── oh-package.json5 │ │ │ │ ├── pubspec.yaml │ │ │ │ ├── release.sh │ │ │ │ ├── test/ │ │ │ │ │ └── widget_test.dart │ │ │ │ ├── web/ │ │ │ │ │ ├── index.html │ │ │ │ │ └── manifest.json │ │ │ │ └── windows/ │ │ │ │ ├── .gitignore │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── flutter/ │ │ │ │ │ ├── CMakeLists.txt │ │ │ │ │ ├── generated_plugin_registrant.cc │ │ │ │ │ ├── generated_plugin_registrant.h │ │ │ │ │ └── generated_plugins.cmake │ │ │ │ ├── packaging/ │ │ │ │ │ ├── exe/ │ │ │ │ │ │ ├── custom-inno-setup-script.iss │ │ │ │ │ │ └── make_config.yaml │ │ │ │ │ └── msix/ │ │ │ │ │ └── make_config.yaml │ │ │ │ └── runner/ │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── Runner.rc │ │ │ │ ├── flutter_window.cpp │ │ │ │ ├── flutter_window.h │ │ │ │ ├── main.cpp │ │ │ │ ├── resource.h │ │ │ │ ├── runner.exe.manifest │ │ │ │ ├── utils.cpp │ │ │ │ ├── utils.h │ │ │ │ ├── win32_window.cpp │ │ │ │ └── win32_window.h │ │ │ └── multiple_flavors/ │ │ │ ├── .gitignore │ │ │ ├── .metadata │ │ │ ├── analysis_options.yaml │ │ │ ├── android/ │ │ │ │ ├── .gitignore │ │ │ │ ├── app/ │ │ │ │ │ ├── build.gradle │ │ │ │ │ └── src/ │ │ │ │ │ ├── debug/ │ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ │ ├── main/ │ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ │ ├── java/ │ │ │ │ │ │ │ └── org/ │ │ │ │ │ │ │ └── leanflutter/ │ │ │ │ │ │ │ └── examples/ │ │ │ │ │ │ │ └── multiple_flavors/ │ │ │ │ │ │ │ └── MainActivity.java │ │ │ │ │ │ └── res/ │ │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── drawable-v21/ │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── values/ │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ └── values-night/ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── profile/ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── build.gradle │ │ │ │ ├── gradle/ │ │ │ │ │ └── wrapper/ │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ ├── gradle.properties │ │ │ │ └── settings.gradle │ │ │ ├── distribute_options.yaml │ │ │ ├── ios/ │ │ │ │ ├── .gitignore │ │ │ │ ├── Flutter/ │ │ │ │ │ ├── AppFrameworkInfo.plist │ │ │ │ │ ├── Debug.xcconfig │ │ │ │ │ └── Release.xcconfig │ │ │ │ ├── Podfile │ │ │ │ ├── Runner/ │ │ │ │ │ ├── AppDelegate.h │ │ │ │ │ ├── AppDelegate.m │ │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Base.lproj/ │ │ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ │ │ └── Main.storyboard │ │ │ │ │ ├── Info.plist │ │ │ │ │ └── main.m │ │ │ │ ├── Runner.xcodeproj/ │ │ │ │ │ ├── project.pbxproj │ │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ └── xcschemes/ │ │ │ │ │ ├── Runner.xcscheme │ │ │ │ │ ├── dev.xcscheme │ │ │ │ │ └── prod.xcscheme │ │ │ │ ├── Runner.xcworkspace/ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ │ ├── dev_ExportOptions.plist │ │ │ │ └── prod_ExportOptions.plist │ │ │ ├── lib/ │ │ │ │ └── main.dart │ │ │ ├── macos/ │ │ │ │ ├── .gitignore │ │ │ │ ├── Flutter/ │ │ │ │ │ ├── Flutter-Debug.xcconfig │ │ │ │ │ ├── Flutter-Release.xcconfig │ │ │ │ │ └── GeneratedPluginRegistrant.swift │ │ │ │ ├── Podfile │ │ │ │ ├── Runner/ │ │ │ │ │ ├── AppDelegate.swift │ │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Base.lproj/ │ │ │ │ │ │ └── MainMenu.xib │ │ │ │ │ ├── Configs/ │ │ │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ │ │ ├── Debug.xcconfig │ │ │ │ │ │ ├── Release.xcconfig │ │ │ │ │ │ └── Warnings.xcconfig │ │ │ │ │ ├── DebugProfile.entitlements │ │ │ │ │ ├── Info.plist │ │ │ │ │ ├── MainFlutterWindow.swift │ │ │ │ │ └── Release.entitlements │ │ │ │ ├── Runner.xcodeproj/ │ │ │ │ │ ├── project.pbxproj │ │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ └── xcschemes/ │ │ │ │ │ ├── Runner.xcscheme │ │ │ │ │ ├── dev.xcscheme │ │ │ │ │ └── prod.xcscheme │ │ │ │ ├── Runner.xcworkspace/ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ ├── RunnerTests/ │ │ │ │ │ └── RunnerTests.swift │ │ │ │ └── packaging/ │ │ │ │ └── dmg/ │ │ │ │ └── make_config.yaml │ │ │ ├── ohos/ │ │ │ │ ├── .gitignore │ │ │ │ ├── AppScope/ │ │ │ │ │ ├── app.json5 │ │ │ │ │ └── resources/ │ │ │ │ │ └── base/ │ │ │ │ │ └── element/ │ │ │ │ │ └── string.json │ │ │ │ ├── build-profile.json5 │ │ │ │ ├── entry/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── build-profile.json5 │ │ │ │ │ ├── hvigorfile.ts │ │ │ │ │ ├── oh-package.json5 │ │ │ │ │ └── src/ │ │ │ │ │ ├── main/ │ │ │ │ │ │ ├── ets/ │ │ │ │ │ │ │ ├── entryability/ │ │ │ │ │ │ │ │ └── EntryAbility.ets │ │ │ │ │ │ │ ├── pages/ │ │ │ │ │ │ │ │ └── Index.ets │ │ │ │ │ │ │ └── plugins/ │ │ │ │ │ │ │ └── GeneratedPluginRegistrant.ets │ │ │ │ │ │ ├── module.json5 │ │ │ │ │ │ └── resources/ │ │ │ │ │ │ ├── base/ │ │ │ │ │ │ │ ├── element/ │ │ │ │ │ │ │ │ ├── color.json │ │ │ │ │ │ │ │ └── string.json │ │ │ │ │ │ │ └── profile/ │ │ │ │ │ │ │ ├── buildinfo.json5 │ │ │ │ │ │ │ └── main_pages.json │ │ │ │ │ │ ├── en_US/ │ │ │ │ │ │ │ └── element/ │ │ │ │ │ │ │ └── string.json │ │ │ │ │ │ └── zh_CN/ │ │ │ │ │ │ └── element/ │ │ │ │ │ │ └── string.json │ │ │ │ │ └── ohosTest/ │ │ │ │ │ ├── ets/ │ │ │ │ │ │ ├── test/ │ │ │ │ │ │ │ ├── Ability.test.ets │ │ │ │ │ │ │ └── List.test.ets │ │ │ │ │ │ ├── testability/ │ │ │ │ │ │ │ ├── TestAbility.ets │ │ │ │ │ │ │ └── pages/ │ │ │ │ │ │ │ └── Index.ets │ │ │ │ │ │ └── testrunner/ │ │ │ │ │ │ └── OpenHarmonyTestRunner.ts │ │ │ │ │ ├── module.json5 │ │ │ │ │ └── resources/ │ │ │ │ │ └── base/ │ │ │ │ │ ├── element/ │ │ │ │ │ │ ├── color.json │ │ │ │ │ │ └── string.json │ │ │ │ │ └── profile/ │ │ │ │ │ └── test_pages.json │ │ │ │ ├── hvigor/ │ │ │ │ │ └── hvigor-config.json5 │ │ │ │ ├── hvigorfile.ts │ │ │ │ └── oh-package.json5 │ │ │ ├── pubspec.yaml │ │ │ ├── release.sh │ │ │ └── test/ │ │ │ └── widget_test.dart │ │ ├── melos.yaml │ │ ├── packages/ │ │ │ ├── fastforge/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── bin/ │ │ │ │ │ └── main.dart │ │ │ │ ├── lib/ │ │ │ │ │ └── fastforge.dart │ │ │ │ └── pubspec.yaml │ │ │ ├── flutter_app_builder/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── flutter_app_builder.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── build_config.dart │ │ │ │ │ ├── build_error.dart │ │ │ │ │ ├── build_result.dart │ │ │ │ │ ├── builders/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ ├── app_builder_android.dart │ │ │ │ │ │ │ └── build_android_result.dart │ │ │ │ │ │ ├── app_builder.dart │ │ │ │ │ │ ├── builders.dart │ │ │ │ │ │ ├── ios/ │ │ │ │ │ │ │ ├── app_builder_ios.dart │ │ │ │ │ │ │ └── build_ios_result.dart │ │ │ │ │ │ ├── linux/ │ │ │ │ │ │ │ ├── app_builder_linux.dart │ │ │ │ │ │ │ └── build_linux_result.dart │ │ │ │ │ │ ├── macos/ │ │ │ │ │ │ │ ├── app_builder_macos.dart │ │ │ │ │ │ │ └── build_macos_result.dart │ │ │ │ │ │ ├── ohos/ │ │ │ │ │ │ │ ├── app_builder_ohos.dart │ │ │ │ │ │ │ └── build_ohos_result.dart │ │ │ │ │ │ ├── web/ │ │ │ │ │ │ │ ├── app_builder_web.dart │ │ │ │ │ │ │ └── build_web_result.dart │ │ │ │ │ │ └── windows/ │ │ │ │ │ │ ├── app_builder_windows.dart │ │ │ │ │ │ └── build_windows_result.dart │ │ │ │ │ ├── commands/ │ │ │ │ │ │ └── flutter.dart │ │ │ │ │ └── flutter_app_builder.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ └── src/ │ │ │ │ ├── build_config_test.dart │ │ │ │ ├── builders/ │ │ │ │ │ ├── android/ │ │ │ │ │ │ └── build_android_result_test.dart │ │ │ │ │ ├── ios/ │ │ │ │ │ │ └── build_ios_result_test.dart │ │ │ │ │ ├── linux/ │ │ │ │ │ │ └── build_linux_result_test.dart │ │ │ │ │ ├── macos/ │ │ │ │ │ │ └── build_ios_result_test.dart │ │ │ │ │ ├── web/ │ │ │ │ │ │ └── build_web_result_test.dart │ │ │ │ │ └── windows/ │ │ │ │ │ └── build_windows_result_test.dart │ │ │ │ └── commands/ │ │ │ │ └── flutter_test.dart │ │ │ ├── flutter_app_packager/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── flutter_app_packager.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── app_package_maker.dart │ │ │ │ │ │ ├── distribute_options_base.dart │ │ │ │ │ │ ├── make_config.dart │ │ │ │ │ │ ├── make_error.dart │ │ │ │ │ │ └── make_result.dart │ │ │ │ │ ├── flutter_app_packager.dart │ │ │ │ │ └── makers/ │ │ │ │ │ ├── aab/ │ │ │ │ │ │ └── app_package_maker_aab.dart │ │ │ │ │ ├── apk/ │ │ │ │ │ │ └── app_package_maker_apk.dart │ │ │ │ │ ├── app/ │ │ │ │ │ │ └── app_package_maker_app.dart │ │ │ │ │ ├── appimage/ │ │ │ │ │ │ ├── app_package_maker_appimage.dart │ │ │ │ │ │ └── make_appimage_config.dart │ │ │ │ │ ├── deb/ │ │ │ │ │ │ ├── app_package_maker_deb.dart │ │ │ │ │ │ └── make_deb_config.dart │ │ │ │ │ ├── direct/ │ │ │ │ │ │ └── app_package_maker_direct.dart │ │ │ │ │ ├── dmg/ │ │ │ │ │ │ ├── app_package_maker_dmg.dart │ │ │ │ │ │ ├── commands/ │ │ │ │ │ │ │ └── appdmg.dart │ │ │ │ │ │ └── make_dmg_config.dart │ │ │ │ │ ├── exe/ │ │ │ │ │ │ ├── app_package_maker_exe.dart │ │ │ │ │ │ ├── inno_setup/ │ │ │ │ │ │ │ ├── inno_setup_compiler.dart │ │ │ │ │ │ │ └── inno_setup_script.dart │ │ │ │ │ │ └── make_exe_config.dart │ │ │ │ │ ├── hap/ │ │ │ │ │ │ └── app_package_maker_hap.dart │ │ │ │ │ ├── ipa/ │ │ │ │ │ │ └── app_package_maker_ipa.dart │ │ │ │ │ ├── makers.dart │ │ │ │ │ ├── msix/ │ │ │ │ │ │ ├── app_package_maker_msix.dart │ │ │ │ │ │ └── make_msix_config.dart │ │ │ │ │ ├── pacman/ │ │ │ │ │ │ ├── app_package_maker_pacman.dart │ │ │ │ │ │ └── make_pacman_config.dart │ │ │ │ │ ├── pkg/ │ │ │ │ │ │ ├── app_package_maker_pkg.dart │ │ │ │ │ │ └── make_pkg_config.dart │ │ │ │ │ ├── rpm/ │ │ │ │ │ │ ├── app_package_maker_rpm.dart │ │ │ │ │ │ ├── make_rpm_config.dart │ │ │ │ │ │ └── rpmbuild.dart │ │ │ │ │ └── zip/ │ │ │ │ │ └── app_package_maker_zip.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ └── src/ │ │ │ │ └── api/ │ │ │ │ └── make_config_test.dart │ │ │ ├── flutter_app_publisher/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── flutter_app_publisher.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── app_package_publisher.dart │ │ │ │ │ ├── flutter_app_publisher.dart │ │ │ │ │ └── publishers/ │ │ │ │ │ ├── appcenter/ │ │ │ │ │ │ ├── app_package_publisher_appcenter.dart │ │ │ │ │ │ └── publish_appcenter_config.dart │ │ │ │ │ ├── appstore/ │ │ │ │ │ │ ├── app_package_publisher_appstore.dart │ │ │ │ │ │ └── publish_appstore_config.dart │ │ │ │ │ ├── fir/ │ │ │ │ │ │ ├── app_package_publisher_fir.dart │ │ │ │ │ │ └── publish_fir_config.dart │ │ │ │ │ ├── firebase/ │ │ │ │ │ │ ├── app_package_publisher_firebase.dart │ │ │ │ │ │ └── publish_firebase_config.dart │ │ │ │ │ ├── firebase_hosting/ │ │ │ │ │ │ ├── app_package_publisher_firebase_hosting.dart │ │ │ │ │ │ └── publish_firebase_hosting_config.dart │ │ │ │ │ ├── github/ │ │ │ │ │ │ ├── app_package_publisher_github.dart │ │ │ │ │ │ └── publish_github_config.dart │ │ │ │ │ ├── pgyer/ │ │ │ │ │ │ ├── app_package_publisher_pgyer.dart │ │ │ │ │ │ └── publish_pgyer_config.dart │ │ │ │ │ ├── playstore/ │ │ │ │ │ │ ├── app_package_publisher_playstore.dart │ │ │ │ │ │ └── publish_playstore_config.dart │ │ │ │ │ ├── publishers.dart │ │ │ │ │ ├── qiniu/ │ │ │ │ │ │ ├── app_package_publisher_qiniu.dart │ │ │ │ │ │ └── publish_qiniu_config.dart │ │ │ │ │ └── vercel/ │ │ │ │ │ ├── app_package_publisher_vercel.dart │ │ │ │ │ └── publish_vercel_config.dart │ │ │ │ └── pubspec.yaml │ │ │ ├── flutter_distributor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── bin/ │ │ │ │ │ ├── command_package.dart │ │ │ │ │ ├── command_publish.dart │ │ │ │ │ ├── command_release.dart │ │ │ │ │ ├── command_upgrade.dart │ │ │ │ │ └── main.dart │ │ │ │ ├── lib/ │ │ │ │ │ ├── flutter_distributor.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── distribute_options.dart │ │ │ │ │ ├── extensions/ │ │ │ │ │ │ ├── extensions.dart │ │ │ │ │ │ └── string.dart │ │ │ │ │ ├── flutter_distributor.dart │ │ │ │ │ ├── release.dart │ │ │ │ │ ├── release_job.dart │ │ │ │ │ └── utils/ │ │ │ │ │ ├── default_shell_executor.dart │ │ │ │ │ ├── logger.dart │ │ │ │ │ ├── pub_dev_api.dart │ │ │ │ │ └── utils.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ └── src/ │ │ │ │ └── extensions/ │ │ │ │ └── string_test.dart │ │ │ ├── parse_app_package/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── bin/ │ │ │ │ │ └── main.dart │ │ │ │ ├── lib/ │ │ │ │ │ ├── parse_app_package.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── app_package_parser.dart │ │ │ │ │ ├── parse_app_package.dart │ │ │ │ │ └── parsers/ │ │ │ │ │ ├── apk/ │ │ │ │ │ │ └── app_package_parser_apk.dart │ │ │ │ │ ├── ipa/ │ │ │ │ │ │ └── app_package_parser_ipa.dart │ │ │ │ │ └── parsers.dart │ │ │ │ └── pubspec.yaml │ │ │ ├── shell_executor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── lib/ │ │ │ │ │ ├── shell_executor.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── command.dart │ │ │ │ │ ├── command_error.dart │ │ │ │ │ ├── shell_executor.dart │ │ │ │ │ └── utils/ │ │ │ │ │ └── path_expansion.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ └── src/ │ │ │ │ └── utils/ │ │ │ │ └── path_expansion_test.dart │ │ │ ├── shell_uikit/ │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── example/ │ │ │ │ │ ├── progress_bar_example.dart │ │ │ │ │ └── spinner_example.dart │ │ │ │ ├── lib/ │ │ │ │ │ ├── shell_uikit.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── progress_bar.dart │ │ │ │ │ └── spinner.dart │ │ │ │ └── pubspec.yaml │ │ │ └── unified_distributor/ │ │ │ ├── .gitignore │ │ │ ├── LICENSE │ │ │ ├── analysis_options.yaml │ │ │ ├── lib/ │ │ │ │ ├── src/ │ │ │ │ │ ├── check_version_result.dart │ │ │ │ │ ├── cli/ │ │ │ │ │ │ ├── cli.dart │ │ │ │ │ │ ├── command_package.dart │ │ │ │ │ │ ├── command_publish.dart │ │ │ │ │ │ ├── command_release.dart │ │ │ │ │ │ └── command_upgrade.dart │ │ │ │ │ ├── distribute_options.dart │ │ │ │ │ ├── extensions/ │ │ │ │ │ │ └── string.dart │ │ │ │ │ ├── release.dart │ │ │ │ │ ├── release_job.dart │ │ │ │ │ ├── unified_distributor.dart │ │ │ │ │ └── utils/ │ │ │ │ │ ├── default_shell_executor.dart │ │ │ │ │ ├── logger.dart │ │ │ │ │ └── pub_dev_api.dart │ │ │ │ └── unified_distributor.dart │ │ │ ├── pubspec.yaml │ │ │ └── test/ │ │ │ └── src/ │ │ │ └── extensions/ │ │ │ └── string_test.dart │ │ ├── pubspec.yaml │ │ └── website/ │ │ ├── .gitignore │ │ ├── astro.config.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ └── starlight/ │ │ │ │ └── TableOfContents.astro │ │ │ ├── content/ │ │ │ │ ├── config.ts │ │ │ │ ├── docs/ │ │ │ │ │ ├── index.mdx │ │ │ │ │ └── zh-hans/ │ │ │ │ │ └── index.mdx │ │ │ │ └── i18n/ │ │ │ │ ├── en.json │ │ │ │ └── zh-cn.json │ │ │ ├── env.d.ts │ │ │ └── tailwind.css │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ ├── proxy/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── LICENSE │ │ ├── analysis_options.yaml │ │ ├── lib/ │ │ │ ├── proxy.dart │ │ │ ├── proxy_method_channel.dart │ │ │ └── proxy_platform_interface.dart │ │ ├── pubspec.yaml │ │ └── windows/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── include/ │ │ │ └── proxy/ │ │ │ └── proxy_plugin_c_api.h │ │ ├── proxy_plugin.cpp │ │ ├── proxy_plugin.h │ │ ├── proxy_plugin_c_api.cpp │ │ └── test/ │ │ └── proxy_plugin_test.cpp │ └── window_ext/ │ ├── .gitignore │ ├── .metadata │ ├── LICENSE │ ├── analysis_options.yaml │ ├── lib/ │ │ ├── window_ext.dart │ │ ├── window_ext_listener.dart │ │ └── window_ext_manager.dart │ ├── macos/ │ │ ├── Classes/ │ │ │ └── WindowExtPlugin.swift │ │ └── window_ext.podspec │ ├── pubspec.yaml │ └── windows/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── include/ │ │ └── window_ext/ │ │ └── window_ext_plugin_c_api.h │ ├── test/ │ │ └── window_ext_plugin_test.cpp │ ├── window_ext_plugin.cpp │ ├── window_ext_plugin.h │ └── window_ext_plugin_c_api.cpp ├── pubspec.yaml ├── services/ │ └── helper/ │ ├── Cargo.toml │ ├── build.rs │ └── src/ │ ├── main.rs │ └── service/ │ ├── hub.rs │ ├── mod.rs │ └── windows.rs ├── setup.dart └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── packaging/ │ └── exe/ │ ├── ChineseSimplified.isl │ ├── inno_setup.iss │ └── make_config.yaml └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐞 问题反馈 / Bug Report description: 反馈你遇到的问题 / Report an issue title: "[Bug]: " labels: ["bug"] body: - type: markdown attributes: value: | 感谢你提交问题!在提交之前,请确保已搜索过 [现有 Issue](https://github.com/appshubcc/Bettbox/issues)。 Thanks for reporting! Please search [existing issues](https://github.com/appshubcc/Bettbox/issues) before submitting. - type: textarea id: bug-description attributes: label: 问题描述 / Description placeholder: 请清晰地描述你遇到的问题,必要时附上截图。 / Describe the issue in detail, with screenshots if possible. validations: required: true - type: input id: version attributes: label: 软件版本 / Version placeholder: 例如 1.15.6 validations: required: true - type: dropdown id: os attributes: label: 操作系统 / OS options: - Android - Windows - macOS - Linux validations: required: true - type: textarea id: steps attributes: label: 复现步骤 / Reproduction Steps placeholder: | 1. 打开... 2. 点击... 3. 出现... validations: required: false - type: textarea id: logs attributes: label: 相关日志 / Logs description: 请在设置中开启 Debug 等级日志,并在此粘贴相关内容。尽量不要上传庞大的日志文件。 / Paste relevant debug logs here. render: shell validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: 💬 交流与讨论 / Channel & Chat url: https://t.me/appshub_chat about: 访问我们的 Telegram 讨论组 / Join the Telegram group ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: ✨ 功能请求 / Feature Request description: 提交一个新的功能建议 / Propose a new feature title: "[Feature]: " labels: ["enhancement"] body: - type: markdown attributes: value: | 感谢你为提高项目质量做出贡献!在提交建议前,请检查 [已有 Issue](https://github.com/appshubcc/Bettbox/issues)。 Thanks for your contribution! Please check [existing issues](https://github.com/appshubcc/Bettbox/issues) before proposing. - type: textarea id: feature-description attributes: label: 功能描述 / Description placeholder: 请清晰简洁地描述该功能。 / A concise description of the feature request. validations: required: true - type: textarea id: use-case attributes: label: 使用场景 / Use Case placeholder: 描述该功能的使用场景及它可以解决的问题。 / Describe the usage scenario and what problem it solves. validations: required: false - type: checkboxes id: os-labels attributes: label: 适用系统 / Target OS options: - label: Android - label: Windows - label: macOS - label: Linux validations: required: true ================================================ FILE: .github/release_template.md ================================================ # Bettbox 正式版本发布
### ✈️ Telegram 社区交流
[![Telegram Group](https://img.shields.io/badge/Appshub-Chat-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/appshub_chat) [![Telegram Channel](https://img.shields.io/badge/Appshub-Channel-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/appshub_channel) ---
### ⬇️ Download / 下载链接 **Note: For desktop CPUs from 2012 or earlier, please download the Compatible version**
**注意:桌面端2012年同期及之前的CPU,需要使用Compatible兼容版本** ---
| **OS/ 系统** | **Requirements / 版本要求** | **Direct Links / 点击直链下载** | |:---:|:---|:---| | Android | Android 8.0+
*(Compatible with Android TV)* |
| | Windows | Windows 10+
*(Compatible for Older CPU)* |
| | macOS | macOS 12.0+
*(Compatible for 10.15-11.7)* |
| | Linux | Linux Kernel 5.4+
*(Compatible for Older CPU)* |

| ---
### 🐛 Feedback / 问题反馈 > **Note / 提示:** > Detailed and well-structured issues will be prioritized(logs / reproduction steps)
> 书写认真、信息完整(包含必要的复现步骤和日志)的 issues 会被优先处理和对待 **Bug Report / 提交故障**: [Click Here / 点击这里](https://github.com/appshubcc/Bettbox/issues/new?template=bug_report.yml) **Feature Request / 需求建议**: [Click Here / 点击这里](https://github.com/appshubcc/Bettbox/issues/new?template=feature_request.yml)
================================================ FILE: .github/release_template_pre.md ================================================ # Bettbox 预览版本发布 **注意:预览版本通常包含最新的功能以及不稳定性,如遇问题请提交issue[[反馈]](https://github.com/appshubcc/Bettbox/issues/new?template=bug_report.yml)联系社区交流。
当前版本最新更新日志以及变更,请参考最新提交[[Commits]](https://github.com/appshubcc/Bettbox/commits/vVERSION/)**
### ✈️ Telegram 社区交流
[![Telegram Group](https://img.shields.io/badge/Appshub-Chat-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/appshub_chat) [![Telegram Channel](https://img.shields.io/badge/Appshub-Channel-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/appshub_channel) ---
### ⬇️ Download / 下载链接 **Note: For desktop CPUs from 2012 or earlier, please download the Compatible version**
**注意:桌面端2012年同期及之前的CPU,需要使用Compatible兼容版本** ---
| **OS/ 系统** | **Requirements / 版本要求** | **Direct Links / 点击直链下载** | |:---:|:---|:---| | Android | Android 8.0+
*(Compatible with Android TV)* |
| | Windows | Windows 10+
*(Compatible for Older CPU)* |
| | macOS | macOS 12.0+
*(Compatible for 10.15-11.7)* |
| | Linux | Linux Kernel 5.4+
*(Compatible for Older CPU)* |

| ---
### 🐛 Feedback / 问题反馈 > **Note / 提示:** > Detailed and well-structured issues will be prioritized(logs / reproduction steps)
> 书写认真、信息完整(包含必要的复现步骤和日志)的 issues 会被优先处理和对待 **Bug Report / 提交故障**: [Click Here / 点击这里](https://github.com/appshubcc/Bettbox/issues/new?template=bug_report.yml) **Feature Request / 需求建议**: [Click Here / 点击这里](https://github.com/appshubcc/Bettbox/issues/new?template=feature_request.yml)
================================================ FILE: .github/workflows/build.yaml ================================================ name: build on: push: tags: - 'v*' env: IS_STABLE: ${{ !contains(github.ref, '-') }} IS_PRERELEASE: ${{ contains(github.ref_name, 'pre') }} SHOULD_RELEASE: ${{ !contains(github.ref, '-') || contains(github.ref_name, 'pre') }} GRADLE_OPTS: -Dorg.gradle.vfs.watch=false FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true SENTRY_BACKEND: none jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - platform: android os: ubuntu-24.04 arch: arm64 - platform: android os: ubuntu-24.04 arch: amd64 - platform: android os: ubuntu-24.04 arch: arm - platform: android os: ubuntu-24.04 arch: universal - platform: windows os: windows-2022 arch: amd64 - platform: windows os: windows-11-arm arch: arm64 - platform: macos os: macos-15 arch: arm64 - platform: macos os: macos-15 arch: amd64 - platform: linux os: ubuntu-22.04 arch: amd64 - platform: linux os: ubuntu-24.04-arm arch: arm64 - platform: linux os: ubuntu-22.04 arch: amd64 compatible: true - platform: windows os: windows-2022 arch: amd64 compatible: true - platform: macos os: macos-15 arch: amd64 compatible: true steps: - name: Checkout uses: actions/checkout@v6 with: submodules: recursive fetch-depth: 1 - name: Setup Rust (Windows) if: matrix.platform == 'windows' uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.arch == 'arm64' && 'aarch64-pc-windows-msvc' || 'x86_64-pc-windows-msvc' }} - name: Enable Git long paths (Windows) if: matrix.platform == 'windows' run: git config --global core.longpaths true - name: Setup Keystore (Android) if: matrix.platform == 'android' run: | echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties - name: Setup Java if: matrix.platform == 'android' uses: actions/setup-java@v5 with: distribution: temurin java-version: '17' cache: 'gradle' - name: Install Dependencies (Linux) if: matrix.platform == 'linux' run: | sudo apt-get update sudo apt-get install -y libcurl4-openssl-dev ninja-build libgtk-3-dev rpm - name: Setup Golang uses: actions/setup-go@v6 with: go-version: ${{ matrix.compatible == true && '1.20.x' || '1.24.x' }} cache-dependency-path: core/go.sum - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: ${{ contains(matrix.os, 'arm') && 'master' || 'stable' }} cache: true cache-key: flutter-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('**/pubspec.lock') }} flutter-version: ${{ contains(matrix.os, 'arm') && '1c6c6f4' || '3.35.7' }} - name: Use prebuilt QuickJS bridge (Windows arm64) if: matrix.platform == 'windows' && matrix.arch == 'arm64' shell: pwsh run: | $dll = "windows\overrides\flutter_js\arm64\quickjs_c_bridge.dll" if (-not (Test-Path $dll)) { throw "Missing prebuilt QuickJS bridge: $dll" } Get-Item $dll | Select-Object FullName, Length, LastWriteTime - name: Clean Gradle (Android) if: matrix.platform == 'android' run: | cd android flutter clean - name: Build Bettbox env: SENTRY_DSN: ${{ secrets.SENTRY_DSN }} run: | flutter pub get dart run build_runner build -d dart setup.dart ${{ matrix.platform }} --arch ${{ matrix.arch }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }} ${{ matrix.compatible == true && '--compatible' || '' }} - name: Upload Artifacts uses: actions/upload-artifact@v6 with: name: Bettbox-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.compatible == true && '-compatible' || '' }} path: ./dist compression-level: 9 overwrite: true upload: permissions: write-all needs: [ build ] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Download uses: actions/download-artifact@v7 with: path: ./dist/ pattern: Bettbox-* merge-multiple: true - name: Generate release.md run: | echo -e "- Fixes and improvements\n- 具体变更参考最新Commits\n- 注意: 部分小米&Android16+机型在更新APP前,需提前关闭VPN以防止系统问题造成的网络锁死" > release.md - name: Patch release.md run: | version=$(echo "${{ github.ref_name }}" | sed 's/^v//') base_version=$(echo "$version" | sed 's/-pre[0-9]*//') if [ "${{ env.IS_PRERELEASE }}" == "true" ]; then sed -e "s|BASE_VERSION|$base_version|g" -e "s|VERSION|$version|g" ./.github/release_template_pre.md >> release.md else sed -e "s|BASE_VERSION|$base_version|g" -e "s|VERSION|$version|g" ./.github/release_template.md >> release.md fi - name: Release if: ${{ env.SHOULD_RELEASE == 'true' }} uses: softprops/action-gh-release@v3.0.0 with: files: ./dist/* body_path: './release.md' prerelease: ${{ env.IS_PRERELEASE == 'true' }} ================================================ FILE: .gitignore ================================================ # Miscellaneous *.md !.github/release_template.md !.github/release_template_pre.md *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ /specs/ /dist/ .test/ .vscode/ /services/helper/target/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release /android/core/.cxx #libclash /libclash/ #jniLibs /android/app/src/main/jniLibs/ # FVM Version Cache .fvm/ .fvmrc ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: 796c8ef79279f9c774545b3771238c3098dbefab channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: android create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: ios create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: linux create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: macos create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: web create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: windows create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: Makefile ================================================ android_arm64: dart ./setup.dart android --arch arm64 macos_arm64: dart ./setup.dart macos --arch arm64 android_app: dart ./setup.dart android android_arm64_core: dart ./setup.dart android --arch arm64 --out core macos_arm64_core: dart ./setup.dart macos --arch arm64 --out core ================================================ FILE: analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml analyzer: exclude: - lib/l10n/intl/** - lib/clash/generated/** - lib/**/generated/** - "**/*.g.dart" - "**/*.freezed.dart" - "plugins/**" linter: rules: prefer_single_quotes: true ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /gradle/ /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks ================================================ FILE: android/app/build.gradle.kts ================================================ import java.util.Properties plugins { id("com.android.application") id("kotlin-android") id("dev.flutter.flutter-gradle-plugin") } val localProperties = Properties().apply { rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) } } val mStoreFile = file("keystore.jks") val mStorePassword: String? = localProperties.getProperty("storePassword") val mKeyAlias: String? = localProperties.getProperty("keyAlias") val mKeyPassword: String? = localProperties.getProperty("keyPassword") val isRelease = mStoreFile.exists() && mStorePassword != null && mKeyAlias != null && mKeyPassword != null android { namespace = "com.appshub.bettbox" compileSdk = 36 ndkVersion = "28.2.13676358" compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { applicationId = "com.appshub.bettbox" minSdk = 26 targetSdk = 36 versionCode = flutter.versionCode versionName = flutter.versionName } signingConfigs { if (isRelease) { create("release") { storeFile = mStoreFile storePassword = mStorePassword keyAlias = mKeyAlias keyPassword = mKeyPassword } } } buildTypes { debug { isMinifyEnabled = false applicationIdSuffix = ".debug" } release { isMinifyEnabled = true isDebuggable = false signingConfig = signingConfigs.getByName(if (isRelease) "release" else "debug") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } packaging { jniLibs { useLegacyPackaging = true } } } flutter { source = "../.." } dependencies { implementation(project(":core")) implementation("com.google.code.gson:gson:2.10.1") implementation("com.android.tools.smali:smali-dexlib2:3.0.9") { exclude(group = "com.google.guava", module = "guava") } } configurations.all { resolutionStrategy { eachDependency { if (requested.group == "androidx.datastore") useVersion("1.1.2") } } } ================================================ FILE: android/app/proguard-rules.pro ================================================ -keep class com.appshub.bettbox.models.**{ *; } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/BettboxApplication.kt ================================================ package com.appshub.bettbox import android.app.Application import android.content.Context class BettboxApplication : Application() { companion object { private lateinit var instance: BettboxApplication fun getAppContext(): Context = instance.applicationContext } override fun onCreate() { super.onCreate() instance = this } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/FilesProvider.kt ================================================ package com.appshub.bettbox import android.database.Cursor import android.database.MatrixCursor import android.os.CancellationSignal import android.os.ParcelFileDescriptor import android.provider.DocumentsContract.Document import android.provider.DocumentsContract.Root import android.provider.DocumentsProvider import java.io.File import java.io.FileNotFoundException class FilesProvider : DocumentsProvider() { companion object { private const val DEFAULT_ROOT_ID = "0" private val DEFAULT_DOCUMENT_COLUMNS = arrayOf( Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, ) private val DEFAULT_ROOT_COLUMNS = arrayOf( Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID ) } override fun onCreate(): Boolean = true override fun queryRoots(projection: Array?): Cursor = MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply { newRow().apply { add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID) add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY) add(Root.COLUMN_ICON, R.mipmap.ic_launcher) add(Root.COLUMN_TITLE, context!!.getString(R.string.bett_box)) add(Root.COLUMN_SUMMARY, "Data") add(Root.COLUMN_DOCUMENT_ID, "/") } } override fun queryChildDocuments( parentDocumentId: String, projection: Array?, sortOrder: String? ): Cursor { val result = MatrixCursor(resolveDocumentProjection(projection)) val parentFile = (if (parentDocumentId == "/") context?.filesDir else File(parentDocumentId)) ?: throw FileNotFoundException("Parent directory not found") parentFile.listFiles()?.forEach { includeFile(result, it) } return result } override fun queryDocument(documentId: String, projection: Array?): Cursor = MatrixCursor(resolveDocumentProjection(projection)).apply { includeFile(this, File(documentId)) } override fun openDocument( documentId: String, mode: String, signal: CancellationSignal? ): ParcelFileDescriptor = ParcelFileDescriptor.open(File(documentId), ParcelFileDescriptor.parseMode(mode)) private fun includeFile(result: MatrixCursor, file: File) { result.newRow().apply { add(Document.COLUMN_DOCUMENT_ID, file.absolutePath) add(Document.COLUMN_DISPLAY_NAME, file.name) add(Document.COLUMN_SIZE, file.length()) add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_DELETE) add(Document.COLUMN_MIME_TYPE, getDocumentType(file)) } } private fun getDocumentType(file: File): String = if (file.isDirectory) Document.MIME_TYPE_DIR else "application/octet-stream" private fun resolveDocumentProjection(projection: Array?): Array = projection ?: DEFAULT_DOCUMENT_COLUMNS } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/GlobalState.kt ================================================ package com.appshub.bettbox import android.os.SystemClock import com.appshub.bettbox.plugins.AppPlugin import com.appshub.bettbox.plugins.ServicePlugin import com.appshub.bettbox.plugins.TilePlugin import com.appshub.bettbox.plugins.VpnPlugin import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.plugins.GeneratedPluginRegistrant import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock enum class RunState { START, PENDING, STOP } object GlobalState { val runLock = ReentrantLock() private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) const val NOTIFICATION_CHANNEL = "Bettbox" const val NOTIFICATION_ID = 1 private const val TOGGLE_DEBOUNCE_MS = 1000L private const val PENDING_TIMEOUT_MS = 5000L private const val STOP_LOCK_TIMEOUT_MS = 5000L @Volatile private var lastToggleAt = 0L @Volatile var currentRunState: RunState = RunState.STOP private set private val _runState = MutableStateFlow(RunState.STOP) val runState = _runState.asStateFlow() private var pendingTimeoutJob: Job? = null var flutterEngine: FlutterEngine? = null private var serviceEngine: FlutterEngine? = null @Volatile var isSmartStopped = false @Volatile var isStopping = false fun updateRunState(newState: RunState) { if (newState != RunState.PENDING) { pendingTimeoutJob?.cancel() pendingTimeoutJob = null } currentRunState = newState _runState.value = newState } private fun startPendingTimeout() { pendingTimeoutJob?.cancel() pendingTimeoutJob = scope.launch { delay(PENDING_TIMEOUT_MS) if (currentRunState == RunState.PENDING) { android.util.Log.w("GlobalState", "PENDING state timeout, resetting to STOP") updateRunState(RunState.STOP) } } } fun updateIsStopping(value: Boolean) { isStopping = value runCatching { val ts = if (value) System.currentTimeMillis() else 0L BettboxApplication.getAppContext() .getSharedPreferences("vpn_state", android.content.Context.MODE_PRIVATE) .edit() .putLong("stop_lock_ts", ts) .apply() } } fun isCurrentlyStopping(): Boolean { if (isStopping) return true return runCatching { val sp = BettboxApplication.getAppContext() .getSharedPreferences("vpn_state", android.content.Context.MODE_PRIVATE) val ts = sp.getLong("stop_lock_ts", 0L) if (ts == 0L) return false val now = System.currentTimeMillis() if (now - ts > STOP_LOCK_TIMEOUT_MS) { sp.edit().remove("stop_lock_ts").apply() false } else { true } }.getOrDefault(false) } fun getCurrentAppPlugin(): AppPlugin? { val currentEngine = flutterEngine ?: serviceEngine return currentEngine?.plugins?.get(AppPlugin::class.java) as? AppPlugin } fun syncStatus() { val status = VpnPlugin.getStatus() updateRunState(if (status) RunState.START else RunState.STOP) } suspend fun getText(text: String): String = getCurrentAppPlugin()?.getText(text) ?: "" fun getCurrentTilePlugin(): TilePlugin? { val currentEngine = flutterEngine ?: serviceEngine return currentEngine?.plugins?.get(TilePlugin::class.java) as? TilePlugin } fun getCurrentVPNPlugin(): VpnPlugin? { return serviceEngine?.plugins?.get(VpnPlugin::class.java) as? VpnPlugin } fun handleToggle() { if (!acquireToggleSlot()) return if (!handleStart(skipDebounce = true)) { handleStop(skipDebounce = true) } } fun handleStart(skipDebounce: Boolean = false): Boolean { if (!skipDebounce && !acquireToggleSlot()) return false if (currentRunState != RunState.STOP) return false updateRunState(RunState.PENDING) startPendingTimeout() runLock.withLock { getCurrentTilePlugin()?.handleStart() ?: initServiceEngine() } return true } fun handleStop(skipDebounce: Boolean = false) { if (!skipDebounce && !acquireToggleSlot()) return if (currentRunState != RunState.START) return updateRunState(RunState.PENDING) startPendingTimeout() runLock.withLock { getCurrentTilePlugin()?.handleStop() } } private fun acquireToggleSlot(): Boolean { val now = SystemClock.elapsedRealtime() synchronized(this) { if (now - lastToggleAt < TOGGLE_DEBOUNCE_MS) return false lastToggleAt = now return true } } fun handleTryDestroy() { if (flutterEngine == null) destroyServiceEngine() } fun destroyServiceEngine() { runLock.withLock { serviceEngine?.destroy() serviceEngine = null } } fun initServiceEngine(flags: List? = null) { runLock.withLock { if (serviceEngine != null) return serviceEngine = FlutterEngine(BettboxApplication.getAppContext()).apply { plugins.add(VpnPlugin) plugins.add(AppPlugin()) plugins.add(TilePlugin()) plugins.add(ServicePlugin()) GeneratedPluginRegistrant.registerWith(this) } val vpnService = DartExecutor.DartEntrypoint( FlutterInjector.instance().flutterLoader().findAppBundlePath(), "_service" ) val defaultArgs = if (flutterEngine == null && !isCurrentlyStopping()) listOf("quick") else null val args = flags ?: defaultArgs serviceEngine?.dartExecutor?.executeDartEntrypoint(vpnService, args) } } fun isServiceEngineRunning(): Boolean = serviceEngine != null fun reconnectIpc() { (serviceEngine?.plugins?.get(TilePlugin::class.java) as? TilePlugin)?.handleReconnectIpc() } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/MainActivity.kt ================================================ package com.appshub.bettbox import android.content.Context import com.appshub.bettbox.plugins.AppPlugin import com.appshub.bettbox.plugins.ServicePlugin import com.appshub.bettbox.plugins.TilePlugin import com.appshub.bettbox.plugins.VpnPlugin import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.plugins.GeneratedPluginRegistrant class MainActivity : FlutterActivity() { companion object { private const val MAIN_ENGINE_ID = "bettbox_main_engine" } override fun provideFlutterEngine(context: Context): FlutterEngine { val engineCache = FlutterEngineCache.getInstance() return engineCache.get(MAIN_ENGINE_ID) ?: createAndCacheEngine(context, engineCache) } private fun createAndCacheEngine(context: Context, cache: FlutterEngineCache) = FlutterEngine(context.applicationContext).apply { GeneratedPluginRegistrant.registerWith(this) dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) cache.put(MAIN_ENGINE_ID, this) GlobalState.flutterEngine = this } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) listOf(VpnPlugin, AppPlugin(), ServicePlugin(), TilePlugin()).forEach { plugin -> if (flutterEngine.plugins.get(plugin.javaClass) == null) { flutterEngine.plugins.add(plugin) } } GlobalState.flutterEngine = flutterEngine } override fun shouldDestroyEngineWithHost(): Boolean = false } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/TempActivity.kt ================================================ package com.appshub.bettbox import android.app.Activity import android.os.Bundle import com.appshub.bettbox.extensions.wrapAction class TempActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) when (intent.action) { wrapAction("START") -> GlobalState.handleStart() wrapAction("STOP") -> GlobalState.handleStop() wrapAction("CHANGE") -> GlobalState.handleToggle() } finishAndRemoveTask() } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/extensions/Ext.kt ================================================ package com.appshub.bettbox.extensions import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.Network import android.os.Build import android.system.OsConstants.IPPROTO_TCP import android.system.OsConstants.IPPROTO_UDP import android.util.Base64 import androidx.core.graphics.drawable.toBitmap import com.appshub.bettbox.TempActivity import com.appshub.bettbox.models.CIDR import com.appshub.bettbox.models.Metadata import com.appshub.bettbox.models.VpnOptions import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress import java.util.concurrent.locks.ReentrantLock import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine suspend fun Drawable.getBase64(maxSizePx: Int = 128): String = withContext(Dispatchers.IO) { val defaultSize = 96 val intrinsicWidth = if (intrinsicWidth > 0) intrinsicWidth else defaultSize val intrinsicHeight = if (intrinsicHeight > 0) intrinsicHeight else defaultSize val maxDim = maxOf(intrinsicWidth, intrinsicHeight) val targetSize = minOf(maxDim, maxSizePx) val scale = targetSize.toFloat() / maxDim.toFloat() val targetWidth = (intrinsicWidth * scale).toInt().coerceAtLeast(1) val targetHeight = (intrinsicHeight * scale).toInt().coerceAtLeast(1) val bitmap = toBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888) ByteArrayOutputStream().use { byteArrayOutputStream -> bitmap.compress(Bitmap.CompressFormat.PNG, 80, byteArrayOutputStream) Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP) } } fun Metadata.getProtocol(): Int? = when { network.startsWith("tcp") -> IPPROTO_TCP network.startsWith("udp") -> IPPROTO_UDP else -> null } fun VpnOptions.getIpv4RouteAddress(): List = routeAddress.filter { it.isIpv4() }.map { it.toCIDR() } fun VpnOptions.getIpv6RouteAddress(): List = routeAddress.filter { it.isIpv6() }.map { it.toCIDR() } fun String.isIpv4(): Boolean { val parts = split("/") require(parts.size == 2) { "Invalid CIDR format" } val ip = parts[0] return ip.contains('.') && !ip.contains(':') } fun String.isIpv6(): Boolean { val parts = split("/") require(parts.size == 2) { "Invalid CIDR format" } val ip = parts[0] return ip.contains(':') && !ip.contains('.') } fun String.toCIDR(): CIDR { val parts = split("/") require(parts.size == 2) { "Invalid CIDR format" } val ipAddress = parts[0] val prefixLength = parts[1].toIntOrNull() ?: throw IllegalArgumentException("Invalid prefix length") val address = InetAddress.getByName(ipAddress) val maxPrefix = if (address.address.size == 4) 32 else 128 require(prefixLength in 0..maxPrefix) { "Invalid prefix length for IP version" } return CIDR(address, prefixLength) } fun ConnectivityManager.resolveDns(network: Network?): List = getLinkProperties(network)?.dnsServers?.map { it.asSocketAddressText(53) } ?: emptyList() fun InetAddress.asSocketAddressText(port: Int): String = when (this) { is Inet6Address -> "[${numericToTextFormat(this)}]:$port" is Inet4Address -> "$hostAddress:$port" else -> throw IllegalArgumentException("Unsupported Inet type ${javaClass}") } fun Context.wrapAction(action: String): String = "${packageName}.action.$action" fun Context.getActionIntent(action: String): Intent = Intent(this, TempActivity::class.java).apply { this.action = wrapAction(action) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) } fun Context.getActionPendingIntent(action: String): PendingIntent { val flags = if (Build.VERSION.SDK_INT >= 31) { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT } return PendingIntent.getActivity(this, 0, getActionIntent(action), flags) } private fun numericToTextFormat(address: Inet6Address): String = buildString(39) { val src = address.address for (i in 0 until 8) { append(Integer.toHexString(src[i shl 1].toInt() shl 8 and 0xff00 or (src[(i shl 1) + 1].toInt() and 0xff))) if (i < 7) append(":") } if (address.scopeId > 0) { append("%") append(address.scopeId) } } suspend fun MethodChannel.awaitResult(method: String, arguments: Any? = null): T? = withContext(Dispatchers.Main) { suspendCancellableCoroutine { continuation -> invokeMethod(method, arguments, object : MethodChannel.Result { @Suppress("UNCHECKED_CAST") override fun success(result: Any?) { if (continuation.isActive) continuation.resume(result as? T) } override fun error(code: String, message: String?, details: Any?) { if (continuation.isActive) continuation.resume(null) } override fun notImplemented() { if (continuation.isActive) continuation.resume(null) } }) } } fun ReentrantLock.safeLock() { if (!isHeldByCurrentThread) lock() } fun ReentrantLock.safeUnlock() { if (isHeldByCurrentThread) unlock() } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/models/Package.kt ================================================ package com.appshub.bettbox.models data class Package( val packageName: String, val label: String, val system: Boolean, val internet: Boolean, val lastUpdateTime: Long, ) ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/models/Process.kt ================================================ package com.appshub.bettbox.models data class Process( val id: String, val metadata: Metadata, ) data class Metadata( val network: String, val sourceIP: String, val sourcePort: Int, val destinationIP: String, val destinationPort: Int, val host: String ) ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/models/Props.kt ================================================ package com.appshub.bettbox.models import java.net.InetAddress enum class AccessControlMode { acceptSelected, rejectSelected, } data class AccessControl( val enable: Boolean, val mode: AccessControlMode, val acceptList: List, val rejectList: List, ) data class CIDR(val address: InetAddress, val prefixLength: Int) data class VpnOptions( val enable: Boolean, val port: Int, val accessControl: AccessControl, val allowBypass: Boolean, val systemProxy: Boolean, val bypassDomain: List, val routeAddress: List, val routeMode: String = "config", val ipv4Address: String, val ipv6Address: String, val dnsServerAddress: String, val dozeSuspend: Boolean = false, val mtu: Int = 4064, ) data class StartForegroundParams( val title: String, val content: String, ) ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/modules/SuspendModule.kt ================================================ package com.appshub.bettbox.modules import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.PowerManager import android.util.Log import androidx.core.content.getSystemService import com.appshub.bettbox.core.Core class SuspendModule(private val context: Context) { companion object { private const val TAG = "SuspendModule" } private var isInstalled = false private var isSuspended = false private val powerManager: PowerManager? by lazy { context.getSystemService() } private val isScreenOn: Boolean get() = powerManager?.isInteractive ?: true private val isDeviceIdleMode: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && powerManager?.isDeviceIdleMode == true private val shouldSuspend: Boolean get() = !isScreenOn && isDeviceIdleMode private fun updateSuspendState() { val shouldSuspendNow = shouldSuspend Log.d(TAG, "updateSuspendState - shouldSuspend: $shouldSuspendNow, isSuspended: $isSuspended") when { shouldSuspendNow && !isSuspended -> { Log.i(TAG, "Entering Doze - Suspending core") Core.suspended(true) isSuspended = true } !shouldSuspendNow && isSuspended -> { Log.i(TAG, "Exiting Doze - Resuming core") Core.suspended(false) isSuspended = false } } } private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { intent?.action?.let { Log.d(TAG, "Received $it") updateSuspendState() } } } fun install() { if (isInstalled) return isInstalled = true isSuspended = false val filter = IntentFilter().apply { addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_SCREEN_OFF) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED) } } context.registerReceiver(receiver, filter) updateSuspendState() Log.i(TAG, "SuspendModule installed - SDK: ${Build.VERSION.SDK_INT}") } fun uninstall() { if (!isInstalled) return isInstalled = false runCatching { context.unregisterReceiver(receiver) if (isSuspended) { Log.i(TAG, "Uninstalling - Resume from suspend") Core.suspended(false) isSuspended = false } }.onFailure { Log.w(TAG, "Failed to unregister receiver: ${it.message}") } Log.i(TAG, "SuspendModule uninstalled") } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/plugins/AppPlugin.kt ================================================ package com.appshub.bettbox.plugins import android.Manifest import android.app.Activity import android.app.ActivityManager import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.ComponentInfo import android.content.pm.PackageManager import android.net.VpnService import android.os.Build import android.provider.Settings import android.util.Base64 import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.getSystemService import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import com.appshub.bettbox.BettboxApplication import com.appshub.bettbox.GlobalState import com.appshub.bettbox.R import com.appshub.bettbox.extensions.awaitResult import com.appshub.bettbox.extensions.getActionIntent import com.appshub.bettbox.models.Package import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.Result import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.lang.ref.WeakReference import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.concurrent.ConcurrentHashMap import android.content.res.Configuration import android.net.Uri import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.Drawable class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { private var activityRef: WeakReference? = null private var cachedTaskId: Int? = null private lateinit var channel: MethodChannel private lateinit var scope: CoroutineScope private var vpnCallBack: (() -> Unit)? = null private val packages = mutableListOf() private val chinaPackageCache = ConcurrentHashMap() private val iconCacheDir by lazy { File(BettboxApplication.getAppContext().cacheDir, "app_icons").apply { if (!exists()) mkdirs() } } companion object { private const val ICON_SIZE_DP = 48 private const val VPN_PERMISSION_REQUEST_CODE = 1001 private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002 private const val CACHE_MAX_FILES = 500 private const val PNG_MAGIC_SIZE = 8 private val SKIP_PREFIX_LIST = listOf( "com.google", "com.android.chrome", "com.android.vending", "com.facebook", "com.instagram", "com.whatsapp", "com.twitter", "com.linkedin", "com.snapchat", "com.amazon", "com.microsoft", "com.apple", "com.dropbox", "com.mozilla", "com.brave", "com.duckduckgo", "com.vivaldi", "com.kiwibrowser", "org.torproject.torbrowser", "com.opera.browser", "com.lemon.browser", "net.waterfox", "ch.protonmail", "org.thoughtcrime.securesms", "org.telegram", "com.surfshark", "com.netflix", "com.spotify", "tv.twitch", "com.hulu", "com.disney", "com.hbo", "com.primevideo", "com.zhiliaoapp.musically", "com.nytimes", "bbc.mobile", "com.wsj", "com.bloomberg", "com.medium", "com.quora", "com.github", "io.github", "com.slack", "com.notion", "us.zoom", "com.discord", "com.reddit", "com.pinterest", "com.tumblr", "jp.naver.line", "com.skype", "com.box", "org.wikipedia", "com.gitlab", "com.openai", "com.valvesoftware", "com.roblox", "com.ea.gp", "com.ubisoft", "com.sogou.activity.src", "com.qihoo.browser", "com.qihoo.haosou", "com.liebao", "com.mx.browser", "com.browser2345", "com.ijinshan.browser", "com.quark.browser", "com.ylmf.androidclient", "mark.via", "com.xbrowser.play", "com.mycompany.app.soulbrowser", "com.hshentong.alook", "info.bmmk.mbrowser", "com.rainsee.browser", "com.liuzh.browser", "com.yuzhe.browser", "org.easyweb.browser", "any.browser", "us.spotco.fennec_dos", "app.grapheneos.vanadium", "org.ironfoxoss", "com.samsung.android.app.sbrowser", "com.mi.global.browser", "com.android.browser", "com.huawei.browser", "com.hihonor.browser", "com.heytap.browser", "com.coloros.browser", "com.oppo.browser", "com.vivo.browser", "com.bbk.browser", "com.meizu.browser", "com.meizu.mbrowser", "com.lenovo.browser", "com.zte.browser", "com.gionee.browser" ) private val CHINA_APP_PREFIX_LIST = listOf( "com.tencent", "com.alibaba", "com.ali", "com.alipay", "com.taobao", "com.baidu", "com.iqiyi", "com.bytedance", "com.ss.android", "com.kuaishou", "com.smile.gifmaker", "com.xunmeng", "com.pinduoduo", "com.sankuai", "com.meituan", "com.jingdong", "com.jd", "tv.danmaku", "com.sina", "com.weibo", "com.sohu", "com.netease", "com.zhihu", "com.xingin", "com.huawei", "com.xiaomi", "com.miui", "com.oppo", "com.coloros", "com.oplus", "andes.oplus", "com.vivo", "com.bbk", "com.iqoo", "com.meizu", "com.flyme", "com.gionee", "cn.nubia", "com.zte", "com.lenovo", "com.oneplus", "com.qihoo", "com.360", "com.ijiami", "com.bangcle", "com.secneo", "com.kiwisec", "com.stub", "com.wrapper", "cn.securitystack", "com.mogosec", "com.secoen", "com.secshell", "com.umeng", "com.igexin", "cn.jpush", "cn.jiguang", "com.bugly", "com.mob", "cn.wps", "com.kingsoft", "com.xunlei", "com.unionpay", "com.cainiao", "com.sf", "com.sdu", "com.xiaojukeji", "com.autonavi", "com.amap", "com.chinamobile", "com.chinaunicom", "com.chinatelecom", "com.icbc", "com.ccb", "com.cmbchina", "com.mx", "com.qq", "app.eleven.com.fastfiletransfer", "org.localsend.localsend_app" ) private val CHINA_APP_REGEX by lazy { ("(" + CHINA_APP_PREFIX_LIST.joinToString("|").replace(".", "\\.") + ").*").toRegex() } } private var isBlockNotification = false private var isActivityAttached = false override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel.setMethodCallHandler(this) scope.launch(Dispatchers.IO) { cleanIconCache() } } private fun initShortcuts(label: String) { val iconRes = if (isSystemInDarkMode()) R.mipmap.ic_launcher_round else R.mipmap.ic_launcher_round_light val shortcut = ShortcutInfoCompat.Builder(BettboxApplication.getAppContext(), "toggle") .setShortLabel(label) .setIcon(IconCompat.createWithResource(BettboxApplication.getAppContext(), iconRes)) .setIntent(BettboxApplication.getAppContext().getActionIntent("CHANGE")) .build() ShortcutManagerCompat.setDynamicShortcuts(BettboxApplication.getAppContext(), listOf(shortcut)) } private fun isSystemInDarkMode(): Boolean { val nightModeFlags = BettboxApplication.getAppContext().resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK return nightModeFlags == Configuration.UI_MODE_NIGHT_YES } private fun isAndroidTV(): Boolean { val uiMode = BettboxApplication.getAppContext().resources.configuration.uiMode return (uiMode and Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) scope.cancel() } private fun tip(message: String?) { if (GlobalState.flutterEngine == null) { Toast.makeText(BettboxApplication.getAppContext(), message, Toast.LENGTH_LONG).show() } } override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "moveTaskToBack" -> { activityRef?.get()?.moveTaskToBack(true) result.success(true) } "updateExcludeFromRecents" -> { updateExcludeFromRecents(call.argument("value")) result.success(true) } "initShortcuts" -> { initShortcuts(call.arguments as String) result.success(true) } "getPackages" -> scope.launch { val forceRefresh = call.argument("forceRefresh") ?: false runCatching { result.success(getPackagesToList(forceRefresh)) } .onFailure { result.error("GET_PACKAGES_FAILED", it.message, null) } } "getChinaPackageNames" -> scope.launch { runCatching { result.success(getChinaPackageNamesList()) } .onFailure { result.error("GET_CHINA_PACKAGES_FAILED", it.message, null) } } "getPackageIcon" -> scope.launch { val packageName = call.argument("packageName") val forceRefresh = call.argument("forceRefresh") ?: false val icon = runCatching { packageName?.let { getPackageIconBytes(it, forceRefresh) } ?: getDefaultIconBytes() }.getOrNull() ?: getDefaultIconBytes() result.success(icon) } "tip" -> { tip(call.argument("message")) result.success(true) } "openFile" -> { openFile(call.argument("path")!!) result.success(true) } "getSelfLastUpdateTime" -> { result.success(getSelfLastUpdateTime()) } "isIgnoringBatteryOptimizations" -> { result.success(isIgnoringBatteryOptimizations()) } "requestIgnoreBatteryOptimizations" -> { requestIgnoreBatteryOptimizations() result.success(true) } "setLauncherIcon" -> { setLauncherIcon(call.argument("useLightIcon") ?: false) result.success(true) } "hasPackageListPermission" -> { result.success(hasPackageListPermission()) } "requestPackageListPermission" -> { requestPackageListPermission() result.success(true) } "hasCameraPermission" -> { result.success(hasCameraPermission()) } "openAppSettings" -> { openAppSettings() result.success(true) } "isAndroidTV" -> { result.success(isAndroidTV()) } else -> result.notImplemented() } } private fun openFile(path: String) { val context = BettboxApplication.getAppContext() val file = File(path) val uri = FileProvider.getUriForFile( context, "${context.packageName}.fileProvider", file ) val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(uri, "text/plain") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } runCatching { context.startActivity(intent) } } private fun updateExcludeFromRecents(value: Boolean?) { val am = BettboxApplication.getAppContext().getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager val task = am?.appTasks?.firstOrNull { task -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { task.taskInfo.taskId == activityRef?.get()?.taskId } else { @Suppress("DEPRECATION") task.taskInfo.id == activityRef?.get()?.taskId } } when (value) { true -> task?.setExcludeFromRecents(value) false -> task?.setExcludeFromRecents(value) null -> task?.setExcludeFromRecents(false) } } private fun getIconSizePx(): Int { val density = BettboxApplication.getAppContext().resources.displayMetrics.density return (ICON_SIZE_DP * density).toInt().coerceAtLeast(1) } private fun drawableToPngBytes(drawable: Drawable, sizePx: Int): ByteArray { val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, sizePx, sizePx) drawable.draw(canvas) return java.io.ByteArrayOutputStream().use { outputStream -> bitmap.compress(Bitmap.CompressFormat.PNG, 80, outputStream) bitmap.recycle() outputStream.toByteArray() } } private fun getDefaultIconBytes(): ByteArray? = runCatching { drawableToPngBytes(BettboxApplication.getAppContext().packageManager.defaultActivityIcon, getIconSizePx()) }.getOrNull() private fun isPngBytes(bytes: ByteArray): Boolean { if (bytes.size < PNG_MAGIC_SIZE) return false val magic = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A) return bytes.take(PNG_MAGIC_SIZE).toByteArray().contentEquals(magic) } private suspend fun getPackageIconBytes(packageName: String, forceRefresh: Boolean = false): ByteArray? = withContext(Dispatchers.IO) { val pm = BettboxApplication.getAppContext().packageManager ?: return@withContext null runCatching { val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) } else { pm.getPackageInfo(packageName, 0) } val lastUpdateTime = packageInfo?.lastUpdateTime ?: 0L val cacheKey = "${packageName}_${lastUpdateTime}" val cacheFile = File(iconCacheDir, cacheKey) if (forceRefresh && cacheFile.exists() && (System.currentTimeMillis() - lastUpdateTime) < 24 * 60 * 60 * 1000) { cacheFile.delete() } if (cacheFile.exists() && cacheFile.length() > 0) { val cachedBytes = cacheFile.readBytes() if (isPngBytes(cachedBytes)) return@withContext cachedBytes cacheFile.delete() } iconCacheDir.listFiles()?.forEach { if (it.name.startsWith("${packageName}_") && it.name != cacheKey) it.delete() } pm.getApplicationIcon(packageName)?.let { drawable -> val bytes = drawableToPngBytes(drawable, getIconSizePx()) runCatching { cacheFile.writeBytes(bytes) } return@withContext bytes } } null } private suspend fun getPackages(forceRefresh: Boolean = false): List = withContext(Dispatchers.IO) { if (forceRefresh) packages.clear() if (packages.isNotEmpty()) return@withContext packages val pm = BettboxApplication.getAppContext().packageManager ?: return@withContext emptyList() val selfPackageName = BettboxApplication.getAppContext().packageName packages.addAll(pm.getInstalledApplications(PackageManager.GET_META_DATA).mapNotNull { appInfo -> val packageName = appInfo.packageName ?: return@mapNotNull null if (packageName == selfPackageName) return@mapNotNull null val label = runCatching { appInfo.loadLabel(pm).toString() }.getOrDefault(packageName) val system = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0 val internet = runCatching { pm.checkPermission(Manifest.permission.INTERNET, packageName) == PackageManager.PERMISSION_GRANTED }.getOrDefault(false) val lastUpdateTime = appInfo.sourceDir?.let { File(it).lastModified() } ?: 0L Package(packageName, label, system, internet, lastUpdateTime) }) packages } private suspend fun getPackagesToList(forceRefresh: Boolean = false): List> = getPackages(forceRefresh).map { mapOf("packageName" to it.packageName, "label" to it.label, "system" to it.system, "internet" to it.internet, "lastUpdateTime" to it.lastUpdateTime) } private suspend fun getChinaPackageNamesList(): List = getPackages().map { it.packageName }.filter { isChinaPackage(it) } private fun cleanIconCache() { runCatching { iconCacheDir.listFiles()?.takeIf { it.size > CACHE_MAX_FILES }?.let { files -> files.sortedBy { it.lastModified() }.take(files.size - CACHE_MAX_FILES).forEach { it.delete() } } } } fun requestVpnPermission(callBack: () -> Unit) { vpnCallBack = callBack val intent = VpnService.prepare(BettboxApplication.getAppContext()) if (intent != null) { activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) return } vpnCallBack?.invoke() } fun requestNotificationsPermission() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return if (isBlockNotification || activityRef?.get() == null) return if (ContextCompat.checkSelfPermission(BettboxApplication.getAppContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) return activityRef?.get()?.let { ActivityCompat.requestPermissions(it, arrayOf(Manifest.permission.POST_NOTIFICATIONS), NOTIFICATION_PERMISSION_REQUEST_CODE) } } suspend fun getText(text: String): String? = withContext(Dispatchers.Default) { channel.awaitResult("getText", text) } private fun isChinaPackage(packageName: String): Boolean = chinaPackageCache.getOrPut(packageName) { isChinaPackageInternal(packageName) } private fun isChinaPackageInternal(packageName: String): Boolean { val context = BettboxApplication.getAppContext() val pm = context.packageManager ?: return false if (SKIP_PREFIX_LIST.any { packageName == it || packageName.startsWith("$it.") }) return false if (packageName.matches(CHINA_APP_REGEX)) return true if (isChinaCertificate(packageName, pm)) return true val flags = PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS runCatching { val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong())) } else { pm.getPackageInfo(packageName, flags) } val components = mutableListOf().apply { packageInfo.services?.let { addAll(it) } packageInfo.activities?.let { addAll(it) } packageInfo.receivers?.let { addAll(it) } packageInfo.providers?.let { addAll(it) } } if (components.any { it.name.matches(CHINA_APP_REGEX) }) return true } return false } private fun isChinaCertificate(packageName: String, pm: PackageManager): Boolean = runCatching { val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES) } else { @Suppress("DEPRECATION") pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) } val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { packageInfo.signingInfo?.apkContentsSigners } else { @Suppress("DEPRECATION") packageInfo.signatures } signatures?.any { signature -> val cert = CertificateFactory.getInstance("X.509") .generateCertificate(java.io.ByteArrayInputStream(signature.toByteArray())) cert is X509Certificate && (cert.subjectDN.name.contains("C=CN", ignoreCase = true) || cert.subjectDN.name.contains("C=86", ignoreCase = true)) } == true }.getOrDefault(false) override fun onAttachedToActivity(binding: ActivityPluginBinding) { activityRef = WeakReference(binding.activity) if (!isActivityAttached) { isActivityAttached = true binding.addActivityResultListener(::onActivityResult) binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener) } } override fun onDetachedFromActivityForConfigChanges() { activityRef = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activityRef = WeakReference(binding.activity) } override fun onDetachedFromActivity() { channel.invokeMethod("exit", null) activityRef = null cachedTaskId = null isActivityAttached = false } private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (!isActivityAttached) return false if (requestCode == VPN_PERMISSION_REQUEST_CODE && resultCode == FlutterActivity.RESULT_OK) { GlobalState.initServiceEngine() vpnCallBack?.invoke() } return true } private fun onRequestPermissionsResultListener(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean { if (!isActivityAttached) return false if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) isBlockNotification = true return true } private fun isIgnoringBatteryOptimizations(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val powerManager = BettboxApplication.getAppContext().getSystemService(android.content.Context.POWER_SERVICE) as? android.os.PowerManager powerManager?.isIgnoringBatteryOptimizations(BettboxApplication.getAppContext().packageName) ?: false } else true private fun requestIgnoreBatteryOptimizations() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { data = Uri.parse("package:${BettboxApplication.getAppContext().packageName}") addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } runCatching { BettboxApplication.getAppContext().startActivity(intent) } .onFailure { runCatching { BettboxApplication.getAppContext().startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }) } } } private fun setLauncherIcon(useLightIcon: Boolean) { val context = BettboxApplication.getAppContext() val pm = context.packageManager val packageName = context.packageName val defaultComponent = android.content.ComponentName(packageName, "com.appshub.bettbox.MainActivity") val lightComponent = android.content.ComponentName(packageName, "com.appshub.bettbox.MainActivityLight") if (useLightIcon) { pm.setComponentEnabledSetting(lightComponent, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) pm.setComponentEnabledSetting(defaultComponent, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) } else { pm.setComponentEnabledSetting(defaultComponent, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) pm.setComponentEnabledSetting(lightComponent, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) } VpnPlugin.updateNotificationIcon() } private fun hasPackageListPermission(): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return true val context = BettboxApplication.getAppContext() val pm = context.packageManager return arrayOf("com.android.settings", "com.android.systemui").any { pkg -> runCatching { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm.getPackageInfo(pkg, PackageManager.PackageInfoFlags.of(0)) } else { pm.getPackageInfo(pkg, 0) } true }.getOrDefault(false) } } private fun requestPackageListPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) openAppSettings() } private fun hasCameraPermission(): Boolean = ContextCompat.checkSelfPermission(BettboxApplication.getAppContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED private fun openAppSettings() { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.parse("package:${BettboxApplication.getAppContext().packageName}") } activityRef?.get()?.startActivity(intent) ?: run { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) BettboxApplication.getAppContext().startActivity(intent) } } private fun getSelfLastUpdateTime(): Long { val context = BettboxApplication.getAppContext() val pm = context.packageManager return runCatching { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm?.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(0)) } else { pm?.getPackageInfo(context.packageName, 0) }?.lastUpdateTime }.getOrDefault(0L) ?: 0L } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/plugins/ServicePlugin.kt ================================================ package com.appshub.bettbox.plugins import android.os.Handler import android.os.Looper import android.util.Log import com.appshub.bettbox.GlobalState import com.appshub.bettbox.RunState import com.appshub.bettbox.models.VpnOptions import com.google.gson.Gson import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.util.concurrent.CopyOnWriteArrayList class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private lateinit var channel: MethodChannel companion object { private val activeChannels = CopyOnWriteArrayList() private val mainHandler = Handler(Looper.getMainLooper()) private val gson = Gson() private const val TAG = "ServicePlugin" private fun notify(method: String) { mainHandler.post { activeChannels.forEach { ch -> runCatching { ch.invokeMethod(method, null) } .onFailure { Log.e(TAG, "$method notify error: ${it.message}") } } } } fun notifyNetworkChanged() = notify("networkChanged") fun notifyQuickResponse() = notify("quickResponse") fun notifyVpnStartFailed() = notify("vpnStartFailed") fun notifyRunStateChanged(state: RunState) { mainHandler.post { activeChannels.forEach { ch -> runCatching { ch.invokeMethod("runStateChanged", state.name) } .onFailure { Log.e(TAG, "runStateChanged notify error: ${it.message}") } } } } } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(binding.binaryMessenger, "service").apply { setMethodCallHandler(this@ServicePlugin) } activeChannels.add(channel) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) activeChannels.remove(channel) } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "startVpn" -> handleStartVpn(call, result) "stopVpn" -> { VpnPlugin.handleStop(force = true) result.success(true) } "smartStop" -> { VpnPlugin.handleSmartStop() result.success(true) } "smartResume" -> { val data = call.argument("data") val options = gson.fromJson(data, VpnOptions::class.java) VpnPlugin.handleSmartResume(options) result.success(true) } "setSmartStopped" -> { GlobalState.isSmartStopped = call.argument("value") ?: false result.success(true) } "isSmartStopped" -> result.success(GlobalState.isSmartStopped) "getLocalIpAddresses" -> result.success(VpnPlugin.getLocalIpAddresses()) "setQuickResponse" -> { VpnPlugin.setQuickResponse(call.argument("enabled") ?: false) result.success(true) } "init" -> { GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission() GlobalState.initServiceEngine() result.success(true) } "isServiceEngineRunning" -> result.success(GlobalState.isServiceEngineRunning()) "status" -> result.success(GlobalState.currentRunState == RunState.START) "reconnectIpc" -> { GlobalState.reconnectIpc() result.success(true) } "destroy" -> { GlobalState.destroyServiceEngine() result.success(true) } else -> result.notImplemented() } } private fun handleStartVpn(call: MethodCall, result: MethodChannel.Result) { val data = call.argument("data") if (data.isNullOrBlank() || data == "null") { result.error("INVALID_ARGUMENT", "options data is null", null) return } runCatching { gson.fromJson(data, VpnOptions::class.java) } .onSuccess { options -> VpnPlugin.handleStart(options) result.success(true) } .onFailure { result.error("PARSE_ERROR", it.message, null) } } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/plugins/TilePlugin.kt ================================================ package com.appshub.bettbox.plugins import android.os.Handler import android.os.Looper import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private var channel: MethodChannel? = null private val mainHandler = Handler(Looper.getMainLooper()) companion object { private const val TAG = "TilePlugin" } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(binding.binaryMessenger, "tile").apply { setMethodCallHandler(this@TilePlugin) } } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { runCatching { channel?.invokeMethod("detached", null) } .onFailure { Log.e(TAG, "Failed to invoke detached: ${it.message}") } channel?.setMethodCallHandler(null) channel = null } fun handleStart() = safeInvokeMethod("start") fun handleStop() = safeInvokeMethod("stop") fun handleReconnectIpc() = safeInvokeMethod("reconnectIpc") private fun safeInvokeMethod(method: String) { val ch = channel ?: return if (Looper.myLooper() == Looper.getMainLooper()) { runCatching { ch.invokeMethod(method, null) } .onFailure { Log.e(TAG, "Failed to invoke $method: ${it.message}") } } else { mainHandler.post { runCatching { ch.invokeMethod(method, null) } .onFailure { Log.e(TAG, "Failed to invoke $method: ${it.message}") } } } } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = result.notImplemented() } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/plugins/VpnPlugin.kt ================================================ package com.appshub.bettbox.plugins import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.os.Build import android.os.IBinder import androidx.core.content.getSystemService import com.appshub.bettbox.BettboxApplication import com.appshub.bettbox.GlobalState import com.appshub.bettbox.RunState import com.appshub.bettbox.core.Core import com.appshub.bettbox.extensions.awaitResult import com.appshub.bettbox.extensions.resolveDns import com.appshub.bettbox.models.StartForegroundParams import com.appshub.bettbox.models.VpnOptions import com.appshub.bettbox.modules.SuspendModule import com.appshub.bettbox.services.BaseServiceInterface import com.appshub.bettbox.services.BettboxService import com.appshub.bettbox.services.BettboxVpnService import com.google.gson.Gson import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import java.util.Collections import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import java.net.InetSocketAddress import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.withLock data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private var bettBoxService: BaseServiceInterface? = null private var options: VpnOptions? = null private var isBind = false private val isBinding = AtomicBoolean(false) private var job = SupervisorJob() private var scope = CoroutineScope(Dispatchers.Default + job as kotlin.coroutines.CoroutineContext) private var lastStartForegroundParams: StartForegroundParams? = null private val uidPageNameMap = ConcurrentHashMap() private var suspendModule: SuspendModule? = null private var quickResponseEnabled = false private var disconnectCount = 0 private var disconnectWindowStart = 0L private val disconnectWindowMs = 5000L private val maxDisconnectsInWindow = 3 private var lastNetworkType: Int? = null private var lastDns = "" val networks: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) private val connectivity by lazy { BettboxApplication.getAppContext().getSystemService() } private var bindTimeoutJob: Job? = null private val attachedMessengers = mutableSetOf() private val channelMap = ConcurrentHashMap() private val activeChannels = CopyOnWriteArrayList() private val networkCallbackRegistered = AtomicBoolean(false) private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { bindTimeoutJob?.cancel() bindTimeoutJob = null isBind = true isBinding.set(false) bettBoxService = when (service) { is BettboxVpnService.LocalBinder -> service.getService() is BettboxService.LocalBinder -> service.getService() else -> throw Exception("invalid binder") } handleStartService() } override fun onServiceDisconnected(arg: ComponentName) { isBind = false isBinding.set(false) bettBoxService = null if (GlobalState.currentRunState == RunState.START) { android.util.Log.w("VpnPlugin", "Service unexpectedly disconnected while running, syncing state") GlobalState.updateRunState(RunState.STOP) ServicePlugin.notifyVpnStartFailed() ServicePlugin.notifyRunStateChanged(RunState.STOP) } } } override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { val isFirstAttach = attachedMessengers.isEmpty() attachedMessengers.add(flutterPluginBinding.binaryMessenger) if (job.isCancelled) { job = SupervisorJob() scope = CoroutineScope(Dispatchers.Default + job as kotlin.coroutines.CoroutineContext) } val channel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn") channel.setMethodCallHandler(this) channelMap[flutterPluginBinding.binaryMessenger] = channel activeChannels.add(channel) if (isFirstAttach) { scope.launch { registerNetworkCallback() } } if (GlobalState.currentRunState == RunState.START && bettBoxService == null) { android.util.Log.d("VpnPlugin", "VPN is running but service connection lost, rebinding...") options?.let { bindService() } } } override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { attachedMessengers.remove(flutterPluginBinding.binaryMessenger) channelMap.remove(flutterPluginBinding.binaryMessenger)?.let { channel -> channel.setMethodCallHandler(null) activeChannels.remove(channel) } if (attachedMessengers.isEmpty()) { unRegisterNetworkCallback() job.cancel() } } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "start" -> { try { val data = call.argument("data") if (data == null) { result.error("INVALID_ARGUMENT", "data parameter is required", null) return } val vpnOptions = Gson().fromJson(data, VpnOptions::class.java) result.success(handleStart(vpnOptions)) } catch (e: Exception) { android.util.Log.e("VpnPlugin", "Failed to start VPN: ${e.message}") result.error("PARSE_ERROR", "Failed to parse VpnOptions: ${e.message}", null) } } "stop" -> { handleStop() result.success(true) } "getLocalIpAddresses" -> { result.success(getLocalIpAddresses()) } "setSmartStopped" -> { val value = call.argument("value") ?: false GlobalState.isSmartStopped = value result.success(true) } "isSmartStopped" -> { result.success(GlobalState.isSmartStopped) } "smartStop" -> { handleSmartStop() result.success(true) } "smartResume" -> { val data = call.argument("data") result.success(handleSmartResume(Gson().fromJson(data, VpnOptions::class.java))) } "setQuickResponse" -> { quickResponseEnabled = call.argument("enabled") ?: false result.success(true) } "status" -> { result.success(GlobalState.currentRunState == RunState.START) } else -> { result.notImplemented() } } } fun setQuickResponse(enabled: Boolean) { quickResponseEnabled = enabled } fun getLocalIpAddresses(): List = runCatching { networks.flatMap { network -> connectivity?.getLinkProperties(network) ?.linkAddresses ?.mapNotNull { it.address } ?.filter { !it.isLoopbackAddress && it.hostAddress?.contains(":") == false } ?.mapNotNull { it.hostAddress } ?: emptyList() } }.getOrElse { android.util.Log.e("VpnPlugin", "getLocalIpAddresses error: ${it.message}") emptyList() } fun handleStart(options: VpnOptions): Boolean { onUpdateNetwork() if (options.enable != this.options?.enable) { this.bettBoxService = null } this.options = options when (options.enable) { true -> handleStartVpn() false -> handleStartService() } return true } private fun handleStartVpn() { GlobalState.getCurrentAppPlugin()?.requestVpnPermission { handleStartService() } } fun requestGc() { invokeDart("gc") } fun onUpdateNetwork() { val dns = when { networks.isNotEmpty() -> { networks.flatMap { network -> connectivity?.resolveDns(network) ?: emptyList() }.toSet() } else -> { val cm = connectivity val activeNetwork = cm?.activeNetwork if (activeNetwork != null && cm != null) { cm.resolveDns(activeNetwork).toSet() } else { emptySet() } } }.let { dnsSet -> when { dnsSet.isNotEmpty() -> dnsSet.joinToString(",") else -> getAllNetworksDns() } } if (dns == lastDns) return lastDns = dns invokeDart("dnsChanged", dns) } private fun getAllNetworksDns(): String { return runCatching { connectivity?.allNetworks?.flatMap { network -> connectivity?.resolveDns(network) ?: emptyList() }?.filter { it.isNotBlank() }?.toSet()?.joinToString(",") ?: "" }.getOrElse { "" } } private val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { networks.add(network) onUpdateNetwork() handleNetworkChange() } override fun onLost(network: Network) { networks.remove(network) onUpdateNetwork() handleNetworkChange() } } private val request = NetworkRequest.Builder().apply { addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) }.build() private fun registerNetworkCallback() { if (!networkCallbackRegistered.compareAndSet(false, true)) return runCatching { networks.clear() connectivity?.registerNetworkCallback(request, callback) }.onFailure { networkCallbackRegistered.set(false) android.util.Log.e("VpnPlugin", "Failed to register network callback: ${it.message}") } } private fun unRegisterNetworkCallback() { if (!networkCallbackRegistered.compareAndSet(true, false)) return runCatching { connectivity?.unregisterNetworkCallback(callback) }.onFailure { android.util.Log.e("VpnPlugin", "Failed to unregister network callback: ${it.message}") }.also { networks.clear() onUpdateNetwork() } } private fun handleNetworkChange() { val currentNetworkType = getCurrentNetworkType() if (lastNetworkType == null) { lastNetworkType = currentNetworkType return } if (currentNetworkType != lastNetworkType) { lastNetworkType = currentNetworkType ServicePlugin.notifyNetworkChanged() if (!quickResponseEnabled) return if (GlobalState.currentRunState != RunState.START) return val now = System.currentTimeMillis() if (now - disconnectWindowStart > disconnectWindowMs) { disconnectWindowStart = now disconnectCount = 0 } if (disconnectCount < maxDisconnectsInWindow) { disconnectCount++ android.util.Log.d("VpnPlugin", "Quick Response: Network changed, closing connections ($disconnectCount/$maxDisconnectsInWindow)") invokeDart("closeConnections") } else { android.util.Log.d("VpnPlugin", "Quick Response: Disconnect limit reached, ignoring") } } } private fun invokeDart(method: String, arguments: Any? = null) { if (activeChannels.isEmpty()) return scope.launch { withContext(Dispatchers.Main) { activeChannels.forEach { channel -> runCatching { channel.invokeMethod(method, arguments) } .onFailure { android.util.Log.w("VpnPlugin", "invokeDart($method) failed: ${it.message}") } } } } } private fun getCurrentNetworkType(): Int { val activeNetwork = connectivity?.activeNetwork ?: return -1 val caps = connectivity?.getNetworkCapabilities(activeNetwork) ?: return -1 return when { caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 2 else -> 0 } } private suspend fun startForeground() { val shouldUpdate = GlobalState.runLock.withLock { GlobalState.currentRunState == RunState.START || GlobalState.isSmartStopped } if (!shouldUpdate) return try { bettBoxService?.startForeground() } catch (e: Exception) { android.util.Log.e("VpnPlugin", "startForeground error: ${e.message}") } } fun updateNotificationIcon() { scope.launch { runCatching { (bettBoxService as? BettboxService)?.resetNotificationBuilder() (bettBoxService as? BettboxVpnService)?.resetNotificationBuilder() bettBoxService?.startForeground() }.onFailure { android.util.Log.e("VpnPlugin", "updateNotificationIcon error: ${it.message}") } } } fun getStatus(): Boolean { return GlobalState.runLock.withLock { GlobalState.currentRunState == RunState.START && bettBoxService != null } } private fun handleStartService() { if (GlobalState.isCurrentlyStopping()) { android.util.Log.w("VpnPlugin", "VPN is in stopping state, ignore start request") return } if (bettBoxService == null) { bindService() return } scope.launch { try { val prepareIntent = try { android.net.VpnService.prepare(BettboxApplication.getAppContext()) } catch (e: Exception) { null } if (prepareIntent != null) { android.util.Log.w("VpnPlugin", "VPN permission required before start") GlobalState.updateRunState(RunState.STOP) withContext(Dispatchers.Main) { GlobalState.getCurrentAppPlugin()?.requestVpnPermission { handleStartService() } } return@launch } val currentOptions = options val startAllowed = GlobalState.runLock.withLock { if (GlobalState.currentRunState == RunState.START) { android.util.Log.d("VpnPlugin", "Service already running, refreshing notification") scope.launch { startForeground() } return@withLock false } if (currentOptions == null) { android.util.Log.e("VpnPlugin", "Start failed: options is null") GlobalState.updateRunState(RunState.STOP) return@withLock false } GlobalState.updateRunState(RunState.START) lastStartForegroundParams = null true } if (!startAllowed || currentOptions == null) return@launch performStartCore(currentOptions, retry = true, notifyOnFailure = true) } catch (e: Exception) { android.util.Log.e("VpnPlugin", "Fatal error in start flow: ${e.message}") GlobalState.updateRunState(RunState.STOP) } } } private suspend fun performStartCore( currentOptions: VpnOptions, retry: Boolean, notifyOnFailure: Boolean ) { var fd: Int? = 0 try { fd = bettBoxService?.start(currentOptions) } catch (e: Exception) { android.util.Log.e("VpnPlugin", "First start attempt failed: ${e.message}") } if (fd == null || (currentOptions.enable && fd <= 0)) { if (retry) { android.util.Log.w("VpnPlugin", "VPN establish failed, retrying...") delay(300) try { fd = bettBoxService?.start(currentOptions) } catch (e: Exception) { android.util.Log.e("VpnPlugin", "Retry start failed: ${e.message}") } } } if (fd == null || (currentOptions.enable && fd <= 0)) { android.util.Log.e("VpnPlugin", "VPN start failed after all attempts") GlobalState.runLock.withLock { GlobalState.updateRunState(RunState.STOP) } if (notifyOnFailure) { ServicePlugin.notifyVpnStartFailed() } return } val canStart = GlobalState.runLock.withLock { if (GlobalState.currentRunState != RunState.START) { bettBoxService?.stop() false } else true } if (!canStart) return com.appshub.bettbox.core.Core.startTun( fd = fd ?: 0, protect = this@VpnPlugin::protect, resolverProcess = this@VpnPlugin::resolverProcess, ) GlobalState.runLock.withLock { if (GlobalState.currentRunState != RunState.START) { Core.stopTun() return@withLock } scope.launch { startForeground() } if (currentOptions.dozeSuspend) { suspendModule?.uninstall() suspendModule = SuspendModule(BettboxApplication.getAppContext()) suspendModule?.install() } } onUpdateNetwork() } private fun protect(fd: Int): Boolean = runCatching { (bettBoxService as? BettboxVpnService)?.protect(fd) == true }.getOrElse { android.util.Log.e("VpnPlugin", "protect error: ${it.message}") false } private fun resolverProcess( protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int, ): String = runCatching { val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1 } else { uid } if (nextUid == -1) { return@runCatching "" } uidPageNameMap.getOrPut(nextUid) { BettboxApplication.getAppContext().packageManager?.getPackagesForUid(nextUid) ?.firstOrNull() ?: "" } }.getOrElse { android.util.Log.e("VpnPlugin", "resolverProcess error: ${it.message}") "" } fun handleStop(force: Boolean = false) { val serviceRef: BaseServiceInterface? val wasBound: Boolean val shouldForceStop: Boolean GlobalState.runLock.withLock { if (!force && GlobalState.currentRunState == RunState.STOP) return GlobalState.updateIsStopping(true) GlobalState.updateRunState(RunState.STOP) serviceRef = bettBoxService wasBound = isBind shouldForceStop = force || bettBoxService == null } suspendModule?.uninstall() suspendModule = null Core.stopTun() serviceRef?.stop() runCatching { if (wasBound) { BettboxApplication.getAppContext().unbindService(connection) isBind = false } bettBoxService = null }.onFailure { android.util.Log.e("VpnPlugin", "unbindService error: ${it.message}") } val context = BettboxApplication.getAppContext() if (shouldForceStop) { context.stopService(Intent(context, BettboxVpnService::class.java)) context.stopService(Intent(context, BettboxService::class.java)) } runCatching { context.getSystemService() ?.cancel(GlobalState.NOTIFICATION_ID) }.onFailure { android.util.Log.e("VpnPlugin", "cancel notification error: ${it.message}") } scope.launch { delay(300) GlobalState.updateIsStopping(false) delay(200) withContext(Dispatchers.Main) { GlobalState.handleTryDestroy() } } } fun handleSmartStop() { GlobalState.runLock.withLock { if (GlobalState.currentRunState == RunState.STOP) return GlobalState.updateRunState(RunState.STOP) GlobalState.isSmartStopped = true } suspendModule?.uninstall() suspendModule = null Core.stopTun() scope.launch { startForeground() } } fun handleSmartResume(options: VpnOptions): Boolean { scope.launch { val startAllowed = GlobalState.runLock.withLock { if (GlobalState.currentRunState == RunState.START) return@withLock false GlobalState.isSmartStopped = false this@VpnPlugin.options = options if (bettBoxService == null) { bindService() return@withLock false } GlobalState.updateRunState(RunState.START) lastStartForegroundParams = null true } if (!startAllowed) return@launch performStartCore(options, retry = false, notifyOnFailure = false) } return true } private fun bindService() { if (!isBinding.compareAndSet(false, true)) return bindTimeoutJob?.cancel() bindTimeoutJob = scope.launch { delay(10_000L) if (isBinding.compareAndSet(true, false)) { android.util.Log.w("VpnPlugin", "bindService timeout (10s), resetting bind state") GlobalState.runLock.withLock { if (GlobalState.currentRunState == RunState.PENDING) { GlobalState.updateRunState(RunState.STOP) } } } } try { if (isBind) { BettboxApplication.getAppContext().unbindService(connection) isBind = false } val intent = Intent( BettboxApplication.getAppContext(), if (options?.enable == true) BettboxVpnService::class.java else BettboxService::class.java ) val res = BettboxApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE) if (!res) { isBinding.set(false) bindTimeoutJob?.cancel() bindTimeoutJob = null android.util.Log.e("VpnPlugin", "bindService returned false (rejected by system)") } } catch (e: Exception) { isBinding.set(false) bindTimeoutJob?.cancel() bindTimeoutJob = null android.util.Log.e("VpnPlugin", "bindService error: ${e.message}") } } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/receivers/BootReceiver.kt ================================================ package com.appshub.bettbox.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.util.Log import com.appshub.bettbox.GlobalState class BootReceiver : BroadcastReceiver() { companion object { private const val TAG = "BootReceiver" private const val PREFS_NAME = "FlutterSharedPreferences" private const val AUTO_LAUNCH_KEY = "flutter.autoLaunch" } override fun onReceive(context: Context, intent: Intent) { if (intent.action != Intent.ACTION_BOOT_COMPLETED) return Log.d(TAG, "Device boot completed, checking autoLaunch setting") runCatching { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val autoLaunch = prefs.getBoolean(AUTO_LAUNCH_KEY, false) if (autoLaunch) { Log.d(TAG, "AutoLaunch enabled, triggering silent background boot") GlobalState.initServiceEngine(listOf("boot")) } else { Log.d(TAG, "AutoLaunch disabled, skipping background boot") } }.onFailure { Log.e(TAG, "Error in BootReceiver", it) } } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/receivers/PackageReplacedReceiver.kt ================================================ package com.appshub.bettbox.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import com.appshub.bettbox.GlobalState import com.appshub.bettbox.RunState class PackageReplacedReceiver : BroadcastReceiver() { companion object { private const val TAG = "PackageReplacedReceiver" } override fun onReceive(context: Context, intent: Intent) { if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return val pending = goAsync() if (Build.VERSION.SDK_INT >= 36) { GlobalState.handleStart(skipDebounce = true) } else { runCatching { android.net.VpnService.prepare(context) }.onFailure { Log.e(TAG, "Prepare failed", it) } } pending.finish() } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/services/BaseServiceInterface.kt ================================================ package com.appshub.bettbox.services import android.annotation.SuppressLint import android.app.Notification import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.pm.PackageManager import android.os.Build import androidx.core.app.NotificationCompat import com.appshub.bettbox.GlobalState import com.appshub.bettbox.R import com.appshub.bettbox.models.VpnOptions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import android.content.ComponentName import android.content.Intent interface BaseServiceInterface { suspend fun start(options: VpnOptions): Int fun stop() suspend fun startForeground() } fun Service.createBettboxNotificationBuilder(): Deferred = CoroutineScope(Dispatchers.Main).async { val defaultComponent = ComponentName(packageName, "com.appshub.bettbox.MainActivity") val lightComponent = ComponentName(packageName, "com.appshub.bettbox.MainActivityLight") val defaultState = runCatching { packageManager.getComponentEnabledSetting(defaultComponent) } .getOrDefault(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) val lightState = runCatching { packageManager.getComponentEnabledSetting(lightComponent) } .getOrDefault(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) val targetComponent = when { lightState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> lightComponent lightState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> defaultComponent defaultState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> defaultComponent defaultState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> lightComponent else -> runCatching { packageManager.getActivityInfo(lightComponent, 0) .takeIf { it.enabled }?.let { lightComponent } }.getOrNull() ?: defaultComponent } android.util.Log.d("Notification", "Using ${targetComponent.className}") val intent = Intent().apply { component = targetComponent action = Intent.ACTION_MAIN addCategory(Intent.CATEGORY_LAUNCHER) flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } val flags = if (Build.VERSION.SDK_INT >= 31) { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT } val pendingIntent = PendingIntent.getActivity(this@createBettboxNotificationBuilder, 0, intent, flags) NotificationCompat.Builder(this@createBettboxNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL).apply { setSmallIcon(R.drawable.ic) setContentTitle("Bettbox") setContentIntent(pendingIntent) setCategory(NotificationCompat.CATEGORY_SERVICE) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE } setOngoing(true) setShowWhen(false) setOnlyAlertOnce(true) } } fun Service.ensureNotificationChannel() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return val manager = getSystemService(NotificationManager::class.java) val channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL) if (channel == null || channel.importance != NotificationManager.IMPORTANCE_HIGH) { manager?.createNotificationChannel( NotificationChannel(GlobalState.NOTIFICATION_CHANNEL, "Bettbox Service", NotificationManager.IMPORTANCE_HIGH) ) } } @SuppressLint("ForegroundServiceType") fun Service.startForeground(notification: Notification, useSpecialType: Boolean = true) { ensureNotificationChannel() val type = if (Build.VERSION.SDK_INT >= 34 && useSpecialType && !GlobalState.isSmartStopped) { android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE } else { 0 } runCatching { if (type != 0) { startForeground(GlobalState.NOTIFICATION_ID, notification, type) } else { startForeground(GlobalState.NOTIFICATION_ID, notification) } }.onFailure { android.util.Log.e("BaseServiceInterface", "startForeground failed: ${it.message}") startForeground(GlobalState.NOTIFICATION_ID, notification) } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/services/BettboxService.kt ================================================ package com.appshub.bettbox.services import android.annotation.SuppressLint import android.app.Service import android.content.Intent import android.os.Binder import android.os.Build import android.os.IBinder import android.text.SpannableString import android.text.Spanned import android.text.style.RelativeSizeSpan import androidx.core.app.NotificationCompat import com.appshub.bettbox.GlobalState import com.appshub.bettbox.R import com.appshub.bettbox.models.VpnOptions class BettboxService : Service(), BaseServiceInterface { @Volatile private var cachedBuilder: NotificationCompat.Builder? = null private val binder = LocalBinder() @Volatile private var hasStartedForeground = false inner class LocalBinder : Binder() { fun getService() = this@BettboxService } override suspend fun start(options: VpnOptions) = 0 override fun stop() { hasStartedForeground = false stopSelf() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { stopForeground(STOP_FOREGROUND_REMOVE) } } fun resetNotificationBuilder() { cachedBuilder = null } private suspend fun notificationBuilder() = cachedBuilder ?: createBettboxNotificationBuilder().await().also { cachedBuilder = it } @SuppressLint("ForegroundServiceType") override suspend fun startForeground() { ensureNotificationChannel() val title: String val content: String if (GlobalState.isSmartStopped) { title = getString(R.string.core_suspended) content = getString(R.string.smart_auto_stop_service_running) } else { title = getString(R.string.core_connected) content = getString(R.string.service_running) } val builder = notificationBuilder() val separator = " ‹ " val combinedText = "$title$separator$content" val spannable = SpannableString(combinedText).apply { val startIndex = title.length + separator.length if (startIndex in 1..combinedText.length) { setSpan( RelativeSizeSpan(0.80f), startIndex, combinedText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) } } val notification = builder.setContentTitle(spannable) .setContentText(null) .setStyle(null) .setTicker(combinedText) .build() if (!hasStartedForeground) { this.startForeground(notification, useSpecialType = !GlobalState.isSmartStopped) hasStartedForeground = true } else { getSystemService(android.app.NotificationManager::class.java)?.notify(GlobalState.NOTIFICATION_ID, notification) } } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) GlobalState.getCurrentVPNPlugin()?.requestGc() } override fun onBind(intent: Intent): IBinder = binder override fun onDestroy() { stop() super.onDestroy() } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/services/BettboxTileService.kt ================================================ package com.appshub.bettbox.services import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.annotation.RequiresApi import com.appshub.bettbox.GlobalState import com.appshub.bettbox.RunState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @RequiresApi(Build.VERSION_CODES.N) class BettboxTileService : TileService() { private var scope: CoroutineScope? = null private fun updateTile(runState: RunState) { qsTile?.apply { state = when (runState) { RunState.START -> Tile.STATE_ACTIVE RunState.PENDING -> Tile.STATE_UNAVAILABLE RunState.STOP -> Tile.STATE_INACTIVE } updateTile() } } override fun onStartListening() { super.onStartListening() scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) GlobalState.syncStatus() updateTile(GlobalState.currentRunState) scope?.launch { GlobalState.runState.onEach { updateTile(it) }.launchIn(this) } } override fun onStopListening() { if (GlobalState.currentRunState == RunState.PENDING) { GlobalState.syncStatus() } scope?.cancel() scope = null super.onStopListening() } override fun onClick() { super.onClick() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && isLocked) { unlockAndRun { GlobalState.handleToggle() } } else { GlobalState.handleToggle() } } override fun onDestroy() { scope?.cancel() scope = null super.onDestroy() } } ================================================ FILE: android/app/src/main/kotlin/com/appshub/bettbox/services/BettboxVpnService.kt ================================================ package com.appshub.bettbox.services import android.annotation.SuppressLint import android.content.Intent import android.net.ProxyInfo import android.net.VpnService import android.os.Binder import android.os.Build import android.os.IBinder import android.os.Parcel import android.util.Log import androidx.core.app.NotificationCompat import com.appshub.bettbox.GlobalState import com.appshub.bettbox.extensions.getIpv4RouteAddress import com.appshub.bettbox.extensions.getIpv6RouteAddress import com.appshub.bettbox.extensions.toCIDR import com.appshub.bettbox.models.AccessControlMode import com.appshub.bettbox.models.VpnOptions import com.appshub.bettbox.plugins.VpnPlugin import com.appshub.bettbox.R class BettboxVpnService : VpnService(), BaseServiceInterface { companion object { private const val TAG = "BettboxVpnService" } @Volatile private var isStopped = false @Volatile private var hasStartedForeground = false override fun onCreate() { super.onCreate() GlobalState.initServiceEngine() } override suspend fun start(options: VpnOptions): Int = with(Builder()) { options.ipv4Address.takeIf { it.isNotEmpty() }?.let { ipv4 -> val cidr = ipv4.toCIDR() addAddress(cidr.address, cidr.prefixLength) Log.d("addAddress", "address: ${cidr.address} prefixLength:${cidr.prefixLength}") val routes = options.getIpv4RouteAddress() if (routes.isNotEmpty()) { runCatching { routes.forEach { addRoute(it.address, it.prefixLength) } } .onFailure { addRoute("0.0.0.0", 0) } } else { addRoute("0.0.0.0", 0) } } ?: addRoute("0.0.0.0", 0) if (options.ipv6Address.isNotEmpty()) { runCatching { val cidr = options.ipv6Address.toCIDR() Log.d("addAddress6", "address: ${cidr.address} prefixLength:${cidr.prefixLength}") addAddress(cidr.address, cidr.prefixLength) val routes = options.getIpv6RouteAddress() if (routes.isNotEmpty()) { runCatching { routes.forEach { addRoute(it.address, it.prefixLength) } } .onFailure { addRoute("::", 0) } } else { addRoute("::", 0) } }.onFailure { Log.d("addAddress6", "IPv6 is not supported.") } } if (options.dnsServerAddress.isNotBlank()) { runCatching { addDnsServer(options.dnsServerAddress) } .onFailure { Log.e(TAG, "Invalid DNS: ${options.dnsServerAddress}") } } setMtu(options.mtu.coerceIn(1280..65535).takeIf { it > 0 } ?: 1480) options.accessControl.takeIf { it.enable }?.let { ac -> when (ac.mode) { AccessControlMode.acceptSelected -> (ac.acceptList + packageName).forEach { addAllowedApplication(it) } AccessControlMode.rejectSelected -> (ac.rejectList - packageName).forEach { addDisallowedApplication(it) } } } setSession("Bettbox") setBlocking(false) if (Build.VERSION.SDK_INT >= 29) setMetered(false) if (options.allowBypass) allowBypass() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) { setHttpProxy(ProxyInfo.buildDirectProxy("127.0.0.1", options.port, options.bypassDomain)) } establish()?.detachFd()?.also { return it } Log.e(TAG, "Establish VPN rejected by system") -1 } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_STICKY override fun stop() { if (isStopped) return isStopped = true hasStartedForeground = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { runCatching { stopForeground(STOP_FOREGROUND_REMOVE) } .onFailure { Log.e(TAG, "Failed to stop foreground: ${it.message}") } } stopSelf() } @Volatile private var cachedBuilder: NotificationCompat.Builder? = null fun resetNotificationBuilder() { cachedBuilder = null } private suspend fun notificationBuilder(): NotificationCompat.Builder { if (cachedBuilder == null) { cachedBuilder = createBettboxNotificationBuilder().await() } return cachedBuilder!! } @SuppressLint("ForegroundServiceType") override suspend fun startForeground() { ensureNotificationChannel() val title: String val content: String if (GlobalState.isSmartStopped) { title = getString(R.string.core_suspended) content = getString(R.string.smart_auto_stop_service_running) } else { title = getString(R.string.core_connected) content = getString(R.string.service_running) } val builder = notificationBuilder() val separator = " ︙ " val combinedText = "$title$separator$content" val spannable = android.text.SpannableString(combinedText) val startIndex = title.length + separator.length if (startIndex in 1..combinedText.length) { spannable.setSpan( android.text.style.RelativeSizeSpan(0.80f), startIndex, combinedText.length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) } val notification = builder.setContentTitle(spannable) .setContentText(null) .setStyle(null) .setTicker(combinedText) .build() if (!hasStartedForeground) { this.startForeground(notification, useSpecialType = !GlobalState.isSmartStopped) hasStartedForeground = true } else { getSystemService(android.app.NotificationManager::class.java)?.notify(GlobalState.NOTIFICATION_ID, notification) } } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) GlobalState.getCurrentVPNPlugin()?.requestGc() } private val binder = LocalBinder() inner class LocalBinder : Binder() { fun getService(): BettboxVpnService = this@BettboxVpnService override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = runCatching { super.onTransact(code, data, reply, flags).also { success -> if (!success) GlobalState.getCurrentTilePlugin()?.handleStop() } }.getOrElse { Log.e(TAG, "onTransact failed: ${it.message}"); false } } override fun onBind(intent: Intent?): IBinder? { if (intent?.action == VpnService.SERVICE_INTERFACE) { return super.onBind(intent) } return binder } override fun onUnbind(intent: Intent?): Boolean { super.onUnbind(intent) return true } override fun onRevoke() { runCatching { VpnPlugin.handleStop() } super.onRevoke() } override fun onDestroy() { stop() super.onDestroy() } } ================================================ FILE: android/app/src/main/res/drawable/ic_launcher_background_dark.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_launcher_background_light.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_launcher_foreground_dark.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_launcher_foreground_light.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_notification_dark.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_notification_light.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_tile.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/tv_banner.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-night/ic_tile.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-night/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_light.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round_light.xml ================================================ ================================================ FILE: android/app/src/main/res/values/strings.xml ================================================ Bettbox Connected Service is running Suspended Auto-stop is running ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night-v27/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-ru/strings.xml ================================================ Bettbox Подключено Сервис запущен Приостановлено Автостоп запущен ================================================ FILE: android/app/src/main/res/values-v27/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-zh-rCN/strings.xml ================================================ Bettbox 已连接 服务正在运行中 已挂起 智能启停运行中 ================================================ FILE: android/app/src/main/res/values-zh-rTW/strings.xml ================================================ Bettbox 已連線 服務正在運行中 已暫停 智能啟停運行中 ================================================ FILE: android/app/src/main/res/xml/file_paths.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/network_security_config.xml ================================================ localhost 127.0.0.1 ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle.kts ================================================ allprojects { repositories { google() mavenCentral() } } val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() rootProject.layout.buildDirectory.value(newBuildDir) subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } subprojects { project.evaluationDependsOn(":app") } tasks.register("clean") { delete(rootProject.layout.buildDirectory) } ================================================ FILE: android/core/.gitignore ================================================ /build ================================================ FILE: android/core/build.gradle.kts ================================================ plugins { id("com.android.library") id("org.jetbrains.kotlin.android") } android { namespace = "com.appshub.bettbox.core" compileSdk = 36 ndkVersion = "28.2.13676358" defaultConfig { minSdk = 26 } buildTypes { release { isJniDebuggable = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } sourceSets { getByName("main") { jniLibs.srcDirs("src/main/jniLibs") } } externalNativeBuild { cmake { path("src/main/cpp/CMakeLists.txt") version = "3.22.1" } } kotlinOptions { jvmTarget = "17" } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } dependencies { implementation("androidx.annotation:annotation-jvm:1.9.1") } val copyNativeLibs by tasks.register("copyNativeLibs") { doFirst { delete("src/main/jniLibs") } from("../../libclash/android") into("src/main/jniLibs") } afterEvaluate { tasks.named("preBuild") { dependsOn(copyNativeLibs) } } ================================================ FILE: android/core/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: android/core/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/core/src/main/cpp/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.22.1) project("core") message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}") message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}") if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") add_compile_options(-O3) add_compile_options(-flto) add_compile_options(-g0) add_compile_options(-ffunction-sections -fdata-sections) add_compile_options(-fno-exceptions -fno-rtti) add_link_options( -flto -Wl,--gc-sections -Wl,--strip-all -Wl,--exclude-libs=ALL ) if (${ANDROID_ABI} STREQUAL "arm64-v8a" OR ${ANDROID_ABI} STREQUAL "x86_64") add_link_options(-Wl,-z,max-page-size=16384) endif() add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden) endif () set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so") message("LIB_CLASH_PATH ${LIB_CLASH_PATH}") if (EXISTS ${LIB_CLASH_PATH}) message("Found libclash.so for ABI ${ANDROID_ABI}") add_compile_definitions(LIBCLASH) include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}) link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}) add_library(${CMAKE_PROJECT_NAME} SHARED jni_helper.cpp core.cpp) target_link_libraries(${CMAKE_PROJECT_NAME} clash) else () message("Not found libclash.so for ABI ${ANDROID_ABI}") add_library(${CMAKE_PROJECT_NAME} SHARED jni_helper.cpp core.cpp) target_link_libraries(${CMAKE_PROJECT_NAME}) endif () ================================================ FILE: android/core/src/main/cpp/core.cpp ================================================ #ifdef LIBCLASH #include #include #include "jni_helper.h" #include "libclash.h" extern "C" JNIEXPORT void JNICALL Java_com_appshub_bettbox_core_Core_startTun(JNIEnv *env, jobject, const jint fd, jobject cb) { const auto interface = new_global(cb); startTUN(fd, interface); } extern "C" JNIEXPORT void JNICALL Java_com_appshub_bettbox_core_Core_stopTun(JNIEnv *) { stopTun(); } extern "C" JNIEXPORT void JNICALL Java_com_appshub_bettbox_core_Core_suspend(JNIEnv *, jobject, jint suspended) { suspend(suspended); } static jmethodID m_tun_interface_protect; static jmethodID m_tun_interface_resolve_process; static void release_jni_object_impl(void *obj) { ATTACH_JNI(); del_global(static_cast(obj)); } static void call_tun_interface_protect_impl(void *tun_interface, const int fd) { ATTACH_JNI(); env->CallVoidMethod(static_cast(tun_interface), m_tun_interface_protect, fd); } static const char * call_tun_interface_resolve_process_impl(void *tun_interface, int protocol, const char *source, const char *target, const int uid) { ATTACH_JNI(); if (env->PushLocalFrame(8) < 0) { return strdup(""); } const auto packageName = reinterpret_cast(env->CallObjectMethod(static_cast(tun_interface), m_tun_interface_resolve_process, protocol, new_string(source), new_string(target), uid)); const auto result = get_string(packageName); env->PopLocalFrame(nullptr); return result; } extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { JNIEnv *env = nullptr; if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } initialize_jni(vm, env); const auto c_tun_interface = find_class("com/appshub/bettbox/core/TunInterface"); m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V"); m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess", "(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;"); registerCallbacks(&call_tun_interface_protect_impl, &call_tun_interface_resolve_process_impl, &release_jni_object_impl); return JNI_VERSION_1_6; } #endif ================================================ FILE: android/core/src/main/cpp/jni_helper.cpp ================================================ #include "jni_helper.h" #include #include #include static JavaVM *global_vm; static jclass c_string; static jmethodID m_new_string; static jmethodID m_get_bytes; void initialize_jni(JavaVM *vm, JNIEnv *env) { global_vm = vm; c_string = reinterpret_cast(new_global(find_class("java/lang/String"))); m_new_string = find_method(c_string, "", "([B)V"); m_get_bytes = find_method(c_string, "getBytes", "()[B"); } JavaVM *global_java_vm() { return global_vm; } char *jni_get_string(JNIEnv *env, jstring str) { const auto array = reinterpret_cast(env->CallObjectMethod(str, m_get_bytes)); const int length = env->GetArrayLength(array); const auto content = static_cast(malloc(length + 1)); env->GetByteArrayRegion(array, 0, length, reinterpret_cast(content)); content[length] = 0; return content; } jstring jni_new_string(JNIEnv *env, const char *str) { const auto length = static_cast(strlen(str)); const auto array = env->NewByteArray(length); env->SetByteArrayRegion(array, 0, length, reinterpret_cast(str)); return reinterpret_cast(env->NewObject(c_string, m_new_string, array)); } int jni_catch_exception(JNIEnv *env) { const int result = env->ExceptionCheck(); if (result) { env->ExceptionDescribe(); env->ExceptionClear(); } return result; } void jni_attach_thread(scoped_jni *jni) { JavaVM *vm = global_java_vm(); if (vm->GetEnv(reinterpret_cast(&jni->env), JNI_VERSION_1_6) == JNI_OK) { jni->require_release = 0; return; } if (vm->AttachCurrentThread(&jni->env, nullptr) != JNI_OK) { abort(); } jni->require_release = 1; } void jni_detach_thread(const scoped_jni *env) { JavaVM *vm = global_java_vm(); if (env->require_release) { vm->DetachCurrentThread(); } } void release_string(char **str) { free(*str); } ================================================ FILE: android/core/src/main/cpp/jni_helper.h ================================================ #pragma once #include struct scoped_jni { JNIEnv *env; int require_release; }; extern void initialize_jni(JavaVM *vm, JNIEnv *env); extern jstring jni_new_string(JNIEnv *env, const char *str); extern char *jni_get_string(JNIEnv *env, jstring str); extern int jni_catch_exception(JNIEnv *env); extern void jni_attach_thread(scoped_jni *jni); extern void jni_detach_thread(const scoped_jni *env); extern void release_string(char **str); #define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \ scoped_jni _jni{}; \ jni_attach_thread(&_jni); \ JNIEnv *env = _jni.env #define scoped_string __attribute__((cleanup(release_string))) char* #define find_class(name) env->FindClass(name) #define find_method(cls, name, signature) env->GetMethodID(cls, name, signature) #define new_global(obj) env->NewGlobalRef(obj) #define del_global(obj) env->DeleteGlobalRef(obj) #define get_string(jstr) jni_get_string(env, jstr) #define new_string(cstr) jni_new_string(env, cstr) ================================================ FILE: android/core/src/main/java/com/appshub/bettbox/core/Core.kt ================================================ package com.appshub.bettbox.core import android.util.Log import java.net.InetSocketAddress object Core { private external fun startTun(fd: Int, cb: TunInterface) private external fun suspend(suspended: Int) external fun stopTun() init { System.loadLibrary("core") } private fun parseInetSocketAddress(address: String): InetSocketAddress { val lastColonIndex = address.lastIndexOf(':') if (lastColonIndex == -1) { return InetSocketAddress(address, 0) } val host = address.substring(0, lastColonIndex).removeSurrounding("[", "]") val port = address.substring(lastColonIndex + 1).toIntOrNull() ?: 0 return InetSocketAddress(host, port) } fun startTun( fd: Int, protect: (Int) -> Boolean, resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String ) { startTun(fd, object : TunInterface { override fun protect(fd: Int) { runCatching { protect(fd) } .onFailure { Log.e("Core", "protect JNI callback error: ${it.message}") } } override fun resolverProcess( protocol: Int, source: String, target: String, uid: Int ): String = runCatching { resolverProcess( protocol, parseInetSocketAddress(source), parseInetSocketAddress(target), uid ) }.onFailure { Log.e("Core", "resolverProcess JNI callback error: ${it.message}") }.getOrDefault("") }) } fun suspended(value: Boolean) { runCatching { Log.d("Core", "suspended called with value: $value") suspend(if (value) 1 else 0) Log.d("Core", "suspend JNI call completed") }.onFailure { Log.e("Core", "Error calling suspend: ${it.message}", it) } } } ================================================ FILE: android/core/src/main/java/com/appshub/bettbox/core/TunInterface.kt ================================================ package com.appshub.bettbox.core import androidx.annotation.Keep @Keep interface TunInterface { fun protect(fd: Int) fun resolverProcess(protocol: Int, source: String, target: String, uid: Int): String } ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError org.gradle.daemon=true org.gradle.parallel=true org.gradle.caching=true org.gradle.configureondemand=true android.useAndroidX=true android.enableJetifier=true kotlin_version=2.2.10 agp_version=8.12.2 android.enableCcache=true ================================================ FILE: android/settings.gradle.kts ================================================ pluginManagement { val flutterSdkPath = run { val properties = java.util.Properties() file("local.properties").inputStream().use { properties.load(it) } val flutterSdkPath = properties.getProperty("flutter.sdk") require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } flutterSdkPath } includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.12.2" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false } include(":app") include(":core") ================================================ FILE: arb/intl_en.arb ================================================ { "rule": "Rule", "global": "Global", "direct": "Direct", "dashboard": "Dashboard", "proxies": "Proxies", "profile": "Profile", "profiles": "Profiles", "tools": "Tools", "logs": "Logs", "logsDesc": "View captured logs", "resources": "Resources", "syncAll": "Sync All", "syncFailed": "Sync Failed", "resourcesDesc": "External resource info", "scriptDesc": "Global override script config", "trafficUsage": "Traffic Usage", "coreInfo": "Core Info", "networkSpeed": "Network Speed", "outboundMode": "Outbound Mode", "networkDetection": "Network Detection", "upload": "Upload", "download": "Download", "noProxy": "No Proxy", "noProxyDesc": "Please create or add a valid profile", "nullProfileDesc": "No profile. Please add one.", "settings": "Settings", "language": "Language", "defaultText": "Default", "more": "More", "other": "Other", "otherSettings": "Enhanced Tools", "otherSettingsDesc": "Modify enhanced tool settings", "smartAutoStop": "Smart Auto-Stop", "smartAutoStopDesc": "Stop VPN on specific networks", "networkMatch": "Network Match", "networkMatchHint": "Max 2 IPs/CIDRs, comma-separated", "smartAutoStopServiceRunning": "Smart Auto-Stop running", "serviceRunning": "Service Running", "coreConnected": "Connected", "coreSuspended": "Suspended", "invalidIpFormat": "Invalid IP or CIDR format", "tooManyRules": "Max 2 rules allowed", "dozeSuspend": "Doze Support", "dozeSuspendDesc": "Sync with system Doze mode", "storeFix": "Store Fix", "storeFixDesc": "Fix Play Store download issues", "disableQuic": "Disable QUIC", "disableQuicDesc": "Disable QUIC to resolve specific network issues", "excludeChina": "Exclude China", "excludeChinaDesc": "Allow China QUIC traffic instead of blocking all", "fcmOptimization": "FCM Optimization", "fcmOptimizationDesc": "Enhance FCM connection stability", "quickResponse": "Quick Response", "quickResponseDesc": "Disconnect on network change (WiFi/Mobile)", "networkFix": "Network Fix", "networkFixDesc": "Fix Windows network globe icon issue", "batteryOptimization": "Battery Optimization", "batteryOptimizationDesc": "Request battery optimization whitelist", "alreadyInWhitelist": "Already in whitelist", "about": "About", "en": "English", "ja": "Japanese", "ru": "Russian", "zh_CN": "Simplified Chinese", "zh_TC": "Traditional Chinese", "theme": "Theme", "themeDesc": "Set theme color and icon", "override": "Override", "overrideDesc": "Override proxy configurations", "allowLan": "Allow LAN", "allowLanDesc": "Allow LAN access to proxy", "tun": "TUN", "tunDesc": "Take over global device traffic", "minimizeOnExit": "Minimize on Exit", "minimizeOnExitDesc": "Override default exit behavior", "autoLaunch": "Auto Launch", "autoLaunchDesc": "Launch on system startup", "smartDelayLaunch": "Smart Delay", "smartDelayLaunchDesc": "Launch after network connected", "silentLaunch": "Silent Launch", "silentLaunchDesc": "Start in the background", "autoRun": "Auto Run", "autoRunDesc": "Connect on app launch", "logcat": "Log Capture", "logcatDesc": "Show log capture entry", "enableCrashReport": "Crash Analytics", "enableCrashReportDesc": "Upload crash logs when needed", "autoCheckUpdate": "Auto Check Updates", "autoCheckUpdateDesc": "Check updates on app launch", "accessControl": "Access Control", "accessControlDesc": "Configure per-app proxy access", "clearCacheTitle": "Clear Cache", "clearCacheDesc": "Clear FakeIP and DNS cache?", "forceGCTitle": "Force Garbage Collection", "forceGCDesc": "Force kernel garbage collection? Experimental, use with caution.", "fcmTip": "FCM support depends on your device; results are for reference. Disable 'Allow Bypass VPN' in network settings for accurate results.", "application": "Application", "applicationDesc": "Modify application settings", "edit": "Edit", "confirm": "Confirm", "update": "Update", "add": "Add", "save": "Save", "delete": "Delete", "years": "Years", "months": "Months", "hours": "Hours", "days": "Days", "minutes": "Minutes", "seconds": "Seconds", "ago": " Ago", "pleaseCloseTunFirst": "Please close TUN first", "pleaseCloseSystemProxyFirst": "Please close System Proxy first", "just": "Just now", "qrcode": "QR Code", "qrcodeDesc": "Scan QR code to get profile", "clipboard": "Clipboard", "clipboardDesc": "Get profile link from clipboard", "url": "URL", "urlDesc": "Get profile via URL", "file": "File", "fileDesc": "Upload profile file", "name": "Name", "profileNameNullValidationDesc": "Please enter a profile name", "profileUrlNullValidationDesc": "Please enter a profile URL", "profileUrlInvalidValidationDesc": "Please enter a valid URL", "autoUpdate": "Auto Update", "autoUpdateInterval": "Auto update interval (min)", "profileAutoUpdateIntervalNullValidationDesc": "Please enter update interval", "profileAutoUpdateIntervalInvalidValidationDesc": "Please enter a valid interval", "themeMode": "Theme Mode", "themeColor": "Theme Color", "preview": "Preview", "runtimeConfig": "Runtime Config", "cameraPermissionDenied": "Camera Permission Denied", "cameraPermissionDesc": "Camera permission is required to scan QR codes. Please grant it in settings.", "openSettings": "Open Settings", "retry": "Retry", "packageListPermissionRequired": "Permission to access installed apps is required. Grant now?", "packageListPermissionDenied": "Permission denied. Cannot access app list.", "auto": "Auto", "light": "Light", "dark": "Dark", "importFromURL": "Import from URL", "submit": "Submit", "doYouWantToPass": "Do you want to pass", "create": "Create", "defaultSort": "Default Sort", "delaySort": "Sort by Delay", "nameSort": "Sort by Name", "pleaseUploadFile": "Please upload a file", "pleaseUploadValidQrcode": "Please upload a valid QR code", "blacklistMode": "Blacklist Mode", "whitelistMode": "Whitelist Mode", "filterSystemApp": "Filter System Apps", "cancelFilterSystemApp": "Show System Apps", "selectAll": "Select All", "cancelSelectAll": "Deselect All", "appAccessControl": "App Access Control", "accessControlAllowDesc": "Only route selected apps through VPN", "accessControlNotAllowDesc": "Exclude selected apps from VPN", "selected": "Selected", "unableToUpdateCurrentProfileDesc": "Unable to update current profile", "noMoreInfoDesc": "No more info", "profileParseErrorDesc": "Profile parse error", "proxyPort": "Proxy Port", "proxyPortDesc": "Set the Clash listening port", "port": "Port", "logLevel": "Log Level", "show": "Show", "exit": "Exit", "systemProxy": "System Proxy", "project": "Project", "core": "Core", "tabAnimation": "Tab Animation", "desc": "Bettbox is based on the powerful and flexible Mihomo (Clash.Meta) proxy kernel, dedicated to a superior user experience. Forked from FlClash: Better Experience, Out of the box", "startVpn": "Starting...", "stopVpn": "Stopping...", "discovery": "New Version Found", "compatible": "Compatible Mode", "compatibleDesc": "Reduces some features for full Clash compatibility", "notSelectedTip": "Current proxy group cannot be selected.", "tip": "Tip", "backupAndRecovery": "Backup & Restore", "backupAndRecoveryDesc": "Sync data via WebDAV or local files", "account": "Account", "backup": "Backup", "recovery": "Restore", "recoveryProfiles": "Restore Profiles Only", "recoveryAll": "Restore All Data", "recoverySuccess": "Restore Successful", "backupSuccess": "Backup Successful", "noInfo": "No Info", "pleaseBindWebDAV": "Please bind WebDAV", "bind": "Bind", "connectivity": "Connectivity:", "webDAVConfiguration": "WebDAV Configuration", "address": "Address", "addressHelp": "WebDAV server address", "addressTip": "Please enter a valid WebDAV address", "password": "Password", "checkUpdate": "Check for Updates", "discoverNewVersion": "New Version Available", "checkUpdateError": "Already on the latest version", "goDownload": "Download Now", "unknown": "Unknown", "geoData": "GeoData", "externalResources": "External Resources", "checking": "Checking...", "country": "Country", "checkError": "Check Failed", "search": "Search", "allowBypass": "Allow Bypassing VPN", "allowBypassDesc": "Allow specific apps to bypass VPN", "externalController": "External Controller", "externalControllerDesc": "Control core via online port", "controlSecret": "Control Secret", "controlSecretDesc": "RESTful API access password", "generateSecret": "Generate", "secretCopied": "Secret copied to clipboard", "ipv6Desc": "Enable IPv6 traffic routing", "app": "App", "general": "General", "vpnSystemProxyDesc": "Attach HTTP proxy to VpnService", "systemProxyDesc": "Set system proxy", "unifiedDelay": "Unified Delay", "unifiedDelayDesc": "Exclude handshake delays from testing", "tcpConcurrent": "TCP Concurrent", "tcpConcurrentDesc": "Allow concurrent TCP connections", "geodataLoader": "GEO Low Memory", "geodataLoaderDesc": "Use GEO low memory loader", "requests": "Requests", "requestsDesc": "View recent request logs", "findProcessMode": "Find Process", "init": "Init", "infiniteTime": "Never Expires", "expirationTime": "Expiration Time", "connections": "Connections", "connectionsDesc": "View active connections", "intranetIP": "Intranet IP", "view": "View", "cut": "Cut", "copy": "Copy", "paste": "Paste", "testUrl": "Test URL", "startTest": "Start Test", "addProfile": "Add Profile", "customUrl": "Custom URL", "sync": "Sync", "exclude": "Hide from Recents", "excludeDesc": "Hide app from recent tasks list", "oneColumn": "1 Column", "twoColumns": "2 Columns", "threeColumns": "3 Columns", "fourColumns": "4 Columns", "expand": "Standard", "shrink": "Compact", "min": "Min", "tab": "Tab", "list": "List", "delay": "Delay", "style": "Style", "size": "Size", "delayAnimation": "Delay Animation", "delayAnimationDesc": "Customize animation during delay testing", "noAnimation": "Default", "rotatingCircle": "Rotating Circle", "pulse": "Pulse", "spinningLines": "Spinning Lines", "threeInOut": "Three In Out", "threeBounce": "Three Bounce", "circle": "Circle", "fadingCircle": "Fading Circle", "fadingFour": "Fading Four", "wave": "Wave", "doubleBounce": "Double Bounce", "sort": "Sort", "columns": "Columns", "proxiesSetting": "Proxy Settings", "proxyGroup": "Proxy Group", "go": "Go", "externalLink": "External Link", "otherContributors": "Other Contributors", "autoCloseConnections": "Auto-Close Connections", "autoCloseConnectionsDesc": "Close connections when switching nodes", "onlyStatisticsProxy": "Proxy Traffic Only", "onlyStatisticsProxyDesc": "Only record proxy traffic", "pureBlackMode": "Pure Black Mode", "keepAliveIntervalDesc": "TCP keep-alive interval", "entries": " entries", "local": "Local", "remote": "Remote", "remoteBackupDesc": "Backup data to WebDAV", "remoteRecoveryDesc": "Restore data from WebDAV", "localBackupDesc": "Backup data locally", "localRecoveryDesc": "Restore data from file", "mode": "Mode", "time": "Time", "source": "Source", "allApps": "All Apps", "onlyOtherApps": "Third-Party Apps Only", "action": "Action", "intelligentSelected": "Smart Select", "clipboardImport": "Import from Clipboard", "clipboardExport": "Export to Clipboard", "layout": "Layout", "tight": "Compact", "standard": "Standard", "loose": "Loose", "profilesSort": "Profile Sorting", "start": "Start", "stop": "Stop", "powerSwitch": "Power Switch", "runTime": "Uptime", "checkOrAddProfile": "Please add a profile first", "serviceReady": "Service Ready", "appDesc": "App-related settings", "vpnDesc": "VPN-related settings", "dnsDesc": "DNS-related settings", "key": "Key", "value": "Value", "hostsDesc": "Append hosts to current config", "vpnTip": "Restart VPN to apply changes", "vpnEnableDesc": "Route all system traffic via VpnService", "options": "Options", "loopback": "Loopback Unlock", "loopbackDesc": "UWP loopback unlocking tool", "providers": "Providers", "proxyProviders": "Proxy Providers", "ruleProviders": "Rule Providers", "advancedSettings": "Advanced Settings", "nodeExclusion": "Node Exclusion", "nodeExclusionDesc": "Exclude all matched nodes", "nodeExclusionPlaceholder": "HK|Hong Kong|🇭🇰", "formatError": "Please check the format", "healthCheckTimeout": "Timeout", "healthCheckTimeoutDesc": "Node health check timeout", "concurrencyLimit": "Concurrency Limit", "concurrencyLimitDesc": "Maximum concurrent delay tests", "notRecommended": "Not Recommended", "overrideDns": "Override DNS", "overrideDnsDesc": "Override profile's DNS settings", "overrideTestUrl": "Override Config", "ntp": "NTP", "ntpDesc": "Use NTP time service", "overrideNtp": "Override NTP", "overrideNtpDesc": "Override profile's NTP settings", "ntpStatus": "Status", "ntpStatusDesc": "Enable NTP time service", "writeToSystem": "Write to System", "writeToSystemDesc": "Requires administrator privileges", "ntpServer": "Server", "ntpPort": "Port", "ntpInterval": "Update Interval", "sniffer": "Sniffer", "snifferDesc": "Modify domain sniffer config", "overrideSniffer": "Override Sniffer", "overrideSnifferDesc": "Override profile's Sniffer settings", "snifferStatus": "Status", "snifferStatusDesc": "Enable Sniffer service", "forceDnsMapping": "Force DNS Mapping", "forceDnsMappingDesc": "Force mapping DNS results to connections", "parsePureIp": "Parse Pure IP", "parsePureIpDesc": "Parse pure IP connections", "overrideDestination": "Override Destination", "overrideDestinationDesc": "Override destination with sniffed result", "httpPortSniffer": "HTTP Port Sniffing", "tlsPortSniffer": "TLS Port Sniffing", "quicPortSniffer": "QUIC Port Sniffing", "forceDomain": "Force Sniff Domain", "skipDomain": "Skip Domain", "skipSrcAddress": "Skip Source IP", "skipDstAddress": "Skip Destination IP", "snifferPorts": "Ports", "snifferPortsHint": "e.g.: 80, 8080-8880", "snifferDomainHint": "One domain per line", "snifferAddressHint": "One address per line", "tunnel": "Tunnel", "tunnelDesc": "Use traffic forwarding tunnel", "overrideTunnel": "Override Tunnel", "overrideTunnelDesc": "Override profile's Tunnel settings", "tunnelList": "Forwarding List", "addTunnel": "Add Forwarding", "editTunnel": "Edit Forwarding", "deleteTunnel": "Delete Forwarding", "tunnelNetwork": "Network Protocol", "tunnelNetworkHint": "e.g.: tcp, udp", "tunnelAddress": "Listen Address", "tunnelAddressHint": "e.g.: 127.0.0.1:6553", "tunnelTarget": "Target Address", "tunnelTargetHint": "e.g.: 114.114.114.114:53", "tunnelProxy": "Proxy Name", "tunnelProxyHint": "e.g.: proxy (optional)", "experimental": "Experimental", "experimentalDesc": "Use with caution", "overrideExperimental": "Override Experimental", "overrideExperimentalDesc": "Override profile's Experimental settings", "quicGoDisableGso": "Disable QUIC GSO", "quicGoDisableGsoDesc": "Disable QUIC Generic Segmentation Offload", "quicGoDisableEcn": "Disable QUIC ECN", "quicGoDisableEcnDesc": "Disable QUIC Explicit Congestion Notification", "dialerIp4pConvert": "Enable Dialer IP4P Conversion", "dialerIp4pConvertDesc": "Enable dialer IP4P address conversion feature", "status": "Status", "statusDesc": "Uses system DNS when disabled", "preferH3Desc": "Prioritize DoH HTTP/3", "cacheAlgorithm": "Cache Algorithm", "respectRules": "Respect Rules", "respectRulesDesc": "DNS connections follow Rules", "dnsMode": "DNS Mode", "fakeipRange": "FakeIP Range", "fakeipRangeV6": "FakeIPv6 Range", "fakeIpFilterMode": "FakeIP Filter Mode", "fakeIpFilterModeDesc": "Specify FakeIP filter mode", "blacklist": "Blacklist", "whitelist": "Whitelist", "fakeipFilter": "FakeIP Filter", "fakeipTtl": "FakeIP TTL", "defaultNameserver": "Default Nameserver", "defaultNameserverDesc": "Used to resolve DNS servers", "nameserver": "Nameserver", "nameserverDesc": "Used to resolve domains", "useHosts": "Use Hosts", "useSystemHosts": "Use System Hosts", "nameserverPolicy": "Nameserver Policy", "nameserverPolicyDesc": "Specify domain-specific nameservers", "proxyNameserver": "Proxy Nameserver", "proxyNameserverDesc": "Used to resolve proxy nodes", "directNameserver": "Direct Nameserver", "directNameserverDesc": "Used to resolve direct domains", "directNameserverFollowPolicy": "Direct DNS Follows Policy", "fallback": "Fallback", "fallbackDesc": "Usually offshore DNS", "fallbackFilter": "Fallback Filter", "geoipCode": "GeoIP Code", "ipcidr": "IP/CIDR", "domain": "Domain", "reset": "Reset", "action_view": "Show/Hide", "action_start": "Start/Stop", "action_mode": "Switch Mode", "action_proxy": "System Proxy", "action_tun": "TUN", "disclaimer": "Disclaimer", "disclaimerDesc": "This free open-source software is for non-commercial learning and personal use only. Proxy services are independent of this software. By agreeing, you acknowledge this; otherwise, please exit.", "agree": "Agree", "hotkeyManagement": "Hotkey Management", "hotkeyManagementDesc": "Control app via keyboard", "pressKeyboard": "Press a key", "inputCorrectHotkey": "Enter a valid hotkey", "hotkeyConflict": "Hotkey Conflict", "remove": "Remove", "noHotKey": "No Hotkeys", "noNetwork": "No Network", "ipv6InboundDesc": "Allow IPv6 inbound", "exportLogs": "Export Logs", "exportSuccess": "Export Successful", "iconStyle": "Icon Style", "onlyIcon": "Icon Only", "noIcon": "No Icon", "stackMode": "Stack Mode", "strictRoute": "Strict Route", "strictRouteDesc": "Use TUN strict routing mode", "icmpForwarding": "ICMP Forwarding", "icmpForwardingDesc": "Enable ICMP Ping", "dnsHijack": "DNS Hijack", "dnsHijackDesc": "Redirect DNS queries to internal DNS module", "endpointIndependentNat": "NAT Enhancement", "endpointIndependentNatDesc": "Enable endpoint-independent NAT", "network": "Network", "networkDesc": "Modify network-related settings", "bypassDomain": "Bypass Domain", "bypassDomainDesc": "Active only when System Proxy is on", "resetTip": "Are you sure you want to reset?", "regExp": "RegExp", "icon": "Icon", "iconConfiguration": "Icon Configuration", "noData": "No Data", "adminAutoLaunch": "Admin Auto-Launch", "adminAutoLaunchDesc": "Auto-start with admin privileges", "fontFamily": "Font", "systemFont": "System Font", "toggle": "Toggle", "system": "System", "bypassPrivateRoute": "Bypass Private Network", "bypassPrivateRouteDesc": "Automatically bypass private network IP addresses", "pleaseInputAdminPassword": "Please enter the admin password", "copyEnvVar": "Copy Environment Variable", "memoryInfo": "Memory Info", "cancel": "Cancel", "fileIsUpdate": "File modified. Save changes?", "profileHasUpdate": "Profile modified. Disable auto-update?", "hasCacheChange": "Cache modifications?", "copySuccess": "Copy Successful", "success": "Success", "copyLink": "Copy Link", "exportFile": "Export File", "cacheCorrupt": "Cache corrupted. Clear it?", "detectionTip": "Third-party API result (for reference only)", "ipClickBehavior": "Toggle Display", "ipPrivacyProtection": "Hide IP Display", "manualRefreshIp": "Refresh IP", "tryManualRefresh": "Please try manual refresh", "refreshAppList": "Refresh App List", "refreshAppListConfirm": "Refresh the app list?", "switchToDomesticIp": "Get Domestic IP", "listen": "Listen", "undo": "Undo", "redo": "Redo", "none": "None", "basicConfig": "Core Configuration", "basicConfigDesc": "Global core settings", "selectedCountTitle": "{count} items selected", "addRule": "Add Rule", "ruleName": "Rule Name", "content": "Content", "subRule": "Sub Rule", "ruleTarget": "Rule Target", "sourceIp": "Source IP", "noResolve": "No Resolve", "getOriginRules": "Original Rules", "overrideOriginRules": "Override Original Rules", "addedOriginRules": "Append to Original Rules", "enableOverride": "Enable Override", "saveChanges": "Save changes?", "generalDesc": "Modify general settings", "findProcessModeDesc": "Enable process matching", "tabAnimationDesc": "Effective only in mobile view", "navBarHapticFeedback": "Haptic Feedback", "navBarHapticFeedbackDesc": "Vibrate on navigation tab switch", "saveTip": "Are you sure you want to save?", "colorSchemes": "Color Schemes", "palette": "Palette", "tonalSpotScheme": "Tonal Spot", "fidelityScheme": "Fidelity", "monochromeScheme": "Monochrome", "neutralScheme": "Neutral", "vibrantScheme": "Vibrant", "expressiveScheme": "Expressive", "contentScheme": "Content", "rainbowScheme": "Rainbow", "fruitSaladScheme": "Fruit Salad", "developerMode": "Developer Mode", "developerModeEnableTip": "Developer mode is enabled.", "messageTest": "Message Test", "messageTestTip": "This is a message.", "crashTest": "Crash Test", "clearData": "Clear Data", "textScale": "Text Scaling", "lightIcon": "Light Icon", "lightIconDesc": "Manually switch light desktop app icon", "harmonyFont": "HarmonyOS Font", "harmonyFontDesc": "Use optimized HarmonyOS Sans font", "internet": "Internet", "systemApp": "System App", "noNetworkApp": "No Network App", "contactMe": "Contact Me", "recoveryStrategy": "Recovery Strategy", "recoveryStrategy_override": "Override", "recoveryStrategy_compatible": "Compatible", "logsTest": "Logs Test", "emptyTip": "{label} cannot be empty", "urlTip": "{label} must be a URL", "numberTip": "{label} must be a number", "interval": "Interval", "existsTip": "{label} already exists", "deleteTip": "Delete current {label}?", "deleteMultipTip": "Delete selected {label}?", "nullTip": "No {label}", "script": "Script", "color": "Color", "rename": "Rename", "unnamed": "Unnamed", "pleaseEnterScriptName": "Please enter a script name", "overrideInvalidTip": "Inactive in script mode", "mixedPort": "Mixed Port", "socksPort": "Socks Port", "redirPort": "Redir Port", "tproxyPort": "Tproxy Port", "portTip": "{label} must be between 1024 and 49151", "portConflictTip": "Please enter a different port", "import": "Import", "importFromCode": "Import from Code", "importFailed": "Import failed", "importFile": "Import from File", "importUrl": "Import from URL", "autoSetSystemDns": "Auto Set System DNS", "details": "{label} Details", "creationTime": "Creation Time", "progress": "Progress", "host": "Host", "destination": "Destination", "destinationGeoIP": "Destination GeoIP", "destinationIPASN": "Destination IP ASN", "specialProxy": "Special Proxy", "specialRules": "Special Rules", "remoteDestination": "Remote Destination", "networkType": "Network Type", "proxyChains": "Proxy Chains", "log": "Log", "connection": "Active Connections", "request": "Request", "switchLabel": "Switch", "noStatusAvailable": "No Status", "onlinePanel": "Online Panel", "openDashboard": "Open Zashboard", "custom": "Custom", "wakelock": "Wakelock", "wakelockDescription": "Keeps the screen on and app active in the background without requiring special CPU wakelock permissions.", "tunEnableRequireAdmin": "TUN requires admin privileges. Please run as Administrator.", "restartTip": "Restart TUN for changes to take effect", "restart": "Restart", "restartCoreTitle": "Restart Core", "restartCoreDesc": "Manually restart the core?", "highRefreshRate": "High Refresh Rate", "highRefreshRateDesc": "Enable highest refresh rate support" } ================================================ FILE: arb/intl_ru.arb ================================================ { "rule": "Правила", "global": "Глобально", "direct": "Напрямую", "dashboard": "Главная", "proxies": "Прокси", "profile": "Профиль", "profiles": "Профили", "tools": "Настройки", "logs": "Логи", "logsDesc": "Просмотр журналов", "resources": "Ресурсы", "syncAll": "Синхронизировать всё", "syncFailed": "Ошибка синхронизации", "resourcesDesc": "Управление внешними ресурсами", "scriptDesc": "Настройка глобального скрипта переопределения", "trafficUsage": "Трафик", "coreInfo": "Информация о ядре", "networkSpeed": "Скорость сети", "outboundMode": "Режим выхода", "networkDetection": "Проверка сети", "upload": "Отправка", "download": "Загрузка", "noProxy": "Нет прокси", "noProxyDesc": "Создайте или добавьте профиль", "nullProfileDesc": "Нет профиля, добавьте его", "settings": "Настройки", "language": "Язык", "defaultText": "По умолчанию", "more": "Подробнее", "other": "Другое", "otherSettings": "Расширенные инструменты", "otherSettingsDesc": "Настройка расширенных функций", "smartAutoStop": "Умная остановка", "smartAutoStopDesc": "Останавливать прокси при подключении к заданной сети", "networkMatch": "Сопоставление сети", "networkMatchHint": "Введите IP или CIDR, максимум 2, через запятую", "smartAutoStopServiceRunning": "Служба умной остановки работает", "serviceRunning": "Служба запущена", "coreConnected": "Подключено", "coreSuspended": "Приостановлено", "invalidIpFormat": "Неверный формат IP или CIDR", "tooManyRules": "Максимум 2 правила", "dozeSuspend": "Поддержка Doze", "dozeSuspendDesc": "Синхронизация с режимом сна Android", "storeFix": "Исправление магазина", "storeFixDesc": "Исправляет проблемы загрузки Google Play", "disableQuic": "Отключить QUIC", "disableQuicDesc": "Отключить QUIC для решения сетевых проблем", "excludeChina": "Исключить Китай", "excludeChinaDesc": "Разрешить QUIC-трафик Китая вместо полной блокировки", "fcmOptimization": "Оптимизация FCM", "fcmOptimizationDesc": "Повышает стабильность FCM при прямом подключении", "quickResponse": "Быстрый отклик", "quickResponseDesc": "Активно отключать соединения при изменении сети", "networkFix": "Исправление сети", "networkFixDesc": "Исправляет значок сети Windows", "batteryOptimization": "Оптимизация батареи", "batteryOptimizationDesc": "Запросить добавление в белый список энергосбережения", "alreadyInWhitelist": "Уже в белом списке", "about": "О программе", "en": "Английский", "ja": "Японский", "ru": "Русский", "zh_CN": "Китайский (упрощённый)", "zh_TC": "Китайский (традиционный)", "theme": "Тема", "themeDesc": "Настройка темы и иконок", "override": "Переопределение", "overrideDesc": "Переопределение конфигурации прокси", "allowLan": "LAN доступ", "allowLanDesc": "Разрешить доступ из локальной сети", "tun": "Виртуальный адаптер", "tunDesc": "Перехват всего трафика устройства", "minimizeOnExit": "Сворачивать при выходе", "minimizeOnExitDesc": "Изменить поведение при выходе", "autoLaunch": "Автозапуск", "autoLaunchDesc": "Запуск при старте системы", "smartDelayLaunch": "Умная задержка", "smartDelayLaunchDesc": "Запуск после успешного подключения к сети", "silentLaunch": "Тихий запуск", "silentLaunchDesc": "Запуск в фоне без открытия окна", "autoRun": "Автоподключение", "autoRunDesc": "Подключаться при запуске приложения", "logcat": "Сбор логов", "logcatDesc": "Показать раздел логов", "enableCrashReport": "Анализ сбоев", "enableCrashReportDesc": "Отправка отчётов о сбоях при необходимости", "autoCheckUpdate": "Автообновление", "autoCheckUpdateDesc": "Проверка обновлений при запуске", "accessControl": "Контроль доступа", "accessControlDesc": "Настройка доступа приложений к прокси", "clearCacheTitle": "Очистить кэш", "clearCacheDesc": "Очистить кэш FakeIP и DNS?", "forceGCTitle": "Принудительный GC", "forceGCDesc": "Выполнить сборку мусора ядра? Экспериментально, используйте с осторожностью", "fcmTip": "FCM зависит от устройства. Для точных результатов отключите 'Разрешить обход VPN'", "application": "Приложение", "applicationDesc": "Настройки приложения", "edit": "Редактировать", "confirm": "Подтвердить", "update": "Обновить", "add": "Добавить", "save": "Сохранить", "delete": "Удалить", "years": "лет", "months": "месяцев", "hours": "часов", "days": "дней", "minutes": "минут", "seconds": "секунд", "ago": " назад", "pleaseCloseTunFirst": "Сначала отключите виртуальный адаптер", "pleaseCloseSystemProxyFirst": "Сначала отключите системный прокси", "just": "только что", "qrcode": "QR-код", "qrcodeDesc": "Сканировать QR для получения профиля", "clipboard": "Буфер обмена", "clipboardDesc": "Автоматически получать ссылки из буфера обмена", "url": "URL", "urlDesc": "Получить профиль по URL", "file": "Файл", "fileDesc": "Загрузить файл конфигурации", "name": "Имя", "profileNameNullValidationDesc": "Введите имя профиля", "profileUrlNullValidationDesc": "Введите URL профиля", "profileUrlInvalidValidationDesc": "Введите корректный URL профиля", "autoUpdate": "Автообновление", "autoUpdateInterval": "Интервал автообновления (минуты)", "profileAutoUpdateIntervalNullValidationDesc": "Введите интервал автообновления", "profileAutoUpdateIntervalInvalidValidationDesc": "Введите корректный формат интервала", "themeMode": "Режим темы", "themeColor": "Цвет темы", "preview": "Предпросмотр", "runtimeConfig": "Конфигурация", "cameraPermissionDenied": "Доступ к камере запрещён", "cameraPermissionDesc": "Для сканирования QR-кода требуется доступ к камере. Пожалуйста, предоставьте разрешение в настройках.", "openSettings": "Открыть настройки", "retry": "Повторить", "packageListPermissionRequired": "Эта функция требует доступа к списку установленных приложений. Предоставить разрешение?", "packageListPermissionDenied": "Разрешение отклонено. Без доступа невозможно получить список приложений.", "auto": "Авто", "light": "Светлая", "dark": "Тёмная", "importFromURL": "Импорт из URL", "submit": "Отправить", "doYouWantToPass": "Пропустить", "create": "Создать", "defaultSort": "По умолчанию", "delaySort": "По задержке", "nameSort": "По имени", "pleaseUploadFile": "Загрузите файл", "pleaseUploadValidQrcode": "Загрузите корректный QR-код", "blacklistMode": "Режим чёрного списка", "whitelistMode": "Режим белого списка", "filterSystemApp": "Скрыть системные приложения", "cancelFilterSystemApp": "Показать системные приложения", "selectAll": "Выбрать все", "cancelSelectAll": "Отменить выбор", "appAccessControl": "Контроль доступа приложений", "accessControlAllowDesc": "Только выбранные приложения используют VPN", "accessControlNotAllowDesc": "Выбранные приложения исключены из VPN", "selected": "Выбрано", "unableToUpdateCurrentProfileDesc": "Невозможно обновить текущий профиль", "noMoreInfoDesc": "Нет дополнительной информации", "profileParseErrorDesc": "Ошибка разбора профиля", "proxyPort": "Порт прокси", "proxyPortDesc": "Установить порт прослушивания Clash", "port": "Порт", "logLevel": "Уровень логов", "show": "Показать", "exit": "Выход", "systemProxy": "Системный прокси", "project": "Проект", "core": "Ядро", "tabAnimation": "Анимация вкладок", "desc": "Bettbox основан на мощном и гибком прокси-ядре Mihomo (Clash.Meta) и стремится к созданию лучшего пользовательского опыта. Форк от FlClash: Улучшенный опыт, готов к работе «из коробки»", "startVpn": "Запуск VPN", "stopVpn": "Остановка VPN", "discovery": "Доступно обновление", "compatible": "Режим совместимости", "compatibleDesc": "Включает полную поддержку Clash с потерей некоторых функций", "notSelectedTip": "Невозможно выбрать эту группу прокси", "tip": "Подсказка", "backupAndRecovery": "Резервное копирование", "backupAndRecoveryDesc": "Синхронизация данных через WebDAV или файл", "account": "Аккаунт", "backup": "Создать копию", "recovery": "Восстановить", "recoveryProfiles": "Только профили", "recoveryAll": "Все данные", "recoverySuccess": "Восстановление успешно", "backupSuccess": "Резервное копирование успешно", "noInfo": "Нет информации", "pleaseBindWebDAV": "Привяжите WebDAV", "bind": "Привязать", "connectivity": "Подключение:", "webDAVConfiguration": "Настройки WebDAV", "address": "Адрес", "addressHelp": "Адрес сервера WebDAV", "addressTip": "Введите корректный адрес WebDAV", "password": "Пароль", "checkUpdate": "Проверить обновление", "discoverNewVersion": "Доступна новая версия", "checkUpdateError": "Установлена последняя версия", "goDownload": "Перейти к загрузке", "unknown": "Неизвестно", "geoData": "Геоданные", "externalResources": "Внешние ресурсы", "checking": "Проверка...", "country": "Регион", "checkError": "Ошибка проверки", "search": "Поиск", "allowBypass": "Разрешить обход VPN", "allowBypassDesc": "Некоторые приложения смогут обходить VPN", "externalController": "Внешнее управление", "externalControllerDesc": "Управление ядром через REST API", "controlSecret": "Пароль управления", "controlSecretDesc": "Пароль для доступа к RESTful API", "generateSecret": "Сгенерировать", "secretCopied": "Пароль скопирован в буфер обмена", "ipv6Desc": "Включить поддержку IPv6", "app": "Приложение", "general": "Общие", "vpnSystemProxyDesc": "Добавить HTTP-прокси к VPN", "systemProxyDesc": "Настроить системный прокси", "unifiedDelay": "Унифицированная задержка", "unifiedDelayDesc": "Убрать задержку рукопожатия и разбора", "tcpConcurrent": "TCP параллельно", "tcpConcurrentDesc": "Разрешить параллельные TCP-соединения", "geodataLoader": "Экономия памяти GEO", "geodataLoaderDesc": "Использовать загрузчик GEO с низким потреблением памяти", "requests": "Запросы", "requestsDesc": "Просмотр недавних запросов", "findProcessMode": "Поиск процесса", "init": "Инициализация", "infiniteTime": "Бессрочно", "expirationTime": "Срок действия", "connections": "Соединения", "connectionsDesc": "Просмотр текущих соединений", "intranetIP": "Локальный IP", "view": "Просмотр", "cut": "Вырезать", "copy": "Копировать", "paste": "Вставить", "testUrl": "URL теста", "startTest": "Тест задержки", "addProfile": "Добавить профиль", "customUrl": "Пользовательский URL", "sync": "Синхронизировать", "exclude": "Скрыть из недавних", "excludeDesc": "Скрыть приложение из недавних задач", "oneColumn": "1 колонка", "twoColumns": "2 колонки", "threeColumns": "3 колонки", "fourColumns": "4 колонки", "expand": "Стандартный", "shrink": "Компактный", "min": "Минимальный", "tab": "Вкладки", "list": "Список", "delay": "Задержка", "style": "Стиль", "size": "Размер", "delayAnimation": "Анимация задержки", "delayAnimationDesc": "Настройка анимации при тестировании", "noAnimation": "По умолчанию", "rotatingCircle": "Вращающийся круг", "pulse": "Пульсация", "spinningLines": "Вращающиеся линии", "threeInOut": "Три точки", "threeBounce": "Прыгающие точки", "circle": "Круг", "fadingCircle": "Затухающий круг", "fadingFour": "Затухающие точки", "wave": "Волна", "doubleBounce": "Двойной отскок", "sort": "Сортировка", "columns": "Колонки", "proxiesSetting": "Настройки прокси", "proxyGroup": "Группа прокси", "go": "Перейти", "externalLink": "Внешняя ссылка", "otherContributors": "Другие участники", "autoCloseConnections": "Автозакрытие соединений", "autoCloseConnectionsDesc": "Закрывать соединения при смене узла", "onlyStatisticsProxy": "Только прокси-трафик", "onlyStatisticsProxyDesc": "Считать только трафик через прокси", "pureBlackMode": "Чистый чёрный", "keepAliveIntervalDesc": "Интервал TCP keep-alive", "entries": " записей", "local": "Локально", "remote": "Удалённо", "remoteBackupDesc": "Резервное копирование на WebDAV", "remoteRecoveryDesc": "Восстановление с WebDAV", "localBackupDesc": "Локальное резервное копирование", "localRecoveryDesc": "Восстановление из файла", "mode": "Режим", "time": "Время", "source": "Источник", "allApps": "Все приложения", "onlyOtherApps": "Только сторонние", "action": "Действие", "intelligentSelected": "Умный выбор", "clipboardImport": "Импорт из буфера", "clipboardExport": "Экспорт в буфер", "layout": "Макет", "tight": "Компактный", "standard": "Стандартный", "loose": "Свободный", "profilesSort": "Сортировка профилей", "start": "Запуск", "stop": "Остановка", "powerSwitch": "Переключатель", "runTime": "Время работы", "checkOrAddProfile": "Добавьте профиль", "serviceReady": "Служба готова", "appDesc": "Настройки приложения", "vpnDesc": "Настройки VPN", "dnsDesc": "Настройки DNS", "key": "Ключ", "value": "Значение", "hostsDesc": "Добавить hosts к текущей конфигурации", "vpnTip": "Перезапустите VPN для применения изменений", "vpnEnableDesc": "Автоматическая маршрутизация всего трафика через VpnService", "options": "Опции", "loopback": "Разблокировка UWP", "loopbackDesc": "Инструмент для разблокировки UWP loopback", "providers": "Провайдеры", "proxyProviders": "Провайдеры прокси", "ruleProviders": "Провайдеры правил", "advancedSettings": "Расширенные настройки", "nodeExclusion": "Исключение узлов", "nodeExclusionDesc": "Исключить все узлы, соответствующие шаблону", "nodeExclusionPlaceholder": "HK|Гонконг|🇭🇰", "formatError": "Проверьте формат", "healthCheckTimeout": "Таймаут проверки", "healthCheckTimeoutDesc": "Таймаут проверки работоспособности узлов", "concurrencyLimit": "Лимит параллелизма", "concurrencyLimitDesc": "Максимальное количество параллельных тестов задержки", "notRecommended": "Не рекомендуется", "overrideDns": "Переопределить DNS", "overrideDnsDesc": "Включить переопределение настроек DNS в конфигурации", "overrideTestUrl": "Переопределить URL теста", "ntp": "NTP", "ntpDesc": "Использовать службу времени NTP", "overrideNtp": "Переопределить NTP", "overrideNtpDesc": "Включить переопределение настроек NTP в конфигурации", "ntpStatus": "Статус NTP", "ntpStatusDesc": "Включить службу времени NTP", "writeToSystem": "Записать в систему", "writeToSystemDesc": "Требуются права администратора", "ntpServer": "Сервер NTP", "ntpPort": "Порт NTP", "ntpInterval": "Интервал обновления", "sniffer": "Sniffer", "snifferDesc": "Настройка сниффинга доменов", "overrideSniffer": "Переопределить Sniffer", "overrideSnifferDesc": "Включить переопределение настроек Sniffer в конфигурации", "snifferStatus": "Статус сниффера", "snifferStatusDesc": "Включить службу сниффинга", "forceDnsMapping": "Принудительное DNS-отображение", "forceDnsMappingDesc": "Принудительно отображать результаты DNS на соединение", "parsePureIp": "Разбор чистых IP", "parsePureIpDesc": "Разбирать соединения по чистому IP", "overrideDestination": "Переопределить назначение", "overrideDestinationDesc": "Использовать результаты сниффинга для переопределения целевого адреса", "httpPortSniffer": "HTTP порты сниффера", "tlsPortSniffer": "TLS порты сниффера", "quicPortSniffer": "QUIC порты сниффера", "forceDomain": "Принудительный сниффинг доменов", "skipDomain": "Пропустить домены", "skipSrcAddress": "Пропустить IP источника", "skipDstAddress": "Пропустить IP назначения", "snifferPorts": "Порты", "snifferPortsHint": "Например: 80, 8080-8880", "snifferDomainHint": "Один домен на строку", "snifferAddressHint": "Один адрес на строку", "tunnel": "Туннель", "tunnelDesc": "Использовать туннель перенаправления трафика", "overrideTunnel": "Переопределить туннель", "overrideTunnelDesc": "Включить переопределение настроек туннеля в конфигурации", "tunnelList": "Список перенаправлений", "addTunnel": "Добавить перенаправление", "editTunnel": "Изменить перенаправление", "deleteTunnel": "Удалить перенаправление", "tunnelNetwork": "Сетевой протокол", "tunnelNetworkHint": "Например: tcp, udp", "tunnelAddress": "Адрес прослушивания", "tunnelAddressHint": "Например: 127.0.0.1:6553", "tunnelTarget": "Целевой адрес", "tunnelTargetHint": "Например: 114.114.114.114:53", "tunnelProxy": "Имя прокси", "tunnelProxyHint": "Например: proxy (опционально)", "experimental": "Экспериментальное", "experimentalDesc": "Экспериментальные настройки, используйте с осторожностью", "overrideExperimental": "Переопределить экспериментальное", "overrideExperimentalDesc": "Включить переопределение экспериментальных настроек в конфигурации", "quicGoDisableGso": "Отключить GSO QUIC", "quicGoDisableGsoDesc": "Отключить Generic Segmentation Offload для QUIC", "quicGoDisableEcn": "Отключить ECN QUIC", "quicGoDisableEcnDesc": "Отключить Explicit Congestion Notification для QUIC", "dialerIp4pConvert": "Включить преобразование IP4P", "dialerIp4pConvertDesc": "Включить преобразование IP4P в диалере", "status": "Статус", "statusDesc": "Использовать системный DNS при выключении", "preferH3Desc": "Приоритет HTTP/3 для DoH", "cacheAlgorithm": "Алгоритм кэша", "respectRules": "Следовать правилам", "respectRulesDesc": "DNS-соединения следуют правилам", "dnsMode": "Режим DNS", "fakeipRange": "Диапазон FakeIP", "fakeipRangeV6": "Диапазон FakeIPv6", "fakeIpFilterMode": "Режим фильтрации FakeIP", "fakeIpFilterModeDesc": "Указать режим фильтрации FakeIP", "blacklist": "Чёрный список", "whitelist": "Белый список", "fakeipFilter": "Фильтр FakeIP", "fakeipTtl": "Время жизни FakeIP", "defaultNameserver": "DNS по умолчанию", "defaultNameserverDesc": "Используется для разрешения DNS-серверов", "nameserver": "Серверы имён", "nameserverDesc": "Используется для разрешения доменов", "useHosts": "Использовать hosts", "useSystemHosts": "Использовать системные hosts", "nameserverPolicy": "Политика DNS", "nameserverPolicyDesc": "Указать политику DNS для конкретных доменов", "proxyNameserver": "DNS для прокси", "proxyNameserverDesc": "Используется для разрешения доменов прокси", "directNameserver": "DNS для прямых", "directNameserverDesc": "Используется для разрешения прямых доменов", "directNameserverFollowPolicy": "Прямой DNS следует правилам", "fallback": "Резервный DNS", "fallbackDesc": "Обычно используются зарубежные DNS", "fallbackFilter": "Фильтр резервного DNS", "geoipCode": "Код GeoIP", "ipcidr": "IP/CIDR", "domain": "Домен", "reset": "Сброс", "action_view": "Показать/Скрыть", "action_start": "Запуск/Остановка", "action_mode": "Сменить режим", "action_proxy": "Системный прокси", "action_tun": "Виртуальный адаптер", "disclaimer": "Отказ от ответственности", "disclaimerDesc": "Это бесплатное ПО с открытым исходным кодом, предназначенное только для обучения и личного тестирования. Действия прокси-провайдеров не связаны с этим ПО. Соглашаясь, вы подтверждаете, что полностью осведомлены об этом. Если не согласны, пожалуйста, выйдите!", "agree": "Согласен", "hotkeyManagement": "Управление горячими клавишами", "hotkeyManagementDesc": "Управление приложением с клавиатуры", "pressKeyboard": "Нажмите клавиши", "inputCorrectHotkey": "Введите корректное сочетание клавиш", "hotkeyConflict": "Конфликт горячих клавиш", "remove": "Удалить", "noHotKey": "Нет горячих клавиш", "noNetwork": "Нет сети", "ipv6InboundDesc": "Разрешить входящие IPv6", "exportLogs": "Экспорт логов", "exportSuccess": "Экспорт успешен", "iconStyle": "Стиль иконок", "onlyIcon": "Только иконки", "noIcon": "Без иконок", "stackMode": "Режим стека", "strictRoute": "Строгая маршрутизация", "strictRouteDesc": "Использовать строгий режим маршрутизации TUN", "icmpForwarding": "Пересылка ICMP", "icmpForwardingDesc": "Включить поддержку ICMP Ping", "dnsHijack": "Перехват DNS", "dnsHijackDesc": "Перенаправить разбор в модуль DNS", "endpointIndependentNat": "Улучшенный NAT", "endpointIndependentNatDesc": "Включить NAT независимый от конечной точки", "network": "Сеть", "networkDesc": "Настройки сети", "bypassDomain": "Исключить домены", "bypassDomainDesc": "Работает только при включённом системном прокси", "resetTip": "Сбросить настройки?", "regExp": "Регулярное выражение", "icon": "Иконка", "iconConfiguration": "Настройка иконки", "noData": "Нет данных", "adminAutoLaunch": "Автозапуск от администратора", "adminAutoLaunchDesc": "Автозапуск с правами администратора", "fontFamily": "Шрифт", "systemFont": "Системный шрифт", "toggle": "Переключить", "system": "Система", "bypassPrivateRoute": "Обход частной сети", "bypassPrivateRouteDesc": "Автоматически обходить IP-адреса частной сети", "pleaseInputAdminPassword": "Введите пароль администратора", "copyEnvVar": "Копировать переменные окружения", "memoryInfo": "Информация о памяти", "cancel": "Отмена", "fileIsUpdate": "Файл изменён. Сохранить изменения?", "profileHasUpdate": "Конфигурация изменена. Отключить автообновление?", "hasCacheChange": "Сохранить изменения кэша?", "copySuccess": "Скопировано", "success": "Успех", "copyLink": "Копировать ссылку", "exportFile": "Экспорт файла", "cacheCorrupt": "Кэш повреждён. Очистить?", "detectionTip": "Зависит от сторонних API, только для справки", "ipClickBehavior": "Переключение отображения", "ipPrivacyProtection": "Скрыть IP", "manualRefreshIp": "Обновить IP", "tryManualRefresh": "Попробуйте обновить вручную", "refreshAppList": "Обновить список приложений", "refreshAppListConfirm": "Обновить список приложений?", "switchToDomesticIp": "Получить локальный IP", "listen": "Прослушивание", "undo": "Отменить", "redo": "Повторить", "none": "Нет", "basicConfig": "Конфигурация ядра", "basicConfigDesc": "Глобальное изменение конфигурации ядра", "selectedCountTitle": "Выбрано: {count}", "addRule": "Добавить правило", "ruleName": "Имя правила", "content": "Содержимое", "subRule": "Подправило", "ruleTarget": "Цель правила", "sourceIp": "IP источника", "noResolve": "Не разрешать IP", "getOriginRules": "Получить исходные правила", "overrideOriginRules": "Переопределить исходные", "addedOriginRules": "Добавить к исходным", "enableOverride": "Включить переопределение", "saveChanges": "Сохранить изменения?", "generalDesc": "Изменить общие настройки", "findProcessModeDesc": "Включить поиск процесса", "tabAnimationDesc": "Работает только в мобильном режиме", "ua": "User-Agent", "navBarHapticFeedback": "Тактильная отдача", "navBarHapticFeedbackDesc": "Вибрация при переключении нижней панели навигации", "saveTip": "Сохранить изменения?", "colorSchemes": "Цветовые схемы", "palette": "Палитра", "tonalSpotScheme": "Тональный акцент", "fidelityScheme": "Высокая точность", "monochromeScheme": "Монохром", "neutralScheme": "Нейтральный", "vibrantScheme": "Яркий", "expressiveScheme": "Экспрессивный", "contentScheme": "Контентная тема", "rainbowScheme": "Радуга", "fruitSaladScheme": "Фруктовый салат", "developerMode": "Режим разработчика", "developerModeEnableTip": "Режим разработчика включён.", "messageTest": "Тест сообщения", "messageTestTip": "Это тестовое сообщение.", "crashTest": "Тест сбоя", "clearData": "Очистить данные", "zoom": "Масштаб", "textScale": "Масштаб текста", "lightIcon": "Светлая иконка", "lightIconDesc": "Переключить на светлый стиль рабочего стола вручную", "harmonyFont": "Шрифт Harmony", "harmonyFontDesc": "Использовать оптимизированный HarmonyOS Sans", "internet": "Интернет", "systemApp": "Системные приложения", "noNetworkApp": "Приложения без сети", "contactMe": "Связаться со мной", "recoveryStrategy": "Стратегия восстановления", "recoveryStrategy_override": "Перезаписать", "recoveryStrategy_compatible": "Совместимость", "logsTest": "Тест логов", "emptyTip": "{label} не может быть пустым", "urlTip": "{label} должен быть URL", "numberTip": "{label} должен быть числом", "interval": "Интервал", "existsTip": "{label} уже существует", "deleteTip": "Удалить текущий {label}?", "deleteMultipTip": "Удалить выбранные {label}?", "nullTip": "{label} отсутствует", "script": "Скрипт", "color": "Цвет", "rename": "Переименовать", "unnamed": "Без имени", "pleaseEnterScriptName": "Введите название скрипта", "overrideInvalidTip": "Не действует в режиме скрипта", "mixedPort": "Смешанный порт", "socksPort": "Порт Socks", "redirPort": "Порт перенаправления", "tproxyPort": "Порт Tproxy", "portTip": "{label} должен быть от 1024 до 49151", "portConflictTip": "Введите разные порты", "import": "Импорт", "importFromCode": "Импорт из кода", "importFailed": "Ошибка импорта", "importFile": "Импорт из файла", "importUrl": "Импорт по URL", "autoSetSystemDns": "Автоматически настроить системный DNS", "details": "Подробности {label}", "creationTime": "Время создания", "progress": "Прогресс", "host": "Хост", "destination": "Адрес назначения", "destinationGeoIP": "Геолокация назначения", "destinationIPASN": "IP ASN назначения", "specialProxy": "Специальный прокси", "specialRules": "Специальные правила", "remoteDestination": "Удалённое назначение", "networkType": "Тип сети", "proxyChains": "Цепочка прокси", "log": "Лог", "connection": "Активные соединения", "request": "Запрос", "switchLabel": "Переключатель", "noStatusAvailable": "Статус недоступен", "custom": "Пользовательский", "wakelock": "Блокировка сна", "wakelockDescription": "Эта функция не требует специальных разрешений, так как использует только блокировку пробуждения экрана, а не CPU. Приложение остаётся активным в фоне, экран не гаснет автоматически, что полезно в некоторых сценариях.", "tunEnableRequireAdmin": "Для включения виртуального адаптера требуются права администратора. Запустите программу от имени администратора.", "restartTip": "Изменения вступят в силу после перезапуска TUN", "restart": "Перезапуск", "restartCoreTitle": "Перезапуск ядра", "restartCoreDesc": "Перезапустить ядро вручную?", "highRefreshRate": "Высокая частота обновления", "highRefreshRateDesc": "Включить поддержку максимальной частоты обновления устройства", "onlinePanel": "Онлайн-панель", "openDashboard": "Открыть Zashboard", "tolerance": "Допуск" } ================================================ FILE: arb/intl_zh_CN.arb ================================================ { "rule": "规则", "global": "全局", "direct": "直连", "dashboard": "首页", "proxies": "代理", "profile": "配置", "profiles": "配置", "tools": "更多", "logs": "日志", "logsDesc": "查看日志捕获记录", "resources": "资源", "syncAll": "全部同步", "syncFailed": "同步失败", "resourcesDesc": "外部资源相关信息", "scriptDesc": "配置全局覆写脚本", "trafficUsage": "流量统计", "coreInfo": "内核信息", "networkSpeed": "网络速度", "outboundMode": "出站模式", "networkDetection": "网络检测", "upload": "上传", "download": "下载", "noProxy": "暂无代理", "noProxyDesc": "请创建配置或者添加有效配置文件", "nullProfileDesc": "没有配置文件,请先添加配置文件", "settings": "设置", "language": "语言", "defaultText": "默认", "more": "查看", "other": "其他", "otherSettings": "增强工具", "otherSettingsDesc": "修改增强工具设置", "smartAutoStop": "智能启停", "smartAutoStopDesc": "连接指定网络后停止代理服务", "networkMatch": "网络匹配", "networkMatchHint": "输入IP或CIDR,最多2个,以逗号分隔", "smartAutoStopServiceRunning": "智能启停服务运行中", "serviceRunning": "服务正在运行中", "coreConnected": "已连接", "coreSuspended": "已挂起", "invalidIpFormat": "无效的IP或CIDR格式", "tooManyRules": "最多允许2个规则", "dozeSuspend": "休眠支持", "dozeSuspendDesc": "开启后同步系统Doze休眠模式", "storeFix": "商店修复", "storeFixDesc": "修复Google Play商店下载异常", "disableQuic": "禁用QUIC", "disableQuicDesc": "禁用QUIC以解决特定网络问题", "excludeChina": "排除国内", "excludeChinaDesc": "放行中国QUIC流量而非全部禁用", "fcmOptimization": "FCM优化", "fcmOptimizationDesc": "增强FCM直连时的网络稳定性", "quickResponse": "快速响应", "quickResponseDesc": "网络发生变化时主动断开连接", "networkFix": "网络修复", "networkFixDesc": "修复Windows网络检测地球图标问题", "batteryOptimization": "电池优化", "batteryOptimizationDesc": "请求安卓电池优化白名单权限", "alreadyInWhitelist": "当前应用已在白名单内", "about": "关于", "en": "英语", "ja": "日语", "ru": "俄语", "zh_CN": "简体中文", "zh_TC": "繁体中文", "theme": "主题", "themeDesc": "设置主题色彩及图标", "override": "覆写", "overrideDesc": "覆写代理相关配置", "allowLan": "局域网代理", "allowLanDesc": "允许通过局域网访问代理", "tun": "虚拟网卡", "tunDesc": "接管当前设备全局流量", "minimizeOnExit": "退出最小化", "minimizeOnExitDesc": "修改系统默认退出事件", "autoLaunch": "开机启动", "autoLaunchDesc": "跟随系统自启动", "smartDelayLaunch": "智能延迟", "smartDelayLaunchDesc": "在网络成功连接以后启动", "silentLaunch": "静默启动", "silentLaunchDesc": "不打开软件直接在后台启动", "autoRun": "自动连接", "autoRunDesc": "应用打开后自动连接", "logcat": "日志捕获", "logcatDesc": "开启后将会显示日志入口", "enableCrashReport": "应用崩溃分析", "enableCrashReportDesc": "必要时上传应用崩溃日志", "autoCheckUpdate": "自动检查更新", "autoCheckUpdateDesc": "应用启动时自动检查更新", "accessControl": "访问控制", "accessControlDesc": "配置应用访问代理", "clearCacheTitle": "清理缓存", "clearCacheDesc": "是否需要清理FakeIP&DNS缓存?", "forceGCTitle": "强制垃圾回收", "forceGCDesc": "是否进行强制内核垃圾回收?实验性功能,请谨慎使用", "fcmTip": "FCM连接和支持取决于设备本身,显示结果仅供参考。因系统权限原因,您需要关闭网络中的\"允许绕过VPN\"选项,以获得更加准确的结果", "application": "应用程序", "applicationDesc": "修改应用程序设置", "edit": "编辑", "confirm": "确定", "update": "更新", "add": "添加", "save": "保存", "delete": "删除", "years": "年", "months": "月", "hours": "小时", "days": "天", "minutes": "分钟", "seconds": "秒", "ago": "前", "pleaseCloseTunFirst": "请先关闭虚拟网卡", "pleaseCloseSystemProxyFirst": "请先关闭系统代理", "just": "刚刚", "qrcode": "二维码", "qrcodeDesc": "扫描二维码获取配置文件", "clipboard": "剪切板", "clipboardDesc": "自动获取剪切板订阅链接", "url": "URL", "urlDesc": "通过URL获取配置文件", "file": "文件", "fileDesc": "直接上传配置文件", "name": "名称", "profileNameNullValidationDesc": "请输入配置名称", "profileUrlNullValidationDesc": "请输入配置URL", "profileUrlInvalidValidationDesc": "请输入有效配置URL", "autoUpdate": "自动更新", "autoUpdateInterval": "自动更新间隔(分钟)", "profileAutoUpdateIntervalNullValidationDesc": "请输入自动更新间隔时间", "profileAutoUpdateIntervalInvalidValidationDesc": "请输入有效间隔时间格式", "themeMode": "主题模式", "themeColor": "主题色彩", "preview": "预览", "runtimeConfig": "运行时配置", "cameraPermissionDenied": "相机权限被拒绝", "cameraPermissionDesc": "扫描二维码需要相机权限,请在设置中授予相机权限。", "openSettings": "打开设置", "retry": "重试", "packageListPermissionRequired": "此功能需要访问已安装应用列表的权限。是否授予此权限?", "packageListPermissionDenied": "权限被拒绝。没有权限无法访问应用列表。", "auto": "自动", "light": "浅色", "dark": "深色", "importFromURL": "从URL导入", "submit": "提交", "doYouWantToPass": "是否要通过", "create": "创建", "defaultSort": "按默认排序", "delaySort": "按延迟排序", "nameSort": "按名称排序", "pleaseUploadFile": "请上传文件", "pleaseUploadValidQrcode": "请上传有效的二维码", "blacklistMode": "黑名单模式", "whitelistMode": "白名单模式", "filterSystemApp": "过滤系统应用", "cancelFilterSystemApp": "取消过滤系统应用", "selectAll": "全选", "cancelSelectAll": "取消全选", "appAccessControl": "应用访问控制", "accessControlAllowDesc": "只允许选中的应用进入VPN", "accessControlNotAllowDesc": "选中的应用将被排除在VPN之外", "selected": "已选择", "unableToUpdateCurrentProfileDesc": "无法更新当前配置文件", "noMoreInfoDesc": "暂无更多信息", "profileParseErrorDesc": "配置文件解析错误", "proxyPort": "代理端口", "proxyPortDesc": "设置Clash监听端口", "port": "端口", "logLevel": "日志等级", "show": "显示", "exit": "退出", "systemProxy": "系统代理", "project": "项目", "core": "内核", "tabAnimation": "切换动画", "desc": "Bettbox基于强大灵活的Mihomo(Clash.Meta)代理内核,致力于更好的体验,Forked form FlClash,Better Experience, Out of the box", "startVpn": "正在启动", "stopVpn": "正在停止", "discovery": "发现新版本", "compatible": "兼容模式", "compatibleDesc": "开启将失去部分应用能力,获得全量的Clash的支持", "notSelectedTip": "当前代理组无法选中", "tip": "提示", "backupAndRecovery": "备份与恢复", "backupAndRecoveryDesc": "通过在线或本地文件同步数据", "account": "账号", "backup": "备份", "recovery": "恢复", "recoveryProfiles": "仅恢复配置文件", "recoveryAll": "恢复所有数据", "recoverySuccess": "恢复成功", "backupSuccess": "备份成功", "noInfo": "暂无信息", "pleaseBindWebDAV": "请绑定WebDAV", "bind": "绑定", "connectivity": "连通性:", "webDAVConfiguration": "WebDAV配置", "address": "地址", "addressHelp": "WebDAV服务器地址", "addressTip": "请输入有效的WebDAV地址", "password": "密码", "checkUpdate": "检查更新", "discoverNewVersion": "发现新版本", "checkUpdateError": "当前应用已经是最新版了", "goDownload": "前往下载", "unknown": "未知", "geoData": "地理数据", "externalResources": "外部资源", "checking": "检测中...", "country": "区域", "checkError": "检测失败", "search": "搜索", "allowBypass": "允许绕过VPN", "allowBypassDesc": "开启后部分应用可绕过VPN", "externalController": "外部控制", "externalControllerDesc": "通过在线端口控制内核", "controlSecret": "控制密码", "controlSecretDesc": "RESTful API的访问密码", "generateSecret": "生成", "secretCopied": "密码已复制到剪贴板", "ipv6Desc": "开启后将可以接收IPv6流量", "app": "应用", "general": "常规", "vpnSystemProxyDesc": "为VpnService附加HTTP代理", "systemProxyDesc": "设置系统代理", "unifiedDelay": "统一延迟", "unifiedDelayDesc": "去除握手解析等额外延迟", "tcpConcurrent": "TCP并发", "tcpConcurrentDesc": "开启后允许TCP并发连接", "geodataLoader": "GEO节能", "geodataLoaderDesc": "开启后使用GEO低内存加载器", "requests": "请求", "requestsDesc": "查看最近请求记录", "findProcessMode": "查找进程", "init": "初始化", "infiniteTime": "长期有效", "expirationTime": "到期时间", "connections": "连接", "connectionsDesc": "查看当前连接数据", "intranetIP": "内网 IP", "view": "查看", "cut": "剪切", "copy": "复制", "paste": "粘贴", "testUrl": "测试链接", "startTest": "延迟测试", "addProfile": "添加配置", "customUrl": "自定义URL", "sync": "同步", "exclude": "后台隐藏", "excludeDesc": "从最近任务中隐藏应用", "oneColumn": "一列", "twoColumns": "两列", "threeColumns": "三列", "fourColumns": "四列", "expand": "标准", "shrink": "紧凑", "min": "最小", "tab": "标签页", "list": "列表", "delay": "延迟", "style": "风格", "size": "尺寸", "delayAnimation": "延迟动画", "delayAnimationDesc": "自定义测试过程中的动画显示", "noAnimation": "默认", "rotatingCircle": "单圆自转", "pulse": "脉冲律动", "spinningLines": "流光旋绕", "threeInOut": "三星舒合", "threeBounce": "灵珠跃动", "circle": "圆环流转", "fadingCircle": "环影隐渐", "fadingFour": "四方烁动", "wave": "波浪起伏", "doubleBounce": "双重弹奏", "sort": "排序", "columns": "列数", "proxiesSetting": "代理设置", "proxyGroup": "代理组", "go": "前往", "externalLink": "外部链接", "otherContributors": "其他贡献者", "autoCloseConnections": "自动关闭连接", "autoCloseConnectionsDesc": "切换节点后自动关闭连接", "onlyStatisticsProxy": "代理流量统计", "onlyStatisticsProxyDesc": "开启后将只统计代理流量", "pureBlackMode": "纯黑模式", "keepAliveIntervalDesc": "TCP保持活动间隔", "entries": "个条目", "local": "本地", "remote": "远程", "remoteBackupDesc": "备份数据到WebDAV", "remoteRecoveryDesc": "通过WebDAV恢复数据", "localBackupDesc": "备份数据到本地", "localRecoveryDesc": "通过文件恢复数据", "mode": "模式", "time": "时间", "source": "来源", "allApps": "所有应用", "onlyOtherApps": "仅第三方应用", "action": "操作", "intelligentSelected": "智能选择", "clipboardImport": "剪贴板导入", "clipboardExport": "导出剪贴板", "layout": "布局", "tight": "紧凑", "standard": "标准", "loose": "宽松", "profilesSort": "配置排序", "start": "启动", "stop": "停止", "powerSwitch": "启动开关", "runTime": "启动时间", "checkOrAddProfile": "请先添加配置", "serviceReady": "服务已就绪", "appDesc": "处理应用相关设置", "vpnDesc": "修改VPN相关设置", "dnsDesc": "更新DNS相关设置", "key": "键", "value": "值", "hostsDesc": "追加当前配置Hosts", "vpnTip": "重启VPN后改变生效", "vpnEnableDesc": "通过VpnService自动路由系统所有流量", "options": "选项", "loopback": "回环解锁工具", "loopbackDesc": "用于UWP回环解锁", "providers": "提供者", "proxyProviders": "代理提供者", "ruleProviders": "规则提供者", "advancedSettings": "进阶设置", "nodeExclusion": "节点排除", "nodeExclusionDesc": "排除所有匹配到的节点", "nodeExclusionPlaceholder": "HK|香港|🇭🇰", "formatError": "请检查格式是否正确", "healthCheckTimeout": "超时时间", "healthCheckTimeoutDesc": "节点健康检查超时时间", "concurrencyLimit": "并发限制", "concurrencyLimitDesc": "延迟测试的最大并发数量", "notRecommended": "不推荐", "overrideDns": "覆写DNS", "overrideDnsDesc": "开启后将覆盖配置中的DNS选项", "overrideTestUrl": "覆盖配置", "ntp": "NTP", "ntpDesc": "使用NTP时间服务", "overrideNtp": "覆写NTP", "overrideNtpDesc": "开启后将覆盖配置中的NTP选项", "ntpStatus": "状态", "ntpStatusDesc": "开启NTP时间服务", "writeToSystem": "写入系统", "writeToSystemDesc": "需要管理员权限", "ntpServer": "服务器", "ntpPort": "端口", "ntpInterval": "更新时间", "sniffer": "Sniffer", "snifferDesc": "修改域名嗅探配置", "overrideSniffer": "覆写Sniffer", "overrideSnifferDesc": "开启后将覆盖配置中的Sniffer选项", "snifferStatus": "状态", "snifferStatusDesc": "开启嗅探服务设置", "forceDnsMapping": "强制 DNS 映射", "forceDnsMappingDesc": "强制将 DNS 查询结果映射到连接", "parsePureIp": "解析纯 IP 连接", "parsePureIpDesc": "解析纯 IP 连接", "overrideDestination": "覆盖目标地址", "overrideDestinationDesc": "使用嗅探结果覆盖连接目标地址", "httpPortSniffer": "HTTP 端口嗅探", "tlsPortSniffer": "TLS 端口嗅探", "quicPortSniffer": "QUIC 端口嗅探", "forceDomain": "强制嗅探域名", "skipDomain": "跳过域名", "skipSrcAddress": "跳过来源 IP", "skipDstAddress": "跳过目标 IP", "snifferPorts": "端口", "snifferPortsHint": "例如: 80, 8080-8880", "snifferDomainHint": "每行一个域名", "snifferAddressHint": "每行一个地址", "tunnel": "Tunnel", "tunnelDesc": "使用流量转发隧道", "overrideTunnel": "覆写Tunnel", "overrideTunnelDesc": "开启后将覆盖配置中的Tunnel选项", "tunnelList": "转发列表", "addTunnel": "添加转发", "editTunnel": "编辑转发", "deleteTunnel": "删除转发", "tunnelNetwork": "网络协议", "tunnelNetworkHint": "例如: tcp, udp", "tunnelAddress": "监听地址", "tunnelAddressHint": "例如: 127.0.0.1:6553", "tunnelTarget": "目标地址", "tunnelTargetHint": "例如: 114.114.114.114:53", "tunnelProxy": "代理名称", "tunnelProxyHint": "例如: proxy (可选)", "experimental": "Experimental", "experimentalDesc": "实验性配置请谨慎使用", "overrideExperimental": "覆写Experimental", "overrideExperimentalDesc": "开启后将覆盖配置中的实验性配置", "quicGoDisableGso": "禁用QUIC通用分段卸载", "quicGoDisableGsoDesc": "禁用 QUIC 的通用分段卸载功能", "quicGoDisableEcn": "禁用QUIC显式拥塞通知", "quicGoDisableEcnDesc": "禁用 QUIC 的显式拥塞通知功能", "dialerIp4pConvert": "启用拨号IP4P地址转换", "dialerIp4pConvertDesc": "启用拨号器的 IP4P 地址转换功能", "status": "状态", "statusDesc": "关闭后将使用系统DNS", "preferH3Desc": "优先使用DOH的http/3", "cacheAlgorithm": "缓存算法", "respectRules": "遵守规则", "respectRulesDesc": "DNS连接跟随Rules", "dnsMode": "DNS模式", "fakeipRange": "FakeIP范围", "fakeipRangeV6": "FakeIPv6范围", "fakeIpFilterMode": "FakeIP过滤模式", "fakeIpFilterModeDesc": "指定FakeIP过滤模式", "blacklist": "黑名单", "whitelist": "白名单", "fakeipFilter": "FakeIP过滤列表", "fakeipTtl": "FakeIP有效时间", "defaultNameserver": "默认域名服务器", "defaultNameserverDesc": "用于解析DNS服务器", "nameserver": "域名服务器", "nameserverDesc": "用于解析域名", "useHosts": "使用Hosts", "useSystemHosts": "使用系统Hosts", "nameserverPolicy": "域名服务器策略", "nameserverPolicyDesc": "指定对应域名服务器策略", "proxyNameserver": "代理域名服务器", "proxyNameserverDesc": "用于解析代理节点的域名", "directNameserver": "直连域名服务器", "directNameserverDesc": "用于解析直连出口的域名", "directNameserverFollowPolicy": "直连DNS遵循规则", "fallback": "Fallback", "fallbackDesc": "一般情况下使用境外DNS", "fallbackFilter": "Fallback过滤", "geoipCode": "Geoip代码", "ipcidr": "IP/掩码", "domain": "域名", "reset": "重置", "action_view": "显示/隐藏", "action_start": "启动/停止", "action_mode": "切换模式", "action_proxy": "系统代理", "action_tun": "虚拟网卡", "disclaimer": "免责声明", "disclaimerDesc": "本软件为开源免费软件,仅供学习交流等非商业性质的个人测试使用,代理服务商的行为均与本软件无关,同意声明代表您已完全知晓并确认了这一点,如不同意,请选择退出!", "agree": "同意", "hotkeyManagement": "快捷键管理", "hotkeyManagementDesc": "使用键盘控制应用程序", "pressKeyboard": "请按下按键", "inputCorrectHotkey": "请输入正确的快捷键", "hotkeyConflict": "快捷键冲突", "remove": "移除", "noHotKey": "暂无快捷键", "noNetwork": "无网络", "ipv6InboundDesc": "允许IPv6入站", "exportLogs": "导出日志", "exportSuccess": "导出成功", "iconStyle": "图标样式", "onlyIcon": "仅图标", "noIcon": "无图标", "stackMode": "栈模式", "strictRoute": "严格路由", "strictRouteDesc": "使用TUN严格路由模式", "icmpForwarding": "ICMP转发", "icmpForwardingDesc": "开启后将支持ICMP Ping", "dnsHijack": "DNS劫持", "dnsHijackDesc": "将解析导入内部DNS模块", "endpointIndependentNat": "NAT增强", "endpointIndependentNatDesc": "启用独立于端点的NAT", "network": "网络", "networkDesc": "修改网络相关设置", "bypassDomain": "排除域名", "bypassDomainDesc": "仅在系统代理启用时生效", "resetTip": "确定要重置吗?", "regExp": "正则", "icon": "图片", "iconConfiguration": "图片配置", "noData": "暂无数据", "adminAutoLaunch": "管理员自启动", "adminAutoLaunchDesc": "使用管理员模式开机自启动", "fontFamily": "字体", "systemFont": "系统字体", "toggle": "切换", "system": "系统", "bypassPrivateRoute": "绕过私有网络", "bypassPrivateRouteDesc": "自动绕过私有网络IP地址", "pleaseInputAdminPassword": "请输入管理员密码", "copyEnvVar": "复制环境变量", "memoryInfo": "内存信息", "cancel": "取消", "fileIsUpdate": "文件有修改,是否保存修改", "profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ", "hasCacheChange": "是否缓存修改", "copySuccess": "复制成功", "success": "Success", "copyLink": "复制链接", "exportFile": "导出文件", "cacheCorrupt": "缓存已损坏,是否清空?", "detectionTip": "依赖第三方api,仅供参考", "ipClickBehavior": "显示切换", "ipPrivacyProtection": "隐藏IP显示", "manualRefreshIp": "重新获取IP", "tryManualRefresh": "请尝试手动刷新", "refreshAppList": "刷新应用列表", "refreshAppListConfirm": "是否刷新应用列表?", "switchToDomesticIp": "获取国内IP", "listen": "监听", "undo": "撤销", "redo": "重做", "none": "无", "basicConfig": "内核配置", "basicConfigDesc": "全局修改内核配置", "selectedCountTitle": "已选择 {count} 项", "addRule": "添加规则", "ruleName": "规则名称", "content": "内容", "subRule": "子规则", "ruleTarget": "规则目标", "sourceIp": "源IP", "noResolve": "不解析IP", "getOriginRules": "获取原始规则", "overrideOriginRules": "覆盖原始规则", "addedOriginRules": "附加到原始规则", "enableOverride": "启用覆写", "saveChanges": "是否保存更改?", "generalDesc": "修改通用设置", "findProcessModeDesc": "开启后会将可以查找进程", "tabAnimationDesc": "仅在部分移动视图中有效", "navBarHapticFeedback": "触感反馈", "navBarHapticFeedbackDesc": "底部导航栏切换震动反馈", "saveTip": "确定要保存吗?", "colorSchemes": "配色方案", "palette": "调色板", "tonalSpotScheme": "调性点缀", "fidelityScheme": "高保真", "monochromeScheme": "单色", "neutralScheme": "中性", "vibrantScheme": "活力", "expressiveScheme": "表现力", "contentScheme": "内容主题", "rainbowScheme": "彩虹", "fruitSaladScheme": "果缤纷", "developerMode": "开发者模式", "developerModeEnableTip": "开发者模式已启用。", "messageTest": "消息测试", "messageTestTip": "这是一条消息。", "crashTest": "崩溃测试", "clearData": "清除数据", "zoom": "缩放", "textScale": "文本缩放", "lightIcon": "丹青留白", "lightIconDesc": "手动切换浅色系桌面应用图标", "harmonyFont": "鸿蒙字体", "harmonyFontDesc": "使用优化的HarmonyOS Sans", "internet": "互联网", "systemApp": "系统应用", "noNetworkApp": "无网络应用", "contactMe": "联系我", "recoveryStrategy": "恢复策略", "recoveryStrategy_override": "覆盖", "recoveryStrategy_compatible": "兼容", "logsTest": "日志测试", "emptyTip": "{label}不能为空", "urlTip": "{label}必须为URL", "numberTip": "{label}必须为数字", "interval": "间隔", "existsTip": "{label}当前已存在", "deleteTip": "确定删除当前{label}吗?", "deleteMultipTip": "确定删除选中的{label}吗?", "nullTip": "暂无{label}", "script": "脚本", "color": "颜色", "rename": "重命名", "unnamed": "未命名", "pleaseEnterScriptName": "请输入脚本名称", "overrideInvalidTip": "在脚本模式下不生效", "mixedPort": "混合端口", "socksPort": "Socks端口", "redirPort": "Redir端口", "tproxyPort": "Tproxy端口", "portTip": "{label} 必须在 1024 到 49151 之间", "portConflictTip": "请输入不同的端口", "import": "导入", "importFromCode": "通过代码导入", "importFailed": "导入失败", "importFile": "通过文件导入", "importUrl": "通过URL导入", "autoSetSystemDns": "自动设置系统DNS", "details": "{label}详情", "creationTime": "创建时间", "progress": "进程", "host": "主机", "destination": "目标地址", "destinationGeoIP": "目标地理定位", "destinationIPASN": "目标IP ASN", "specialProxy": "特殊代理", "specialRules": "特殊规则", "remoteDestination": "远程目标", "networkType": "网络类型", "proxyChains": "代理链", "log": "日志", "connection": "活跃连接", "request": "请求", "switchLabel": "开关", "noStatusAvailable": "未获取到状态", "onlinePanel": "在线面板", "openDashboard": "打开 Zashboard", "custom": "自定义", "wakelock": "亮屏锁", "wakelockDescription": "本功能不需要任何特殊权限,因为它仅启用屏幕唤醒锁,而不是任何CPU唤醒锁,应用会在后台保持必要的活跃,且屏幕不会自动熄灭,这在一些场景下会很有用", "tunEnableRequireAdmin": "启用虚拟网卡需要管理员权限,请以管理员身份运行程序", "restartTip": "重启TUN后改变生效", "restart": "重启", "restartCoreTitle": "重启内核", "restartCoreDesc": "是否手动重启内核?", "highRefreshRate": "高刷新率", "highRefreshRateDesc": "启用设备最高刷新率支持" } ================================================ FILE: arb/intl_zh_TC.arb ================================================ { "rule": "規則", "global": "全域", "direct": "直連", "dashboard": "首頁", "proxies": "代理", "profile": "配置", "profiles": "配置", "tools": "更多", "logs": "日誌", "logsDesc": "查看日誌擷取記錄", "resources": "資源", "syncAll": "全部同步", "syncFailed": "同步失敗", "resourcesDesc": "外部資源相關資訊", "scriptDesc": "配置全局覆寫腳本", "trafficUsage": "流量統計", "coreInfo": "內核資訊", "networkSpeed": "網路速度", "outboundMode": "出站模式", "networkDetection": "網路檢測", "upload": "上傳", "download": "下載", "noProxy": "暫無代理", "noProxyDesc": "請建立配置或新增有效的設定檔", "nullProfileDesc": "沒有設定檔,請先新增設定檔", "settings": "設定", "language": "語言", "defaultText": "預設", "more": "查看", "other": "其他", "otherSettings": "增強工具", "otherSettingsDesc": "修改增強工具設定", "smartAutoStop": "智慧啟停", "smartAutoStopDesc": "連線指定網路後停止代理服務", "networkMatch": "網路匹配", "networkMatchHint": "輸入 IP 或 CIDR,最多 2 個,以逗號分隔", "smartAutoStopServiceRunning": "智慧啟停服務執行中", "serviceRunning": "服務正在執行中", "coreConnected": "已連線", "coreSuspended": "已掛起", "invalidIpFormat": "無效的 IP 或 CIDR 格式", "tooManyRules": "最多允許 2 個規則", "dozeSuspend": "休眠支援", "dozeSuspendDesc": "開啟後同步系統 Doze 休眠模式", "storeFix": "商店修復", "storeFixDesc": "修復 Google Play 商店下載異常", "disableQuic": "禁用QUIC", "disableQuicDesc": "禁用QUIC以解決特定網路問題", "excludeChina": "排除國內", "excludeChinaDesc": "放行中國QUIC流量而非全部禁用", "fcmOptimization": "FCM 優化", "fcmOptimizationDesc": "增強 FCM 直連時的網路穩定性", "quickResponse": "快速響應", "quickResponseDesc": "網路發生變化時主動斷開連接", "networkFix": "網路修復", "networkFixDesc": "修復 Windows 網路檢測地球圖示問題", "batteryOptimization": "電池最佳化", "batteryOptimizationDesc": "請求 Android 電池最佳化白名單權限", "alreadyInWhitelist": "目前應用程式已在白名單內", "about": "關於", "en": "英語", "ja": "日語", "ru": "俄語", "zh_CN": "簡體中文", "zh_TC": "繁體中文", "theme": "主題", "themeDesc": "設定主題色彩及圖示", "override": "覆寫", "overrideDesc": "覆寫代理相關配置", "allowLan": "區域網路代理", "allowLanDesc": "允許透過區域網路存取代理", "tun": "虛擬網卡", "tunDesc": "接管目前裝置全域流量", "minimizeOnExit": "退出最小化", "minimizeOnExitDesc": "修改系統預設退出事件", "autoLaunch": "開機啟動", "autoLaunchDesc": "跟隨系統自動啟動", "smartDelayLaunch": "智慧延遲", "smartDelayLaunchDesc": "在網路成功連線以後啟動", "silentLaunch": "靜默啟動", "silentLaunchDesc": "不打開軟體直接在背景啟動", "autoRun": "自動連線", "autoRunDesc": "應用打開後自動連線", "logcat": "日誌捕獲", "logcatDesc": "開啟後將會顯示日誌入口", "enableCrashReport": "應用崩潰分析", "enableCrashReportDesc": "必要時上傳應用崩潰日誌", "autoCheckUpdate": "自動檢查更新", "autoCheckUpdateDesc": "應用啟動時自動檢查更新", "accessControl": "存取控制", "accessControlDesc": "設定應用存取代理", "clearCacheTitle": "清理快取", "clearCacheDesc": "是否需要清理 FakeIP & DNS 快取?", "forceGCTitle": "強制垃圾回收", "forceGCDesc": "是否進行強制內核垃圾回收?實驗性功能,請謹慎使用", "fcmTip": "FCM 連線和支援取決於裝置本身,顯示結果僅供參考。因系統權限原因,您需要關閉網路中的 \"允許繞過 VPN\" 選項,以獲得更加準確的結果", "application": "應用程式", "applicationDesc": "修改應用程式設定", "edit": "編輯", "confirm": "確定", "update": "更新", "add": "新增", "save": "儲存", "delete": "刪除", "years": "年", "months": "月", "hours": "小時", "days": "天", "minutes": "分鐘", "seconds": "秒", "ago": "前", "pleaseCloseTunFirst": "請先關閉虛擬網卡", "pleaseCloseSystemProxyFirst": "請先關閉系統代理", "just": "剛剛", "qrcode": "二維碼", "qrcodeDesc": "掃描二維碼獲取設定檔", "clipboard": "剪貼簿", "clipboardDesc": "自動獲取剪貼簿訂閱連結", "url": "URL", "urlDesc": "透過 URL 獲取設定檔", "file": "檔案", "fileDesc": "直接上傳設定檔", "name": "名稱", "profileNameNullValidationDesc": "請輸入配置名稱", "profileUrlNullValidationDesc": "請輸入配置 URL", "profileUrlInvalidValidationDesc": "請輸入有效配置 URL", "autoUpdate": "自動更新", "autoUpdateInterval": "自動更新間隔(分鐘)", "profileAutoUpdateIntervalNullValidationDesc": "請輸入自動更新間隔時間", "profileAutoUpdateIntervalInvalidValidationDesc": "請輸入有效間隔時間格式", "themeMode": "主題模式", "themeColor": "主題色彩", "preview": "預覽", "runtimeConfig": "執行時配置", "cameraPermissionDenied": "相機權限被拒絕", "cameraPermissionDesc": "掃描二維碼需要相機權限,請在設定中授予相機權限。", "openSettings": "打開設定", "retry": "重試", "packageListPermissionRequired": "此功能需要存取已安裝應用程式清單的權限。是否授予此權限?", "packageListPermissionDenied": "權限被拒絕。沒有權限無法存取應用程式清單。", "auto": "自動", "light": "淺色", "dark": "深色", "importFromURL": "從 URL 匯入", "submit": "提交", "doYouWantToPass": "是否要通過", "create": "建立", "defaultSort": "按預設排序", "delaySort": "按延遲排序", "nameSort": "按名稱排序", "pleaseUploadFile": "請上傳檔案", "pleaseUploadValidQrcode": "請上傳有效的二維碼", "blacklistMode": "黑名單模式", "whitelistMode": "白名單模式", "filterSystemApp": "過濾系統應用程式", "cancelFilterSystemApp": "取消過濾系統應用程式", "selectAll": "全選", "cancelSelectAll": "取消全選", "appAccessControl": "應用存取控制", "accessControlAllowDesc": "只允許選取的應用程式進入 VPN", "accessControlNotAllowDesc": "選取的應用程式將被排除在VPN之外", "selected": "已選擇", "unableToUpdateCurrentProfileDesc": "無法更新目前的設定檔", "noMoreInfoDesc": "暫無更多資訊", "profileParseErrorDesc": "設定檔解析錯誤", "proxyPort": "代理連接埠", "proxyPortDesc": "設定 Clash 監聽連接埠", "port": "連接埠", "logLevel": "日誌等級", "show": "顯示", "exit": "退出", "systemProxy": "系統代理", "project": "專案", "core": "內核", "tabAnimation": "切換動畫", "desc": "Bettbox 基於強大靈活的 Mihomo (Clash.Meta) 代理內核,致力於提供更好的體驗,Forked from FlClash,Better Experience, Out of the box", "startVpn": "正在啟動", "stopVpn": "正在停止", "discovery": "發現新版本", "compatible": "相容模式", "compatibleDesc": "開啟將失去部分應用能力,獲得全量的 Clash 支援", "notSelectedTip": "目前的代理群組無法選取", "tip": "提示", "backupAndRecovery": "備份與還原", "backupAndRecoveryDesc": "透過線上或本機檔案同步資料", "account": "帳號", "backup": "備份", "recovery": "還原", "recoveryProfiles": "僅還原設定檔", "recoveryAll": "還原所有資料", "recoverySuccess": "還原成功", "backupSuccess": "備份成功", "noInfo": "暫無資訊", "pleaseBindWebDAV": "請綁定 WebDAV", "bind": "綁定", "connectivity": "連通性:", "webDAVConfiguration": "WebDAV 設定", "address": "地址", "addressHelp": "WebDAV 伺服器地址", "addressTip": "請輸入有效的 WebDAV 地址", "password": "密碼", "checkUpdate": "檢查更新", "discoverNewVersion": "發現新版本", "checkUpdateError": "目前的應用程式已經是最新版了", "goDownload": "前往下載", "unknown": "未知", "geoData": "地理資料", "externalResources": "外部資源", "checking": "檢測中...", "country": "區域", "checkError": "檢測失敗", "search": "搜尋", "allowBypass": "允許繞過 VPN", "allowBypassDesc": "開啟後部分應用程式可繞過 VPN", "externalController": "外部控制", "externalControllerDesc": "透過線上連接埠控制內核", "controlSecret": "控制密碼", "controlSecretDesc": "RESTful API 的存取密碼", "generateSecret": "產生", "secretCopied": "密碼已複製到剪貼簿", "ipv6Desc": "開啟後將可以接收 IPv6 流量", "app": "應用", "general": "一般", "vpnSystemProxyDesc": "為 VpnService 附加 HTTP 代理", "systemProxyDesc": "設定系統代理", "unifiedDelay": "統一延遲", "unifiedDelayDesc": "去除交握解析等額外延遲", "tcpConcurrent": "TCP 並發", "tcpConcurrentDesc": "開啟後允許 TCP 並發連線", "geodataLoader": "GEO 節能", "geodataLoaderDesc": "開啟後使用 GEO 低記憶體載入器", "requests": "請求", "requestsDesc": "查看最近請求記錄", "findProcessMode": "尋找處理程序", "init": "初始化", "infiniteTime": "長期有效", "expirationTime": "到期時間", "connections": "連線", "connectionsDesc": "查看目前連線資料", "intranetIP": "內網 IP", "view": "查看", "cut": "剪下", "copy": "複製", "paste": "貼上", "testUrl": "測試連結", "startTest": "延遲測試", "addProfile": "新增配置", "customUrl": "自訂 URL", "sync": "同步", "exclude": "背景隱藏", "excludeDesc": "從最近任務中隱藏應用程式", "oneColumn": "一列", "twoColumns": "兩列", "threeColumns": "三列", "fourColumns": "四列", "expand": "標準", "shrink": "緊湊", "min": "最小", "tab": "分頁", "list": "清單", "delay": "延遲", "style": "風格", "size": "尺寸", "delayAnimation": "延遲動畫", "delayAnimationDesc": "自訂測試過程中的動畫顯示", "noAnimation": "預設", "rotatingCircle": "單圓自轉", "pulse": "脈衝律動", "spinningLines": "流光旋繞", "threeInOut": "三星舒合", "threeBounce": "靈珠躍動", "circle": "圓環流轉", "fadingCircle": "環影隱漸", "fadingFour": "四方爍動", "wave": "波浪起伏", "doubleBounce": "雙重彈奏", "sort": "排序", "columns": "列數", "proxiesSetting": "代理設定", "proxyGroup": "代理群組", "go": "前往", "externalLink": "外部連結", "otherContributors": "其他貢獻者", "autoCloseConnections": "自動關閉連線", "autoCloseConnectionsDesc": "切換節點後自動關閉連線", "onlyStatisticsProxy": "代理流量統計", "onlyStatisticsProxyDesc": "開啟後將只統計代理流量", "pureBlackMode": "純黑模式", "keepAliveIntervalDesc": "TCP 保持活動間隔", "entries": "個項目", "local": "本機", "remote": "遠端", "remoteBackupDesc": "備份資料到 WebDAV", "remoteRecoveryDesc": "透過 WebDAV 還原資料", "localBackupDesc": "備份資料到本機", "localRecoveryDesc": "透過檔案還原資料", "mode": "模式", "time": "時間", "source": "來源", "allApps": "所有應用程式", "onlyOtherApps": "僅第三方應用程式", "action": "操作", "intelligentSelected": "智慧選擇", "clipboardImport": "剪貼簿匯入", "clipboardExport": "匯出剪貼簿", "layout": "版面配置", "tight": "緊湊", "standard": "標準", "loose": "寬鬆", "profilesSort": "配置排序", "start": "啟動", "stop": "停止", "powerSwitch": "啟動開關", "runTime": "啟動時間", "checkOrAddProfile": "請先新增配置", "serviceReady": "服務已就緒", "appDesc": "處理應用相關設定", "vpnDesc": "修改VPN相關設定", "dnsDesc": "更新 DNS 相關設定", "key": "鍵", "value": "值", "hostsDesc": "附加目前配置 Hosts", "vpnTip": "重啟VPN後改變生效", "vpnEnableDesc": "透過 VpnService 自動路由系統所有流量", "options": "選項", "loopback": "迴環解鎖工具", "loopbackDesc": "用於 UWP 迴環解鎖", "providers": "提供者", "proxyProviders": "代理提供者", "ruleProviders": "規則提供者", "advancedSettings": "進階設定", "nodeExclusion": "節點排除", "nodeExclusionDesc": "排除所有匹配到的節點", "nodeExclusionPlaceholder": "HK|香港|🇭🇰", "formatError": "請檢查格式是否正確", "healthCheckTimeout": "超時時間", "healthCheckTimeoutDesc": "節點健康檢查超時時間", "concurrencyLimit": "並發限制", "concurrencyLimitDesc": "延遲測試的最大並發數量", "notRecommended": "不推薦", "overrideDns": "覆寫 DNS", "overrideDnsDesc": "開啟後將覆蓋配置中的 DNS 選項", "overrideTestUrl": "覆蓋配置", "ntp": "NTP", "ntpDesc": "使用 NTP 時間服務", "overrideNtp": "覆寫 NTP", "overrideNtpDesc": "開啟後將覆蓋配置中的 NTP 選項", "ntpStatus": "狀態", "ntpStatusDesc": "開啟 NTP 時間服務", "writeToSystem": "寫入系統", "writeToSystemDesc": "需要管理員權限", "ntpServer": "伺服器", "ntpPort": "連接埠", "ntpInterval": "更新時間", "sniffer": "Sniffer", "snifferDesc": "修改網域嗅探配置", "overrideSniffer": "覆寫 Sniffer", "overrideSnifferDesc": "開啟後將覆蓋配置中的 Sniffer 選項", "snifferStatus": "狀態", "snifferStatusDesc": "開啟嗅探服務設定", "forceDnsMapping": "強制 DNS 映射", "forceDnsMappingDesc": "強制將 DNS 查詢結果映射到連線", "parsePureIp": "解析純 IP 連線", "parsePureIpDesc": "解析純 IP 連線", "overrideDestination": "覆蓋目標地址", "overrideDestinationDesc": "使用嗅探結果覆蓋連線目標地址", "httpPortSniffer": "HTTP 連接埠嗅探", "tlsPortSniffer": "TLS 連接埠嗅探", "quicPortSniffer": "QUIC 連接埠嗅探", "forceDomain": "強制嗅探網域", "skipDomain": "跳過網域", "skipSrcAddress": "跳過來源 IP", "skipDstAddress": "跳過目標 IP", "snifferPorts": "連接埠", "snifferPortsHint": "例如: 80, 8080-8880", "snifferDomainHint": "每行一個網域", "snifferAddressHint": "每行一個地址", "tunnel": "Tunnel", "tunnelDesc": "使用流量轉發隧道", "overrideTunnel": "覆寫 Tunnel", "overrideTunnelDesc": "開啟後將覆蓋配置中的 Tunnel 選項", "tunnelList": "轉發清單", "addTunnel": "新增轉發", "editTunnel": "編輯轉發", "deleteTunnel": "刪除轉發", "tunnelNetwork": "網路協定", "tunnelNetworkHint": "例如: tcp, udp", "tunnelAddress": "監聽地址", "tunnelAddressHint": "例如: 127.0.0.1:6553", "tunnelTarget": "目標地址", "tunnelTargetHint": "例如: 114.114.114.114:53", "tunnelProxy": "代理名稱", "tunnelProxyHint": "例如: proxy (可選)", "experimental": "Experimental", "experimentalDesc": "實驗性配置請謹慎使用", "overrideExperimental": "覆寫 Experimental", "overrideExperimentalDesc": "開啟後將覆蓋配置中的實驗性配置", "quicGoDisableGso": "停用 QUIC 通用分段卸載", "quicGoDisableGsoDesc": "停用 QUIC 的通用分段卸載功能", "quicGoDisableEcn": "停用 QUIC 顯式擁塞通知", "quicGoDisableEcnDesc": "停用 QUIC 的顯式擁塞通知功能", "dialerIp4pConvert": "啟用撥號 IP4P 地址轉換", "dialerIp4pConvertDesc": "啟用撥號器的 IP4P 地址轉換功能", "status": "狀態", "statusDesc": "關閉後將使用系統 DNS", "preferH3Desc": "優先使用 DOH 的 http/3", "cacheAlgorithm": "快取演算法", "respectRules": "遵守規則", "respectRulesDesc": "DNS 連線跟隨 Rules", "dnsMode": "DNS 模式", "fakeipRange": "FakeIP 範圍", "fakeipRangeV6": "FakeIPv6 範圍", "fakeIpFilterMode": "FakeIP 過濾模式", "fakeIpFilterModeDesc": "指定 FakeIP 過濾模式", "blacklist": "黑名單", "whitelist": "白名單", "fakeipFilter": "FakeIP 過濾清單", "fakeipTtl": "FakeIP 有效時間", "defaultNameserver": "預設網域名稱伺服器", "defaultNameserverDesc": "用於解析 DNS 伺服器", "nameserver": "網域名稱伺服器", "nameserverDesc": "用於解析網域", "useHosts": "使用 Hosts", "useSystemHosts": "使用系統 Hosts", "nameserverPolicy": "網域名稱伺服器策略", "nameserverPolicyDesc": "指定對應網域名稱伺服器策略", "proxyNameserver": "代理網域名稱伺服器", "proxyNameserverDesc": "用於解析代理節點的網域", "directNameserver": "直連網域名稱伺服器", "directNameserverDesc": "用於解析直連出口的網域", "directNameserverFollowPolicy": "直連 DNS 遵循規則", "fallback": "Fallback", "fallbackDesc": "一般情況下使用境外 DNS", "fallbackFilter": "Fallback 過濾", "geoipCode": "GeoIP 代碼", "ipcidr": "IP / 遮罩", "domain": "網域", "reset": "重設", "action_view": "顯示 / 隱藏", "action_start": "啟動 / 停止", "action_mode": "切換模式", "action_proxy": "系統代理", "action_tun": "虛擬網卡", "disclaimer": "免責聲明", "disclaimerDesc": "本軟體為開源免費軟體,僅供學習交流等非商業性質的個人測試使用,代理服務商的行為均與本軟體無關,同意聲明代表您已完全知曉並確認了這一點,如不同意,請選擇退出!", "agree": "同意", "hotkeyManagement": "快捷鍵管理", "hotkeyManagementDesc": "使用鍵盤控制應用程式", "pressKeyboard": "請按下按鍵", "inputCorrectHotkey": "請輸入正確的快捷鍵", "hotkeyConflict": "快捷鍵衝突", "remove": "移除", "noHotKey": "暫無快捷鍵", "noNetwork": "無網路", "ipv6InboundDesc": "允許 IPv6 入站", "exportLogs": "匯出日誌", "exportSuccess": "匯出成功", "iconStyle": "圖示樣式", "onlyIcon": "僅圖示", "noIcon": "無圖示", "stackMode": "堆疊模式", "strictRoute": "嚴格路由", "strictRouteDesc": "使用 TUN 嚴格路由模式", "icmpForwarding": "ICMP 轉發", "icmpForwardingDesc": "開啟後將支援 ICMP Ping", "dnsHijack": "DNS 劫持", "dnsHijackDesc": "將解析匯入內部 DNS 模組", "endpointIndependentNat": "NAT 增強", "endpointIndependentNatDesc": "啟用獨立於端點的 NAT", "network": "網路", "networkDesc": "修改網路相關設定", "bypassDomain": "排除網域", "bypassDomainDesc": "僅在系統代理啟用時生效", "resetTip": "確定要重設嗎?", "regExp": "正規表示式", "icon": "圖片", "iconConfiguration": "圖片設定", "noData": "暫無資料", "adminAutoLaunch": "管理員自啟動", "adminAutoLaunchDesc": "使用管理員模式開機自動啟動", "fontFamily": "字體", "systemFont": "系統字體", "toggle": "切換", "system": "系統", "bypassPrivateRoute": "繞過私有網路", "bypassPrivateRouteDesc": "自動繞過私有網路IP位址", "pleaseInputAdminPassword": "請輸入管理員密碼", "copyEnvVar": "複製環境變數", "memoryInfo": "記憶體資訊", "cancel": "取消", "fileIsUpdate": "檔案有修改,是否儲存修改", "profileHasUpdate": "設定檔已經修改,是否關閉自動更新 ", "hasCacheChange": "是否快取修改", "copySuccess": "複製成功", "success": "Success", "copyLink": "複製連結", "exportFile": "匯出檔案", "cacheCorrupt": "快取已損壞,是否清空?", "detectionTip": "依賴第三方 API,僅供參考", "ipClickBehavior": "顯示切換", "ipPrivacyProtection": "隱藏 IP 顯示", "manualRefreshIp": "重新取得 IP", "tryManualRefresh": "請嘗試手動重新整理", "refreshAppList": "重新整理應用程式清單", "refreshAppListConfirm": "是否重新整理應用程式清單?", "switchToDomesticIp": "取得國內 IP", "listen": "監聽", "undo": "復原", "redo": "重做", "none": "無", "basicConfig": "內核配置", "basicConfigDesc": "全域修改內核配置", "selectedCountTitle": "已選擇 {count} 項", "addRule": "新增規則", "ruleName": "規則名稱", "content": "內容", "subRule": "子規則", "ruleTarget": "規則目標", "sourceIp": "來源 IP", "noResolve": "不解析 IP", "getOriginRules": "獲取原始規則", "overrideOriginRules": "覆蓋原始規則", "addedOriginRules": "附加到原始規則", "enableOverride": "啟用覆寫", "saveChanges": "是否儲存更改?", "generalDesc": "修改一般設定", "findProcessModeDesc": "開啟後將可以尋找處理程序", "tabAnimationDesc": "僅在部分行動檢視中有效", "navBarHapticFeedback": "觸覺回饋", "navBarHapticFeedbackDesc": "底部導覽列切換震動回饋", "saveTip": "確定要儲存嗎?", "colorSchemes": "配色方案", "palette": "調色盤", "tonalSpotScheme": "調性點綴", "fidelityScheme": "高保真", "monochromeScheme": "單色", "neutralScheme": "中性", "vibrantScheme": "活力", "expressiveScheme": "表現力", "contentScheme": "內容主題", "rainbowScheme": "彩虹", "fruitSaladScheme": "果繽紛", "developerMode": "開發者模式", "developerModeEnableTip": "開發者模式已啟用。", "messageTest": "訊息測試", "messageTestTip": "這是一條訊息。", "crashTest": "崩潰測試", "clearData": "清除資料", "zoom": "縮放", "textScale": "文字縮放", "lightIcon": "丹青留白", "lightIconDesc": "手動切換淺色系桌面應用程式圖示", "harmonyFont": "鴻蒙字體", "harmonyFontDesc": "使用最佳化的 HarmonyOS Sans", "internet": "網際網路", "systemApp": "系統應用程式", "noNetworkApp": "無網路應用程式", "contactMe": "聯絡我", "recoveryStrategy": "還原策略", "recoveryStrategy_override": "覆蓋", "recoveryStrategy_compatible": "相容", "logsTest": "日誌測試", "emptyTip": "{label}不能為空", "urlTip": "{label}必須為 URL", "numberTip": "{label}必須為數字", "interval": "間隔", "existsTip": "{label}目前已存在", "deleteTip": "確定刪除目前的 {label} 嗎?", "deleteMultipTip": "確定刪除選取的 {label} 嗎?", "nullTip": "暫無 {label}", "script": "腳本", "color": "顏色", "rename": "重新命名", "unnamed": "未命名", "pleaseEnterScriptName": "請輸入腳本名稱", "overrideInvalidTip": "在腳本模式下不生效", "mixedPort": "混合連接埠", "socksPort": "Socks 連接埠", "redirPort": "Redir 連接埠", "tproxyPort": "Tproxy 連接埠", "portTip": "{label} 必須在 1024 到 49151 之間", "portConflictTip": "請輸入不同的連接埠", "import": "匯入", "importFromCode": "透過程式碼匯入", "importFailed": "匯入失敗", "importFile": "透過檔案匯入", "importUrl": "透過 URL 匯入", "autoSetSystemDns": "自動設定系統 DNS", "details": "{label}詳情", "creationTime": "建立時間", "progress": "處理程序", "host": "主機", "destination": "目標地址", "destinationGeoIP": "目標地理定位", "destinationIPASN": "目標 IP ASN", "specialProxy": "特殊代理", "specialRules": "特殊規則", "remoteDestination": "遠端目標", "networkType": "網路類型", "proxyChains": "代理鏈", "log": "日誌", "connection": "活躍連線", "request": "請求", "switchLabel": "開關", "noStatusAvailable": "未獲取到狀態", "onlinePanel": "線上面板", "openDashboard": "打開 Zashboard", "custom": "自訂", "wakelock": "亮螢幕鎖", "wakelockDescription": "本功能不需要任何特殊權限,因為它僅啟用螢幕喚醒鎖,而不是任何 CPU 喚醒鎖,應用程式會在背景保持必要的活躍,且螢幕不會自動熄滅,這在一些場景下會很有用", "tunEnableRequireAdmin": "啟用虛擬網卡需要管理員權限,請以管理員身分執行程式", "restartTip": "重啟TUN後改變生效", "restart": "重啟", "restartCoreTitle": "重啟內核", "restartCoreDesc": "是否手動重啟內核?", "highRefreshRate": "高重新整理率", "highRefreshRateDesc": "啟用裝置最高重新整理率支援" } ================================================ FILE: build.yaml ================================================ targets: $default: builders: source_gen:combining_builder: options: build_extensions: '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart' '^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart' freezed: options: build_extensions: '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.freezed.dart' ================================================ FILE: core/Clash.Meta/.github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Create a report to help us improve title: "[Bug] " labels: ["bug"] body: - type: checkboxes id: ensure attributes: label: Verify steps description: " 在提交之前,请确认 Please verify that you've followed these steps " options: - label: " 确保你使用的是**本仓库**最新的的 mihomo 或 mihomo Alpha 版本 Ensure you are using the latest version of Mihomo or Mihomo Alpha from **this repository**. " required: true - label: " 如果你可以自己 debug 并解决的话,提交 PR 吧 Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome. " required: false - label: " 我已经在 [Issue Tracker](……/) 中找过我要提出的问题 I have searched on the [issue tracker](……/) for a related issue. " required: true - label: " 我已经使用 Alpha 分支版本测试过,问题依旧存在 I have tested using the dev branch, and the issue still exists. " required: true - label: " 我已经仔细看过 [Documentation](https://wiki.metacubex.one/) 并无法自行解决问题 I have read the [documentation](https://wiki.metacubex.one/) and was unable to solve the issue. " required: true - label: " 这是 Mihomo 核心的问题,并非我所使用的 Mihomo 衍生版本(如 OpenMihomo、KoolMihomo 等)的特定问题 This is an issue of the Mihomo core *per se*, not to the derivatives of Mihomo, like OpenMihomo or KoolMihomo. " required: true - type: input attributes: label: Mihomo version description: "use `mihomo -v`" validations: required: true - type: dropdown id: os attributes: label: What OS are you seeing the problem on? multiple: true options: - macOS - Windows - Linux - OpenBSD/FreeBSD - type: textarea attributes: render: yaml label: "Mihomo config" description: " 在下方附上 Mihomo core 配置文件,请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等) Paste the Mihomo core configuration file below, please make sure that there is no sensitive information in the configuration file (e.g., server address/url, password, port) " validations: required: true - type: textarea attributes: render: shell label: Mihomo log description: " 在下方附上 Mihomo Core 的日志,log level 使用 DEBUG Paste the Mihomo core log below with the log level set to `DEBUG`. " - type: textarea attributes: label: Description validations: required: true ================================================ FILE: core/Clash.Meta/.github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: mihomo Community Support url: https://github.com/MetaCubeX/mihomo/discussions about: Please ask and answer questions about mihomo here. ================================================ FILE: core/Clash.Meta/.github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: Suggest an idea for this project title: "[Feature] " labels: ["enhancement"] body: - type: checkboxes id: ensure attributes: label: Verify steps description: " 在提交之前,请确认 Please verify that you've followed these steps " options: - label: " 我已经在 [Issue Tracker](……/) 中找过我要提出的请求 I have searched on the [issue tracker](……/) for a related feature request. " required: true - label: " 我已经仔细看过 [Documentation](https://wiki.metacubex.one/) 并无法找到这个功能 I have read the [documentation](https://wiki.metacubex.one/) and was unable to solve the issue. " required: true - type: textarea attributes: label: Description description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Mihomo Core 的行为是什麽? validations: required: true - type: textarea attributes: label: Possible Solution description: " 此项非必须,但是如果你有想法的话欢迎提出。 Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change " ================================================ FILE: core/Clash.Meta/.github/genReleaseNote.sh ================================================ #!/bin/bash while getopts "v:" opt; do case $opt in v) version_range=$OPTARG ;; \?) echo "Invalid option: -$OPTARG" >&2 exit 1 ;; esac done if [ -z "$version_range" ]; then echo "Please provide the version range using -v option. Example: ./genReleashNote.sh -v v1.14.1...v1.14.2" exit 1 fi echo "## What's Changed" > release.md git log --pretty=format:"* %h %s by @%an" --grep="^feat" -i $version_range | sort -f | uniq >> release.md echo "" >> release.md echo "## BUG & Fix" >> release.md git log --pretty=format:"* %h %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md echo "" >> release.md echo "## Maintenance" >> release.md git log --pretty=format:"* %h %s by @%an" --grep="^chore\|^docs\|^refactor" -i $version_range | sort -f | uniq >> release.md echo "" >> release.md echo "**Full Changelog**: https://github.com/MetaCubeX/mihomo/compare/$version_range" >> release.md ================================================ FILE: core/Clash.Meta/.github/patch/go1.21.patch ================================================ Subject: [PATCH] Revert "[release-branch.go1.21] crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" --- Index: src/crypto/rand/rand.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand.go b/src/crypto/rand/rand.go --- a/src/crypto/rand/rand.go (revision 8bba868de983dd7bf55fcd121495ba8d6e2734e7) +++ b/src/crypto/rand/rand.go (revision 7e6c963d81e14ee394402671d4044b2940c8d2c1) @@ -15,7 +15,7 @@ // available, /dev/urandom otherwise. // On OpenBSD and macOS, Reader uses getentropy(2). // On other Unix-like systems, Reader reads from /dev/urandom. -// On Windows systems, Reader uses the ProcessPrng API. +// On Windows systems, Reader uses the RtlGenRandom API. // On JS/Wasm, Reader uses the Web Crypto API. // On WASIP1/Wasm, Reader uses random_get from wasi_snapshot_preview1. var Reader io.Reader Index: src/crypto/rand/rand_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand_windows.go b/src/crypto/rand/rand_windows.go --- a/src/crypto/rand/rand_windows.go (revision 8bba868de983dd7bf55fcd121495ba8d6e2734e7) +++ b/src/crypto/rand/rand_windows.go (revision 7e6c963d81e14ee394402671d4044b2940c8d2c1) @@ -15,8 +15,11 @@ type rngReader struct{} -func (r *rngReader) Read(b []byte) (int, error) { - if err := windows.ProcessPrng(b); err != nil { +func (r *rngReader) Read(b []byte) (n int, err error) { + // RtlGenRandom only returns 1<<32-1 bytes at a time. We only read at + // most 1<<31-1 bytes at a time so that this works the same on 32-bit + // and 64-bit systems. + if err := batched(windows.RtlGenRandom, 1<<31-1)(b); err != nil { return 0, err } return len(b), nil Index: src/internal/syscall/windows/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go --- a/src/internal/syscall/windows/syscall_windows.go (revision 8bba868de983dd7bf55fcd121495ba8d6e2734e7) +++ b/src/internal/syscall/windows/syscall_windows.go (revision 7e6c963d81e14ee394402671d4044b2940c8d2c1) @@ -384,7 +384,7 @@ //sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock //sys CreateEvent(eventAttrs *SecurityAttributes, manualReset uint32, initialState uint32, name *uint16) (handle syscall.Handle, err error) = kernel32.CreateEventW -//sys ProcessPrng(buf []byte) (err error) = bcryptprimitives.ProcessPrng +//sys RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036 //sys RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table *byte) (ret uintptr) = kernel32.RtlLookupFunctionEntry //sys RtlVirtualUnwind(handlerType uint32, baseAddress uintptr, pc uintptr, entry uintptr, ctxt uintptr, data *uintptr, frame *uintptr, ctxptrs *byte) (ret uintptr) = kernel32.RtlVirtualUnwind Index: src/internal/syscall/windows/zsyscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go --- a/src/internal/syscall/windows/zsyscall_windows.go (revision 8bba868de983dd7bf55fcd121495ba8d6e2734e7) +++ b/src/internal/syscall/windows/zsyscall_windows.go (revision 7e6c963d81e14ee394402671d4044b2940c8d2c1) @@ -37,14 +37,13 @@ } var ( - modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) - modbcryptprimitives = syscall.NewLazyDLL(sysdll.Add("bcryptprimitives.dll")) - modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) - modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) - modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) - modpsapi = syscall.NewLazyDLL(sysdll.Add("psapi.dll")) - moduserenv = syscall.NewLazyDLL(sysdll.Add("userenv.dll")) - modws2_32 = syscall.NewLazyDLL(sysdll.Add("ws2_32.dll")) + modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) + modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) + modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) + modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) + modpsapi = syscall.NewLazyDLL(sysdll.Add("psapi.dll")) + moduserenv = syscall.NewLazyDLL(sysdll.Add("userenv.dll")) + modws2_32 = syscall.NewLazyDLL(sysdll.Add("ws2_32.dll")) procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges") procDuplicateTokenEx = modadvapi32.NewProc("DuplicateTokenEx") @@ -53,7 +52,7 @@ procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken") procRevertToSelf = modadvapi32.NewProc("RevertToSelf") procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation") - procProcessPrng = modbcryptprimitives.NewProc("ProcessPrng") + procSystemFunction036 = modadvapi32.NewProc("SystemFunction036") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procCreateEventW = modkernel32.NewProc("CreateEventW") procGetACP = modkernel32.NewProc("GetACP") @@ -149,12 +148,12 @@ return } -func ProcessPrng(buf []byte) (err error) { +func RtlGenRandom(buf []byte) (err error) { var _p0 *byte if len(buf) > 0 { _p0 = &buf[0] } - r1, _, e1 := syscall.Syscall(procProcessPrng.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) + r1, _, e1 := syscall.Syscall(procSystemFunction036.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) if r1 == 0 { err = errnoErr(e1) } Index: src/runtime/os_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go --- a/src/runtime/os_windows.go (revision 8bba868de983dd7bf55fcd121495ba8d6e2734e7) +++ b/src/runtime/os_windows.go (revision 7e6c963d81e14ee394402671d4044b2940c8d2c1) @@ -127,8 +127,15 @@ _AddVectoredContinueHandler, _ stdFunction - // Use ProcessPrng to generate cryptographically random data. - _ProcessPrng stdFunction + // Use RtlGenRandom to generate cryptographically random data. + // This approach has been recommended by Microsoft (see issue + // 15589 for details). + // The RtlGenRandom is not listed in advapi32.dll, instead + // RtlGenRandom function can be found by searching for SystemFunction036. + // Also some versions of Mingw cannot link to SystemFunction036 + // when building executable as Cgo. So load SystemFunction036 + // manually during runtime startup. + _RtlGenRandom stdFunction // Load ntdll.dll manually during startup, otherwise Mingw // links wrong printf function to cgo executable (see issue @@ -145,12 +152,12 @@ ) var ( - bcryptprimitivesdll = [...]uint16{'b', 'c', 'r', 'y', 'p', 't', 'p', 'r', 'i', 'm', 'i', 't', 'i', 'v', 'e', 's', '.', 'd', 'l', 'l', 0} - kernel32dll = [...]uint16{'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0} - ntdlldll = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0} - powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0} - winmmdll = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0} - ws2_32dll = [...]uint16{'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0} + advapi32dll = [...]uint16{'a', 'd', 'v', 'a', 'p', 'i', '3', '2', '.', 'd', 'l', 'l', 0} + kernel32dll = [...]uint16{'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0} + ntdlldll = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0} + powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0} + winmmdll = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0} + ws2_32dll = [...]uint16{'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0} ) // Function to be called by windows CreateThread @@ -249,11 +256,11 @@ } _AddVectoredContinueHandler = windowsFindfunc(k32, []byte("AddVectoredContinueHandler\000")) - bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:]) - if bcryptPrimitives == 0 { - throw("bcryptprimitives.dll not found") + a32 := windowsLoadSystemLib(advapi32dll[:]) + if a32 == 0 { + throw("advapi32.dll not found") } - _ProcessPrng = windowsFindfunc(bcryptPrimitives, []byte("ProcessPrng\000")) + _RtlGenRandom = windowsFindfunc(a32, []byte("SystemFunction036\000")) n32 := windowsLoadSystemLib(ntdlldll[:]) if n32 == 0 { @@ -610,7 +617,7 @@ //go:nosplit func getRandomData(r []byte) { n := 0 - if stdcall2(_ProcessPrng, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { + if stdcall2(_RtlGenRandom, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { n = len(r) } extendRandom(r, n) ================================================ FILE: core/Clash.Meta/.github/patch/go1.22.patch ================================================ Subject: [PATCH] Revert "runtime: always use LoadLibraryEx to load system libraries" Revert "syscall: remove Windows 7 console handle workaround" Revert "net: remove sysSocket fallback for Windows 7" Revert "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" --- Index: src/crypto/rand/rand.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand.go b/src/crypto/rand/rand.go --- a/src/crypto/rand/rand.go (revision cb4eee693c382bea4222f20837e26501d40ed892) +++ b/src/crypto/rand/rand.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) @@ -15,7 +15,7 @@ // available, /dev/urandom otherwise. // On OpenBSD and macOS, Reader uses getentropy(2). // On other Unix-like systems, Reader reads from /dev/urandom. -// On Windows systems, Reader uses the ProcessPrng API. +// On Windows systems, Reader uses the RtlGenRandom API. // On JS/Wasm, Reader uses the Web Crypto API. // On WASIP1/Wasm, Reader uses random_get from wasi_snapshot_preview1. var Reader io.Reader Index: src/crypto/rand/rand_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand_windows.go b/src/crypto/rand/rand_windows.go --- a/src/crypto/rand/rand_windows.go (revision cb4eee693c382bea4222f20837e26501d40ed892) +++ b/src/crypto/rand/rand_windows.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) @@ -15,8 +15,11 @@ type rngReader struct{} -func (r *rngReader) Read(b []byte) (int, error) { - if err := windows.ProcessPrng(b); err != nil { +func (r *rngReader) Read(b []byte) (n int, err error) { + // RtlGenRandom only returns 1<<32-1 bytes at a time. We only read at + // most 1<<31-1 bytes at a time so that this works the same on 32-bit + // and 64-bit systems. + if err := batched(windows.RtlGenRandom, 1<<31-1)(b); err != nil { return 0, err } return len(b), nil Index: src/internal/syscall/windows/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go --- a/src/internal/syscall/windows/syscall_windows.go (revision cb4eee693c382bea4222f20837e26501d40ed892) +++ b/src/internal/syscall/windows/syscall_windows.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) @@ -384,7 +384,7 @@ //sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock //sys CreateEvent(eventAttrs *SecurityAttributes, manualReset uint32, initialState uint32, name *uint16) (handle syscall.Handle, err error) = kernel32.CreateEventW -//sys ProcessPrng(buf []byte) (err error) = bcryptprimitives.ProcessPrng +//sys RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036 type FILE_ID_BOTH_DIR_INFO struct { NextEntryOffset uint32 Index: src/internal/syscall/windows/zsyscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go --- a/src/internal/syscall/windows/zsyscall_windows.go (revision cb4eee693c382bea4222f20837e26501d40ed892) +++ b/src/internal/syscall/windows/zsyscall_windows.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) @@ -37,14 +37,13 @@ } var ( - modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) - modbcryptprimitives = syscall.NewLazyDLL(sysdll.Add("bcryptprimitives.dll")) - modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) - modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) - modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) - modpsapi = syscall.NewLazyDLL(sysdll.Add("psapi.dll")) - moduserenv = syscall.NewLazyDLL(sysdll.Add("userenv.dll")) - modws2_32 = syscall.NewLazyDLL(sysdll.Add("ws2_32.dll")) + modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) + modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) + modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) + modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) + modpsapi = syscall.NewLazyDLL(sysdll.Add("psapi.dll")) + moduserenv = syscall.NewLazyDLL(sysdll.Add("userenv.dll")) + modws2_32 = syscall.NewLazyDLL(sysdll.Add("ws2_32.dll")) procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges") procDuplicateTokenEx = modadvapi32.NewProc("DuplicateTokenEx") @@ -56,7 +55,7 @@ procQueryServiceStatus = modadvapi32.NewProc("QueryServiceStatus") procRevertToSelf = modadvapi32.NewProc("RevertToSelf") procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation") - procProcessPrng = modbcryptprimitives.NewProc("ProcessPrng") + procSystemFunction036 = modadvapi32.NewProc("SystemFunction036") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procCreateEventW = modkernel32.NewProc("CreateEventW") procGetACP = modkernel32.NewProc("GetACP") @@ -180,12 +179,12 @@ return } -func ProcessPrng(buf []byte) (err error) { +func RtlGenRandom(buf []byte) (err error) { var _p0 *byte if len(buf) > 0 { _p0 = &buf[0] } - r1, _, e1 := syscall.Syscall(procProcessPrng.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) + r1, _, e1 := syscall.Syscall(procSystemFunction036.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) if r1 == 0 { err = errnoErr(e1) } Index: src/runtime/os_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go --- a/src/runtime/os_windows.go (revision cb4eee693c382bea4222f20837e26501d40ed892) +++ b/src/runtime/os_windows.go (revision 83ff9782e024cb328b690cbf0da4e7848a327f4f) @@ -40,8 +40,8 @@ //go:cgo_import_dynamic runtime._GetSystemInfo GetSystemInfo%1 "kernel32.dll" //go:cgo_import_dynamic runtime._GetThreadContext GetThreadContext%2 "kernel32.dll" //go:cgo_import_dynamic runtime._SetThreadContext SetThreadContext%2 "kernel32.dll" -//go:cgo_import_dynamic runtime._LoadLibraryExW LoadLibraryExW%3 "kernel32.dll" //go:cgo_import_dynamic runtime._LoadLibraryW LoadLibraryW%1 "kernel32.dll" +//go:cgo_import_dynamic runtime._LoadLibraryA LoadLibraryA%1 "kernel32.dll" //go:cgo_import_dynamic runtime._PostQueuedCompletionStatus PostQueuedCompletionStatus%4 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceCounter QueryPerformanceCounter%1 "kernel32.dll" //go:cgo_import_dynamic runtime._RaiseFailFastException RaiseFailFastException%3 "kernel32.dll" @@ -74,7 +74,6 @@ // Following syscalls are available on every Windows PC. // All these variables are set by the Windows executable // loader before the Go program starts. - _AddVectoredContinueHandler, _AddVectoredExceptionHandler, _CloseHandle, _CreateEventA, @@ -98,8 +97,8 @@ _GetSystemInfo, _GetThreadContext, _SetThreadContext, - _LoadLibraryExW, _LoadLibraryW, + _LoadLibraryA, _PostQueuedCompletionStatus, _QueryPerformanceCounter, _RaiseFailFastException, @@ -127,8 +126,23 @@ _WriteFile, _ stdFunction - // Use ProcessPrng to generate cryptographically random data. - _ProcessPrng stdFunction + // Following syscalls are only available on some Windows PCs. + // We will load syscalls, if available, before using them. + _AddDllDirectory, + _AddVectoredContinueHandler, + _LoadLibraryExA, + _LoadLibraryExW, + _ stdFunction + + // Use RtlGenRandom to generate cryptographically random data. + // This approach has been recommended by Microsoft (see issue + // 15589 for details). + // The RtlGenRandom is not listed in advapi32.dll, instead + // RtlGenRandom function can be found by searching for SystemFunction036. + // Also some versions of Mingw cannot link to SystemFunction036 + // when building executable as Cgo. So load SystemFunction036 + // manually during runtime startup. + _RtlGenRandom stdFunction // Load ntdll.dll manually during startup, otherwise Mingw // links wrong printf function to cgo executable (see issue @@ -143,14 +157,6 @@ _ stdFunction ) -var ( - bcryptprimitivesdll = [...]uint16{'b', 'c', 'r', 'y', 'p', 't', 'p', 'r', 'i', 'm', 'i', 't', 'i', 'v', 'e', 's', '.', 'd', 'l', 'l', 0} - ntdlldll = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0} - powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0} - winmmdll = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0} - ws2_32dll = [...]uint16{'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0} -) - // Function to be called by windows CreateThread // to start new os thread. func tstart_stdcall(newm *m) @@ -239,25 +245,51 @@ return unsafe.String(&sysDirectory[0], sysDirectoryLen) } -func windowsLoadSystemLib(name []uint16) uintptr { - return stdcall3(_LoadLibraryExW, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +//go:linkname syscall_getSystemDirectory syscall.getSystemDirectory +func syscall_getSystemDirectory() string { + return unsafe.String(&sysDirectory[0], sysDirectoryLen) +} + +func windowsLoadSystemLib(name []byte) uintptr { + if useLoadLibraryEx { + return stdcall3(_LoadLibraryExA, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + absName := append(sysDirectory[:sysDirectoryLen], name...) + return stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&absName[0]))) + } } func loadOptionalSyscalls() { - bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:]) - if bcryptPrimitives == 0 { - throw("bcryptprimitives.dll not found") + var kernel32dll = []byte("kernel32.dll\000") + k32 := stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&kernel32dll[0]))) + if k32 == 0 { + throw("kernel32.dll not found") } - _ProcessPrng = windowsFindfunc(bcryptPrimitives, []byte("ProcessPrng\000")) + _AddDllDirectory = windowsFindfunc(k32, []byte("AddDllDirectory\000")) + _AddVectoredContinueHandler = windowsFindfunc(k32, []byte("AddVectoredContinueHandler\000")) + _LoadLibraryExA = windowsFindfunc(k32, []byte("LoadLibraryExA\000")) + _LoadLibraryExW = windowsFindfunc(k32, []byte("LoadLibraryExW\000")) + useLoadLibraryEx = (_LoadLibraryExW != nil && _LoadLibraryExA != nil && _AddDllDirectory != nil) + + initSysDirectory() - n32 := windowsLoadSystemLib(ntdlldll[:]) + var advapi32dll = []byte("advapi32.dll\000") + a32 := windowsLoadSystemLib(advapi32dll) + if a32 == 0 { + throw("advapi32.dll not found") + } + _RtlGenRandom = windowsFindfunc(a32, []byte("SystemFunction036\000")) + + var ntdll = []byte("ntdll.dll\000") + n32 := windowsLoadSystemLib(ntdll) if n32 == 0 { throw("ntdll.dll not found") } _RtlGetCurrentPeb = windowsFindfunc(n32, []byte("RtlGetCurrentPeb\000")) _RtlGetNtVersionNumbers = windowsFindfunc(n32, []byte("RtlGetNtVersionNumbers\000")) - m32 := windowsLoadSystemLib(winmmdll[:]) + var winmmdll = []byte("winmm.dll\000") + m32 := windowsLoadSystemLib(winmmdll) if m32 == 0 { throw("winmm.dll not found") } @@ -267,7 +299,8 @@ throw("timeBegin/EndPeriod not found") } - ws232 := windowsLoadSystemLib(ws2_32dll[:]) + var ws232dll = []byte("ws2_32.dll\000") + ws232 := windowsLoadSystemLib(ws232dll) if ws232 == 0 { throw("ws2_32.dll not found") } @@ -286,7 +319,7 @@ context uintptr } - powrprof := windowsLoadSystemLib(powrprofdll[:]) + powrprof := windowsLoadSystemLib([]byte("powrprof.dll\000")) if powrprof == 0 { return // Running on Windows 7, where we don't need it anyway. } @@ -360,6 +393,22 @@ // in sys_windows_386.s and sys_windows_amd64.s: func getlasterror() uint32 +// When loading DLLs, we prefer to use LoadLibraryEx with +// LOAD_LIBRARY_SEARCH_* flags, if available. LoadLibraryEx is not +// available on old Windows, though, and the LOAD_LIBRARY_SEARCH_* +// flags are not available on some versions of Windows without a +// security patch. +// +// https://msdn.microsoft.com/en-us/library/ms684179(v=vs.85).aspx says: +// "Windows 7, Windows Server 2008 R2, Windows Vista, and Windows +// Server 2008: The LOAD_LIBRARY_SEARCH_* flags are available on +// systems that have KB2533623 installed. To determine whether the +// flags are available, use GetProcAddress to get the address of the +// AddDllDirectory, RemoveDllDirectory, or SetDefaultDllDirectories +// function. If GetProcAddress succeeds, the LOAD_LIBRARY_SEARCH_* +// flags can be used with LoadLibraryEx." +var useLoadLibraryEx bool + var timeBeginPeriodRetValue uint32 // osRelaxMinNS indicates that sysmon shouldn't osRelax if the next @@ -507,7 +556,6 @@ initHighResTimer() timeBeginPeriodRetValue = osRelax(false) - initSysDirectory() initLongPathSupport() ncpu = getproccount() @@ -524,7 +572,7 @@ //go:nosplit func readRandom(r []byte) int { n := 0 - if stdcall2(_ProcessPrng, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { + if stdcall2(_RtlGenRandom, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { n = len(r) } return n Index: src/net/hook_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/hook_windows.go b/src/net/hook_windows.go --- a/src/net/hook_windows.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) +++ b/src/net/hook_windows.go (revision ef0606261340e608017860b423ffae5c1ce78239) @@ -13,6 +13,7 @@ hostsFilePath = windows.GetSystemDirectory() + "/Drivers/etc/hosts" // Placeholders for socket system calls. + socketFunc func(int, int, int) (syscall.Handle, error) = syscall.Socket wsaSocketFunc func(int32, int32, int32, *syscall.WSAProtocolInfo, uint32, uint32) (syscall.Handle, error) = windows.WSASocket connectFunc func(syscall.Handle, syscall.Sockaddr) error = syscall.Connect listenFunc func(syscall.Handle, int) error = syscall.Listen Index: src/net/internal/socktest/main_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_test.go b/src/net/internal/socktest/main_test.go --- a/src/net/internal/socktest/main_test.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) +++ b/src/net/internal/socktest/main_test.go (revision ef0606261340e608017860b423ffae5c1ce78239) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !js && !plan9 && !wasip1 && !windows +//go:build !js && !plan9 && !wasip1 package socktest_test Index: src/net/internal/socktest/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_windows_test.go b/src/net/internal/socktest/main_windows_test.go new file mode 100644 --- /dev/null (revision ef0606261340e608017860b423ffae5c1ce78239) +++ b/src/net/internal/socktest/main_windows_test.go (revision ef0606261340e608017860b423ffae5c1ce78239) @@ -0,0 +1,22 @@ +// 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 socktest_test + +import "syscall" + +var ( + socketFunc func(int, int, int) (syscall.Handle, error) + closeFunc func(syscall.Handle) error +) + +func installTestHooks() { + socketFunc = sw.Socket + closeFunc = sw.Closesocket +} + +func uninstallTestHooks() { + socketFunc = syscall.Socket + closeFunc = syscall.Closesocket +} Index: src/net/internal/socktest/sys_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/sys_windows.go b/src/net/internal/socktest/sys_windows.go --- a/src/net/internal/socktest/sys_windows.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) +++ b/src/net/internal/socktest/sys_windows.go (revision ef0606261340e608017860b423ffae5c1ce78239) @@ -9,6 +9,38 @@ "syscall" ) +// Socket wraps [syscall.Socket]. +func (sw *Switch) Socket(family, sotype, proto int) (s syscall.Handle, err error) { + sw.once.Do(sw.init) + + so := &Status{Cookie: cookie(family, sotype, proto)} + sw.fmu.RLock() + f, _ := sw.fltab[FilterSocket] + sw.fmu.RUnlock() + + af, err := f.apply(so) + if err != nil { + return syscall.InvalidHandle, err + } + s, so.Err = syscall.Socket(family, sotype, proto) + if err = af.apply(so); err != nil { + if so.Err == nil { + syscall.Closesocket(s) + } + return syscall.InvalidHandle, err + } + + sw.smu.Lock() + defer sw.smu.Unlock() + if so.Err != nil { + sw.stats.getLocked(so.Cookie).OpenFailed++ + return syscall.InvalidHandle, so.Err + } + nso := sw.addLocked(s, family, sotype, proto) + sw.stats.getLocked(nso.Cookie).Opened++ + return s, nil +} + // WSASocket wraps [syscall.WSASocket]. func (sw *Switch) WSASocket(family, sotype, proto int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (s syscall.Handle, err error) { sw.once.Do(sw.init) Index: src/net/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/main_windows_test.go b/src/net/main_windows_test.go --- a/src/net/main_windows_test.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) +++ b/src/net/main_windows_test.go (revision ef0606261340e608017860b423ffae5c1ce78239) @@ -8,6 +8,7 @@ var ( // Placeholders for saving original socket system calls. + origSocket = socketFunc origWSASocket = wsaSocketFunc origClosesocket = poll.CloseFunc origConnect = connectFunc @@ -17,6 +18,7 @@ ) func installTestHooks() { + socketFunc = sw.Socket wsaSocketFunc = sw.WSASocket poll.CloseFunc = sw.Closesocket connectFunc = sw.Connect @@ -26,6 +28,7 @@ } func uninstallTestHooks() { + socketFunc = origSocket wsaSocketFunc = origWSASocket poll.CloseFunc = origClosesocket connectFunc = origConnect Index: src/net/sock_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/sock_windows.go b/src/net/sock_windows.go --- a/src/net/sock_windows.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) +++ b/src/net/sock_windows.go (revision ef0606261340e608017860b423ffae5c1ce78239) @@ -20,6 +20,21 @@ func sysSocket(family, sotype, proto int) (syscall.Handle, error) { s, err := wsaSocketFunc(int32(family), int32(sotype), int32(proto), nil, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT) + if err == nil { + return s, nil + } + // WSA_FLAG_NO_HANDLE_INHERIT flag is not supported on some + // old versions of Windows, see + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms742212(v=vs.85).aspx + // for details. Just use syscall.Socket, if windows.WSASocket failed. + + // See ../syscall/exec_unix.go for description of ForkLock. + syscall.ForkLock.RLock() + s, err = socketFunc(family, sotype, proto) + if err == nil { + syscall.CloseOnExec(s) + } + syscall.ForkLock.RUnlock() if err != nil { return syscall.InvalidHandle, os.NewSyscallError("socket", err) } Index: src/syscall/exec_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go --- a/src/syscall/exec_windows.go (revision 9779155f18b6556a034f7bb79fb7fb2aad1e26a9) +++ b/src/syscall/exec_windows.go (revision 7f83badcb925a7e743188041cb6e561fc9b5b642) @@ -14,7 +14,6 @@ "unsafe" ) -// ForkLock is not used on Windows. var ForkLock sync.RWMutex // EscapeArg rewrites command line argument s as prescribed @@ -317,6 +316,17 @@ } } + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + isWin7 := maj < 6 || (maj == 6 && min <= 1) + // NT kernel handles are divisible by 4, with the bottom 3 bits left as + // a tag. The fully set tag correlates with the types of handles we're + // concerned about here. Except, the kernel will interpret some + // special handle values, like -1, -2, and so forth, so kernelbase.dll + // checks to see that those bottom three bits are checked, but that top + // bit is not checked. + isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 } + p, _ := GetCurrentProcess() parentProcess := p if sys.ParentProcess != 0 { @@ -325,7 +335,15 @@ fd := make([]Handle, len(attr.Files)) for i := range attr.Files { if attr.Files[i] > 0 { - err := DuplicateHandle(p, Handle(attr.Files[i]), parentProcess, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) + destinationProcessHandle := parentProcess + + // On Windows 7, console handles aren't real handles, and can only be duplicated + // into the current process, not a parent one, which amounts to the same thing. + if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) { + destinationProcessHandle = p + } + + err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) if err != nil { return 0, 0, err } @@ -356,6 +374,14 @@ fd = append(fd, sys.AdditionalInheritedHandles...) + // On Windows 7, console handles aren't real handles, so don't pass them + // through to PROC_THREAD_ATTRIBUTE_HANDLE_LIST. + for i := range fd { + if isLegacyWin7ConsoleHandle(fd[i]) { + fd[i] = 0 + } + } + // The presence of a NULL handle in the list is enough to cause PROC_THREAD_ATTRIBUTE_HANDLE_LIST // to treat the entire list as empty, so remove NULL handles. j := 0 Index: src/runtime/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/syscall_windows.go b/src/runtime/syscall_windows.go --- a/src/runtime/syscall_windows.go (revision 7f83badcb925a7e743188041cb6e561fc9b5b642) +++ b/src/runtime/syscall_windows.go (revision 83ff9782e024cb328b690cbf0da4e7848a327f4f) @@ -413,23 +413,36 @@ const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 +// When available, this function will use LoadLibraryEx with the filename +// parameter and the important SEARCH_SYSTEM32 argument. But on systems that +// do not have that option, absoluteFilepath should contain a fallback +// to the full path inside of system32 for use with vanilla LoadLibrary. +// //go:linkname syscall_loadsystemlibrary syscall.loadsystemlibrary //go:nosplit //go:cgo_unsafe_args -func syscall_loadsystemlibrary(filename *uint16) (handle, err uintptr) { +func syscall_loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle, err uintptr) { lockOSThread() c := &getg().m.syscall - c.fn = getLoadLibraryEx() - c.n = 3 - args := struct { - lpFileName *uint16 - hFile uintptr // always 0 - flags uint32 - }{filename, 0, _LOAD_LIBRARY_SEARCH_SYSTEM32} - c.args = uintptr(noescape(unsafe.Pointer(&args))) + + if useLoadLibraryEx { + c.fn = getLoadLibraryEx() + c.n = 3 + args := struct { + lpFileName *uint16 + hFile uintptr // always 0 + flags uint32 + }{filename, 0, _LOAD_LIBRARY_SEARCH_SYSTEM32} + c.args = uintptr(noescape(unsafe.Pointer(&args))) + } else { + c.fn = getLoadLibrary() + c.n = 1 + c.args = uintptr(noescape(unsafe.Pointer(&absoluteFilepath))) + } cgocall(asmstdcallAddr, unsafe.Pointer(c)) KeepAlive(filename) + KeepAlive(absoluteFilepath) handle = c.r1 if handle == 0 { err = c.err Index: src/syscall/dll_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/dll_windows.go b/src/syscall/dll_windows.go --- a/src/syscall/dll_windows.go (revision 7f83badcb925a7e743188041cb6e561fc9b5b642) +++ b/src/syscall/dll_windows.go (revision 83ff9782e024cb328b690cbf0da4e7848a327f4f) @@ -44,7 +44,7 @@ func SyscallN(trap uintptr, args ...uintptr) (r1, r2 uintptr, err Errno) func loadlibrary(filename *uint16) (handle uintptr, err Errno) -func loadsystemlibrary(filename *uint16) (handle uintptr, err Errno) +func loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle uintptr, err Errno) func getprocaddress(handle uintptr, procname *uint8) (proc uintptr, err Errno) // A DLL implements access to a single DLL. @@ -53,6 +53,9 @@ Handle Handle } +//go:linkname getSystemDirectory +func getSystemDirectory() string // Implemented in runtime package. + // LoadDLL loads the named DLL file into memory. // // If name is not an absolute path and is not a known system DLL used by @@ -69,7 +72,11 @@ var h uintptr var e Errno if sysdll.IsSystemDLL[name] { - h, e = loadsystemlibrary(namep) + absoluteFilepathp, err := UTF16PtrFromString(getSystemDirectory() + name) + if err != nil { + return nil, err + } + h, e = loadsystemlibrary(namep, absoluteFilepathp) } else { h, e = loadlibrary(namep) } ================================================ FILE: core/Clash.Meta/.github/patch/go1.23.patch ================================================ Subject: [PATCH] Revert "runtime: always use LoadLibraryEx to load system libraries" Revert "syscall: remove Windows 7 console handle workaround" Revert "net: remove sysSocket fallback for Windows 7" Revert "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" --- Index: src/crypto/rand/rand.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand.go b/src/crypto/rand/rand.go --- a/src/crypto/rand/rand.go (revision 6885bad7dd86880be6929c02085e5c7a67ff2887) +++ b/src/crypto/rand/rand.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) @@ -16,7 +16,7 @@ // - On macOS and iOS, Reader uses arc4random_buf(3). // - On OpenBSD and NetBSD, Reader uses getentropy(2). // - On other Unix-like systems, Reader reads from /dev/urandom. -// - On Windows, Reader uses the ProcessPrng API. +// - On Windows systems, Reader uses the RtlGenRandom API. // - On js/wasm, Reader uses the Web Crypto API. // - On wasip1/wasm, Reader uses random_get from wasi_snapshot_preview1. var Reader io.Reader Index: src/crypto/rand/rand_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand_windows.go b/src/crypto/rand/rand_windows.go --- a/src/crypto/rand/rand_windows.go (revision 6885bad7dd86880be6929c02085e5c7a67ff2887) +++ b/src/crypto/rand/rand_windows.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) @@ -15,8 +15,11 @@ type rngReader struct{} -func (r *rngReader) Read(b []byte) (int, error) { - if err := windows.ProcessPrng(b); err != nil { +func (r *rngReader) Read(b []byte) (n int, err error) { + // RtlGenRandom only returns 1<<32-1 bytes at a time. We only read at + // most 1<<31-1 bytes at a time so that this works the same on 32-bit + // and 64-bit systems. + if err := batched(windows.RtlGenRandom, 1<<31-1)(b); err != nil { return 0, err } return len(b), nil Index: src/internal/syscall/windows/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go --- a/src/internal/syscall/windows/syscall_windows.go (revision 6885bad7dd86880be6929c02085e5c7a67ff2887) +++ b/src/internal/syscall/windows/syscall_windows.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) @@ -414,7 +414,7 @@ //sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock //sys CreateEvent(eventAttrs *SecurityAttributes, manualReset uint32, initialState uint32, name *uint16) (handle syscall.Handle, err error) = kernel32.CreateEventW -//sys ProcessPrng(buf []byte) (err error) = bcryptprimitives.ProcessPrng +//sys RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036 type FILE_ID_BOTH_DIR_INFO struct { NextEntryOffset uint32 Index: src/internal/syscall/windows/zsyscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go --- a/src/internal/syscall/windows/zsyscall_windows.go (revision 6885bad7dd86880be6929c02085e5c7a67ff2887) +++ b/src/internal/syscall/windows/zsyscall_windows.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) @@ -38,7 +38,6 @@ var ( modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) - modbcryptprimitives = syscall.NewLazyDLL(sysdll.Add("bcryptprimitives.dll")) modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) @@ -57,7 +56,7 @@ procQueryServiceStatus = modadvapi32.NewProc("QueryServiceStatus") procRevertToSelf = modadvapi32.NewProc("RevertToSelf") procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation") - procProcessPrng = modbcryptprimitives.NewProc("ProcessPrng") + procSystemFunction036 = modadvapi32.NewProc("SystemFunction036") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procCreateEventW = modkernel32.NewProc("CreateEventW") procGetACP = modkernel32.NewProc("GetACP") @@ -183,12 +182,12 @@ return } -func ProcessPrng(buf []byte) (err error) { +func RtlGenRandom(buf []byte) (err error) { var _p0 *byte if len(buf) > 0 { _p0 = &buf[0] } - r1, _, e1 := syscall.Syscall(procProcessPrng.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) + r1, _, e1 := syscall.Syscall(procSystemFunction036.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) if r1 == 0 { err = errnoErr(e1) } Index: src/runtime/os_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go --- a/src/runtime/os_windows.go (revision 6885bad7dd86880be6929c02085e5c7a67ff2887) +++ b/src/runtime/os_windows.go (revision 69e2eed6dd0f6d815ebf15797761c13f31213dd6) @@ -39,8 +39,8 @@ //go:cgo_import_dynamic runtime._GetSystemInfo GetSystemInfo%1 "kernel32.dll" //go:cgo_import_dynamic runtime._GetThreadContext GetThreadContext%2 "kernel32.dll" //go:cgo_import_dynamic runtime._SetThreadContext SetThreadContext%2 "kernel32.dll" -//go:cgo_import_dynamic runtime._LoadLibraryExW LoadLibraryExW%3 "kernel32.dll" //go:cgo_import_dynamic runtime._LoadLibraryW LoadLibraryW%1 "kernel32.dll" +//go:cgo_import_dynamic runtime._LoadLibraryA LoadLibraryA%1 "kernel32.dll" //go:cgo_import_dynamic runtime._PostQueuedCompletionStatus PostQueuedCompletionStatus%4 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceCounter QueryPerformanceCounter%1 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceFrequency QueryPerformanceFrequency%1 "kernel32.dll" @@ -74,7 +74,6 @@ // Following syscalls are available on every Windows PC. // All these variables are set by the Windows executable // loader before the Go program starts. - _AddVectoredContinueHandler, _AddVectoredExceptionHandler, _CloseHandle, _CreateEventA, @@ -97,8 +96,8 @@ _GetSystemInfo, _GetThreadContext, _SetThreadContext, - _LoadLibraryExW, _LoadLibraryW, + _LoadLibraryA, _PostQueuedCompletionStatus, _QueryPerformanceCounter, _QueryPerformanceFrequency, @@ -127,8 +126,23 @@ _WriteFile, _ stdFunction - // Use ProcessPrng to generate cryptographically random data. - _ProcessPrng stdFunction + // Following syscalls are only available on some Windows PCs. + // We will load syscalls, if available, before using them. + _AddDllDirectory, + _AddVectoredContinueHandler, + _LoadLibraryExA, + _LoadLibraryExW, + _ stdFunction + + // Use RtlGenRandom to generate cryptographically random data. + // This approach has been recommended by Microsoft (see issue + // 15589 for details). + // The RtlGenRandom is not listed in advapi32.dll, instead + // RtlGenRandom function can be found by searching for SystemFunction036. + // Also some versions of Mingw cannot link to SystemFunction036 + // when building executable as Cgo. So load SystemFunction036 + // manually during runtime startup. + _RtlGenRandom stdFunction // Load ntdll.dll manually during startup, otherwise Mingw // links wrong printf function to cgo executable (see issue @@ -145,13 +159,6 @@ _ stdFunction ) -var ( - bcryptprimitivesdll = [...]uint16{'b', 'c', 'r', 'y', 'p', 't', 'p', 'r', 'i', 'm', 'i', 't', 'i', 'v', 'e', 's', '.', 'd', 'l', 'l', 0} - ntdlldll = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0} - powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0} - winmmdll = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0} -) - // Function to be called by windows CreateThread // to start new os thread. func tstart_stdcall(newm *m) @@ -244,8 +251,18 @@ return unsafe.String(&sysDirectory[0], sysDirectoryLen) } -func windowsLoadSystemLib(name []uint16) uintptr { - return stdcall3(_LoadLibraryExW, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +//go:linkname syscall_getSystemDirectory syscall.getSystemDirectory +func syscall_getSystemDirectory() string { + return unsafe.String(&sysDirectory[0], sysDirectoryLen) +} + +func windowsLoadSystemLib(name []byte) uintptr { + if useLoadLibraryEx { + return stdcall3(_LoadLibraryExA, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + absName := append(sysDirectory[:sysDirectoryLen], name...) + return stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&absName[0]))) + } } //go:linkname windows_QueryPerformanceCounter internal/syscall/windows.QueryPerformanceCounter @@ -263,13 +280,28 @@ } func loadOptionalSyscalls() { - bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:]) - if bcryptPrimitives == 0 { - throw("bcryptprimitives.dll not found") + var kernel32dll = []byte("kernel32.dll\000") + k32 := stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&kernel32dll[0]))) + if k32 == 0 { + throw("kernel32.dll not found") } - _ProcessPrng = windowsFindfunc(bcryptPrimitives, []byte("ProcessPrng\000")) + _AddDllDirectory = windowsFindfunc(k32, []byte("AddDllDirectory\000")) + _AddVectoredContinueHandler = windowsFindfunc(k32, []byte("AddVectoredContinueHandler\000")) + _LoadLibraryExA = windowsFindfunc(k32, []byte("LoadLibraryExA\000")) + _LoadLibraryExW = windowsFindfunc(k32, []byte("LoadLibraryExW\000")) + useLoadLibraryEx = (_LoadLibraryExW != nil && _LoadLibraryExA != nil && _AddDllDirectory != nil) + + initSysDirectory() - n32 := windowsLoadSystemLib(ntdlldll[:]) + var advapi32dll = []byte("advapi32.dll\000") + a32 := windowsLoadSystemLib(advapi32dll) + if a32 == 0 { + throw("advapi32.dll not found") + } + _RtlGenRandom = windowsFindfunc(a32, []byte("SystemFunction036\000")) + + var ntdll = []byte("ntdll.dll\000") + n32 := windowsLoadSystemLib(ntdll) if n32 == 0 { throw("ntdll.dll not found") } @@ -298,7 +330,7 @@ context uintptr } - powrprof := windowsLoadSystemLib(powrprofdll[:]) + powrprof := windowsLoadSystemLib([]byte("powrprof.dll\000")) if powrprof == 0 { return // Running on Windows 7, where we don't need it anyway. } @@ -357,6 +389,22 @@ // in sys_windows_386.s and sys_windows_amd64.s: func getlasterror() uint32 +// When loading DLLs, we prefer to use LoadLibraryEx with +// LOAD_LIBRARY_SEARCH_* flags, if available. LoadLibraryEx is not +// available on old Windows, though, and the LOAD_LIBRARY_SEARCH_* +// flags are not available on some versions of Windows without a +// security patch. +// +// https://msdn.microsoft.com/en-us/library/ms684179(v=vs.85).aspx says: +// "Windows 7, Windows Server 2008 R2, Windows Vista, and Windows +// Server 2008: The LOAD_LIBRARY_SEARCH_* flags are available on +// systems that have KB2533623 installed. To determine whether the +// flags are available, use GetProcAddress to get the address of the +// AddDllDirectory, RemoveDllDirectory, or SetDefaultDllDirectories +// function. If GetProcAddress succeeds, the LOAD_LIBRARY_SEARCH_* +// flags can be used with LoadLibraryEx." +var useLoadLibraryEx bool + var timeBeginPeriodRetValue uint32 // osRelaxMinNS indicates that sysmon shouldn't osRelax if the next @@ -430,7 +478,8 @@ // Only load winmm.dll if we need it. // This avoids a dependency on winmm.dll for Go programs // that run on new Windows versions. - m32 := windowsLoadSystemLib(winmmdll[:]) + var winmmdll = []byte("winmm.dll\000") + m32 := windowsLoadSystemLib(winmmdll) if m32 == 0 { print("runtime: LoadLibraryExW failed; errno=", getlasterror(), "\n") throw("winmm.dll not found") @@ -471,6 +520,28 @@ canUseLongPaths = true } +var osVersionInfo struct { + majorVersion uint32 + minorVersion uint32 + buildNumber uint32 +} + +func initOsVersionInfo() { + info := _OSVERSIONINFOW{} + info.osVersionInfoSize = uint32(unsafe.Sizeof(info)) + stdcall1(_RtlGetVersion, uintptr(unsafe.Pointer(&info))) + osVersionInfo.majorVersion = info.majorVersion + osVersionInfo.minorVersion = info.minorVersion + osVersionInfo.buildNumber = info.buildNumber +} + +//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) { + *majorVersion = osVersionInfo.majorVersion + *minorVersion = osVersionInfo.minorVersion + *buildNumber = osVersionInfo.buildNumber +} + func osinit() { asmstdcallAddr = unsafe.Pointer(abi.FuncPCABI0(asmstdcall)) @@ -483,8 +554,8 @@ initHighResTimer() timeBeginPeriodRetValue = osRelax(false) - initSysDirectory() initLongPathSupport() + initOsVersionInfo() ncpu = getproccount() @@ -500,7 +571,7 @@ //go:nosplit func readRandom(r []byte) int { n := 0 - if stdcall2(_ProcessPrng, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { + if stdcall2(_RtlGenRandom, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { n = len(r) } return n Index: src/net/hook_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/hook_windows.go b/src/net/hook_windows.go --- a/src/net/hook_windows.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) +++ b/src/net/hook_windows.go (revision 21290de8a4c91408de7c2b5b68757b1e90af49dd) @@ -13,6 +13,7 @@ hostsFilePath = windows.GetSystemDirectory() + "/Drivers/etc/hosts" // Placeholders for socket system calls. + socketFunc func(int, int, int) (syscall.Handle, error) = syscall.Socket wsaSocketFunc func(int32, int32, int32, *syscall.WSAProtocolInfo, uint32, uint32) (syscall.Handle, error) = windows.WSASocket connectFunc func(syscall.Handle, syscall.Sockaddr) error = syscall.Connect listenFunc func(syscall.Handle, int) error = syscall.Listen Index: src/net/internal/socktest/main_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_test.go b/src/net/internal/socktest/main_test.go --- a/src/net/internal/socktest/main_test.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) +++ b/src/net/internal/socktest/main_test.go (revision 21290de8a4c91408de7c2b5b68757b1e90af49dd) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !js && !plan9 && !wasip1 && !windows +//go:build !js && !plan9 && !wasip1 package socktest_test Index: src/net/internal/socktest/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_windows_test.go b/src/net/internal/socktest/main_windows_test.go new file mode 100644 --- /dev/null (revision 21290de8a4c91408de7c2b5b68757b1e90af49dd) +++ b/src/net/internal/socktest/main_windows_test.go (revision 21290de8a4c91408de7c2b5b68757b1e90af49dd) @@ -0,0 +1,22 @@ +// 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 socktest_test + +import "syscall" + +var ( + socketFunc func(int, int, int) (syscall.Handle, error) + closeFunc func(syscall.Handle) error +) + +func installTestHooks() { + socketFunc = sw.Socket + closeFunc = sw.Closesocket +} + +func uninstallTestHooks() { + socketFunc = syscall.Socket + closeFunc = syscall.Closesocket +} Index: src/net/internal/socktest/sys_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/sys_windows.go b/src/net/internal/socktest/sys_windows.go --- a/src/net/internal/socktest/sys_windows.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) +++ b/src/net/internal/socktest/sys_windows.go (revision 21290de8a4c91408de7c2b5b68757b1e90af49dd) @@ -9,6 +9,38 @@ "syscall" ) +// Socket wraps [syscall.Socket]. +func (sw *Switch) Socket(family, sotype, proto int) (s syscall.Handle, err error) { + sw.once.Do(sw.init) + + so := &Status{Cookie: cookie(family, sotype, proto)} + sw.fmu.RLock() + f, _ := sw.fltab[FilterSocket] + sw.fmu.RUnlock() + + af, err := f.apply(so) + if err != nil { + return syscall.InvalidHandle, err + } + s, so.Err = syscall.Socket(family, sotype, proto) + if err = af.apply(so); err != nil { + if so.Err == nil { + syscall.Closesocket(s) + } + return syscall.InvalidHandle, err + } + + sw.smu.Lock() + defer sw.smu.Unlock() + if so.Err != nil { + sw.stats.getLocked(so.Cookie).OpenFailed++ + return syscall.InvalidHandle, so.Err + } + nso := sw.addLocked(s, family, sotype, proto) + sw.stats.getLocked(nso.Cookie).Opened++ + return s, nil +} + // WSASocket wraps [syscall.WSASocket]. func (sw *Switch) WSASocket(family, sotype, proto int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (s syscall.Handle, err error) { sw.once.Do(sw.init) Index: src/net/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/main_windows_test.go b/src/net/main_windows_test.go --- a/src/net/main_windows_test.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) +++ b/src/net/main_windows_test.go (revision 21290de8a4c91408de7c2b5b68757b1e90af49dd) @@ -8,6 +8,7 @@ var ( // Placeholders for saving original socket system calls. + origSocket = socketFunc origWSASocket = wsaSocketFunc origClosesocket = poll.CloseFunc origConnect = connectFunc @@ -17,6 +18,7 @@ ) func installTestHooks() { + socketFunc = sw.Socket wsaSocketFunc = sw.WSASocket poll.CloseFunc = sw.Closesocket connectFunc = sw.Connect @@ -26,6 +28,7 @@ } func uninstallTestHooks() { + socketFunc = origSocket wsaSocketFunc = origWSASocket poll.CloseFunc = origClosesocket connectFunc = origConnect Index: src/net/sock_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/sock_windows.go b/src/net/sock_windows.go --- a/src/net/sock_windows.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) +++ b/src/net/sock_windows.go (revision 21290de8a4c91408de7c2b5b68757b1e90af49dd) @@ -20,6 +20,21 @@ func sysSocket(family, sotype, proto int) (syscall.Handle, error) { s, err := wsaSocketFunc(int32(family), int32(sotype), int32(proto), nil, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT) + if err == nil { + return s, nil + } + // WSA_FLAG_NO_HANDLE_INHERIT flag is not supported on some + // old versions of Windows, see + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms742212(v=vs.85).aspx + // for details. Just use syscall.Socket, if windows.WSASocket failed. + + // See ../syscall/exec_unix.go for description of ForkLock. + syscall.ForkLock.RLock() + s, err = socketFunc(family, sotype, proto) + if err == nil { + syscall.CloseOnExec(s) + } + syscall.ForkLock.RUnlock() if err != nil { return syscall.InvalidHandle, os.NewSyscallError("socket", err) } Index: src/syscall/exec_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go --- a/src/syscall/exec_windows.go (revision 9ac42137ef6730e8b7daca016ece831297a1d75b) +++ b/src/syscall/exec_windows.go (revision 6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76) @@ -14,7 +14,6 @@ "unsafe" ) -// ForkLock is not used on Windows. var ForkLock sync.RWMutex // EscapeArg rewrites command line argument s as prescribed @@ -254,6 +253,9 @@ var zeroProcAttr ProcAttr var zeroSysProcAttr SysProcAttr +//go:linkname rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) + func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) { if len(argv0) == 0 { return 0, 0, EWINDOWS @@ -317,6 +319,17 @@ } } + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + isWin7 := maj < 6 || (maj == 6 && min <= 1) + // NT kernel handles are divisible by 4, with the bottom 3 bits left as + // a tag. The fully set tag correlates with the types of handles we're + // concerned about here. Except, the kernel will interpret some + // special handle values, like -1, -2, and so forth, so kernelbase.dll + // checks to see that those bottom three bits are checked, but that top + // bit is not checked. + isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 } + p, _ := GetCurrentProcess() parentProcess := p if sys.ParentProcess != 0 { @@ -325,7 +338,15 @@ fd := make([]Handle, len(attr.Files)) for i := range attr.Files { if attr.Files[i] > 0 { - err := DuplicateHandle(p, Handle(attr.Files[i]), parentProcess, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) + destinationProcessHandle := parentProcess + + // On Windows 7, console handles aren't real handles, and can only be duplicated + // into the current process, not a parent one, which amounts to the same thing. + if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) { + destinationProcessHandle = p + } + + err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) if err != nil { return 0, 0, err } @@ -356,6 +377,14 @@ fd = append(fd, sys.AdditionalInheritedHandles...) + // On Windows 7, console handles aren't real handles, so don't pass them + // through to PROC_THREAD_ATTRIBUTE_HANDLE_LIST. + for i := range fd { + if isLegacyWin7ConsoleHandle(fd[i]) { + fd[i] = 0 + } + } + // The presence of a NULL handle in the list is enough to cause PROC_THREAD_ATTRIBUTE_HANDLE_LIST // to treat the entire list as empty, so remove NULL handles. j := 0 Index: src/runtime/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/syscall_windows.go b/src/runtime/syscall_windows.go --- a/src/runtime/syscall_windows.go (revision 6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76) +++ b/src/runtime/syscall_windows.go (revision 69e2eed6dd0f6d815ebf15797761c13f31213dd6) @@ -413,10 +413,20 @@ const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 +// When available, this function will use LoadLibraryEx with the filename +// parameter and the important SEARCH_SYSTEM32 argument. But on systems that +// do not have that option, absoluteFilepath should contain a fallback +// to the full path inside of system32 for use with vanilla LoadLibrary. +// //go:linkname syscall_loadsystemlibrary syscall.loadsystemlibrary -func syscall_loadsystemlibrary(filename *uint16) (handle, err uintptr) { - handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryExW)), uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +func syscall_loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle, err uintptr) { + if useLoadLibraryEx { + handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryExW)), uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryW)), uintptr(unsafe.Pointer(absoluteFilepath))) + } KeepAlive(filename) + KeepAlive(absoluteFilepath) if handle != 0 { err = 0 } Index: src/syscall/dll_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/dll_windows.go b/src/syscall/dll_windows.go --- a/src/syscall/dll_windows.go (revision 6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76) +++ b/src/syscall/dll_windows.go (revision 69e2eed6dd0f6d815ebf15797761c13f31213dd6) @@ -44,7 +44,7 @@ func SyscallN(trap uintptr, args ...uintptr) (r1, r2 uintptr, err Errno) func loadlibrary(filename *uint16) (handle uintptr, err Errno) -func loadsystemlibrary(filename *uint16) (handle uintptr, err Errno) +func loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle uintptr, err Errno) func getprocaddress(handle uintptr, procname *uint8) (proc uintptr, err Errno) // A DLL implements access to a single DLL. @@ -53,6 +53,9 @@ Handle Handle } +//go:linkname getSystemDirectory +func getSystemDirectory() string // Implemented in runtime package. + // LoadDLL loads the named DLL file into memory. // // If name is not an absolute path and is not a known system DLL used by @@ -69,7 +72,11 @@ var h uintptr var e Errno if sysdll.IsSystemDLL[name] { - h, e = loadsystemlibrary(namep) + absoluteFilepathp, err := UTF16PtrFromString(getSystemDirectory() + name) + if err != nil { + return nil, err + } + h, e = loadsystemlibrary(namep, absoluteFilepathp) } else { h, e = loadlibrary(namep) } ================================================ FILE: core/Clash.Meta/.github/patch/go1.24.patch ================================================ Subject: [PATCH] Revert "runtime: always use LoadLibraryEx to load system libraries" Revert "syscall: remove Windows 7 console handle workaround" Revert "net: remove sysSocket fallback for Windows 7" Revert "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" --- Index: src/crypto/internal/sysrand/rand_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/internal/sysrand/rand_windows.go b/src/crypto/internal/sysrand/rand_windows.go --- a/src/crypto/internal/sysrand/rand_windows.go (revision 3901409b5d0fb7c85a3e6730a59943cc93b2835c) +++ b/src/crypto/internal/sysrand/rand_windows.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) @@ -7,5 +7,26 @@ import "internal/syscall/windows" func read(b []byte) error { - return windows.ProcessPrng(b) + // RtlGenRandom only returns 1<<32-1 bytes at a time. We only read at + // most 1<<31-1 bytes at a time so that this works the same on 32-bit + // and 64-bit systems. + return batched(windows.RtlGenRandom, 1<<31-1)(b) +} + +// batched returns a function that calls f to populate a []byte by chunking it +// into subslices of, at most, readMax bytes. +func batched(f func([]byte) error, readMax int) func([]byte) error { + return func(out []byte) error { + for len(out) > 0 { + read := len(out) + if read > readMax { + read = readMax + } + if err := f(out[:read]); err != nil { + return err + } + out = out[read:] + } + return nil + } } Index: src/crypto/rand/rand.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand.go b/src/crypto/rand/rand.go --- a/src/crypto/rand/rand.go (revision 3901409b5d0fb7c85a3e6730a59943cc93b2835c) +++ b/src/crypto/rand/rand.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) @@ -22,7 +22,7 @@ // - On legacy Linux (< 3.17), Reader opens /dev/urandom on first use. // - On macOS, iOS, and OpenBSD Reader, uses arc4random_buf(3). // - On NetBSD, Reader uses the kern.arandom sysctl. -// - On Windows, Reader uses the ProcessPrng API. +// - On Windows systems, Reader uses the RtlGenRandom API. // - On js/wasm, Reader uses the Web Crypto API. // - On wasip1/wasm, Reader uses random_get. // Index: src/internal/syscall/windows/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go --- a/src/internal/syscall/windows/syscall_windows.go (revision 3901409b5d0fb7c85a3e6730a59943cc93b2835c) +++ b/src/internal/syscall/windows/syscall_windows.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) @@ -416,7 +416,7 @@ //sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock //sys CreateEvent(eventAttrs *SecurityAttributes, manualReset uint32, initialState uint32, name *uint16) (handle syscall.Handle, err error) = kernel32.CreateEventW -//sys ProcessPrng(buf []byte) (err error) = bcryptprimitives.ProcessPrng +//sys RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036 type FILE_ID_BOTH_DIR_INFO struct { NextEntryOffset uint32 Index: src/internal/syscall/windows/zsyscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go --- a/src/internal/syscall/windows/zsyscall_windows.go (revision 3901409b5d0fb7c85a3e6730a59943cc93b2835c) +++ b/src/internal/syscall/windows/zsyscall_windows.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) @@ -38,7 +38,6 @@ var ( modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) - modbcryptprimitives = syscall.NewLazyDLL(sysdll.Add("bcryptprimitives.dll")) modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) @@ -63,7 +62,7 @@ procQueryServiceStatus = modadvapi32.NewProc("QueryServiceStatus") procRevertToSelf = modadvapi32.NewProc("RevertToSelf") procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation") - procProcessPrng = modbcryptprimitives.NewProc("ProcessPrng") + procSystemFunction036 = modadvapi32.NewProc("SystemFunction036") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procCreateEventW = modkernel32.NewProc("CreateEventW") procGetACP = modkernel32.NewProc("GetACP") @@ -236,12 +235,12 @@ return } -func ProcessPrng(buf []byte) (err error) { +func RtlGenRandom(buf []byte) (err error) { var _p0 *byte if len(buf) > 0 { _p0 = &buf[0] } - r1, _, e1 := syscall.Syscall(procProcessPrng.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) + r1, _, e1 := syscall.Syscall(procSystemFunction036.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0) if r1 == 0 { err = errnoErr(e1) } Index: src/runtime/os_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go --- a/src/runtime/os_windows.go (revision 3901409b5d0fb7c85a3e6730a59943cc93b2835c) +++ b/src/runtime/os_windows.go (revision ac3e93c061779dfefc0dd13a5b6e6f764a25621e) @@ -40,8 +40,8 @@ //go:cgo_import_dynamic runtime._GetSystemInfo GetSystemInfo%1 "kernel32.dll" //go:cgo_import_dynamic runtime._GetThreadContext GetThreadContext%2 "kernel32.dll" //go:cgo_import_dynamic runtime._SetThreadContext SetThreadContext%2 "kernel32.dll" -//go:cgo_import_dynamic runtime._LoadLibraryExW LoadLibraryExW%3 "kernel32.dll" //go:cgo_import_dynamic runtime._LoadLibraryW LoadLibraryW%1 "kernel32.dll" +//go:cgo_import_dynamic runtime._LoadLibraryA LoadLibraryA%1 "kernel32.dll" //go:cgo_import_dynamic runtime._PostQueuedCompletionStatus PostQueuedCompletionStatus%4 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceCounter QueryPerformanceCounter%1 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceFrequency QueryPerformanceFrequency%1 "kernel32.dll" @@ -75,7 +75,6 @@ // Following syscalls are available on every Windows PC. // All these variables are set by the Windows executable // loader before the Go program starts. - _AddVectoredContinueHandler, _AddVectoredExceptionHandler, _CloseHandle, _CreateEventA, @@ -98,8 +97,8 @@ _GetSystemInfo, _GetThreadContext, _SetThreadContext, - _LoadLibraryExW, _LoadLibraryW, + _LoadLibraryA, _PostQueuedCompletionStatus, _QueryPerformanceCounter, _QueryPerformanceFrequency, @@ -128,8 +127,23 @@ _WriteFile, _ stdFunction - // Use ProcessPrng to generate cryptographically random data. - _ProcessPrng stdFunction + // Following syscalls are only available on some Windows PCs. + // We will load syscalls, if available, before using them. + _AddDllDirectory, + _AddVectoredContinueHandler, + _LoadLibraryExA, + _LoadLibraryExW, + _ stdFunction + + // Use RtlGenRandom to generate cryptographically random data. + // This approach has been recommended by Microsoft (see issue + // 15589 for details). + // The RtlGenRandom is not listed in advapi32.dll, instead + // RtlGenRandom function can be found by searching for SystemFunction036. + // Also some versions of Mingw cannot link to SystemFunction036 + // when building executable as Cgo. So load SystemFunction036 + // manually during runtime startup. + _RtlGenRandom stdFunction // Load ntdll.dll manually during startup, otherwise Mingw // links wrong printf function to cgo executable (see issue @@ -146,13 +160,6 @@ _ stdFunction ) -var ( - bcryptprimitivesdll = [...]uint16{'b', 'c', 'r', 'y', 'p', 't', 'p', 'r', 'i', 'm', 'i', 't', 'i', 'v', 'e', 's', '.', 'd', 'l', 'l', 0} - ntdlldll = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0} - powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0} - winmmdll = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0} -) - // Function to be called by windows CreateThread // to start new os thread. func tstart_stdcall(newm *m) @@ -245,8 +252,18 @@ return unsafe.String(&sysDirectory[0], sysDirectoryLen) } -func windowsLoadSystemLib(name []uint16) uintptr { - return stdcall3(_LoadLibraryExW, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +//go:linkname syscall_getSystemDirectory syscall.getSystemDirectory +func syscall_getSystemDirectory() string { + return unsafe.String(&sysDirectory[0], sysDirectoryLen) +} + +func windowsLoadSystemLib(name []byte) uintptr { + if useLoadLibraryEx { + return stdcall3(_LoadLibraryExA, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + absName := append(sysDirectory[:sysDirectoryLen], name...) + return stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&absName[0]))) + } } //go:linkname windows_QueryPerformanceCounter internal/syscall/windows.QueryPerformanceCounter @@ -264,13 +281,28 @@ } func loadOptionalSyscalls() { - bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:]) - if bcryptPrimitives == 0 { - throw("bcryptprimitives.dll not found") + var kernel32dll = []byte("kernel32.dll\000") + k32 := stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&kernel32dll[0]))) + if k32 == 0 { + throw("kernel32.dll not found") } - _ProcessPrng = windowsFindfunc(bcryptPrimitives, []byte("ProcessPrng\000")) + _AddDllDirectory = windowsFindfunc(k32, []byte("AddDllDirectory\000")) + _AddVectoredContinueHandler = windowsFindfunc(k32, []byte("AddVectoredContinueHandler\000")) + _LoadLibraryExA = windowsFindfunc(k32, []byte("LoadLibraryExA\000")) + _LoadLibraryExW = windowsFindfunc(k32, []byte("LoadLibraryExW\000")) + useLoadLibraryEx = (_LoadLibraryExW != nil && _LoadLibraryExA != nil && _AddDllDirectory != nil) + + initSysDirectory() - n32 := windowsLoadSystemLib(ntdlldll[:]) + var advapi32dll = []byte("advapi32.dll\000") + a32 := windowsLoadSystemLib(advapi32dll) + if a32 == 0 { + throw("advapi32.dll not found") + } + _RtlGenRandom = windowsFindfunc(a32, []byte("SystemFunction036\000")) + + var ntdll = []byte("ntdll.dll\000") + n32 := windowsLoadSystemLib(ntdll) if n32 == 0 { throw("ntdll.dll not found") } @@ -299,7 +331,7 @@ context uintptr } - powrprof := windowsLoadSystemLib(powrprofdll[:]) + powrprof := windowsLoadSystemLib([]byte("powrprof.dll\000")) if powrprof == 0 { return // Running on Windows 7, where we don't need it anyway. } @@ -358,6 +390,22 @@ // in sys_windows_386.s and sys_windows_amd64.s: func getlasterror() uint32 +// When loading DLLs, we prefer to use LoadLibraryEx with +// LOAD_LIBRARY_SEARCH_* flags, if available. LoadLibraryEx is not +// available on old Windows, though, and the LOAD_LIBRARY_SEARCH_* +// flags are not available on some versions of Windows without a +// security patch. +// +// https://msdn.microsoft.com/en-us/library/ms684179(v=vs.85).aspx says: +// "Windows 7, Windows Server 2008 R2, Windows Vista, and Windows +// Server 2008: The LOAD_LIBRARY_SEARCH_* flags are available on +// systems that have KB2533623 installed. To determine whether the +// flags are available, use GetProcAddress to get the address of the +// AddDllDirectory, RemoveDllDirectory, or SetDefaultDllDirectories +// function. If GetProcAddress succeeds, the LOAD_LIBRARY_SEARCH_* +// flags can be used with LoadLibraryEx." +var useLoadLibraryEx bool + var timeBeginPeriodRetValue uint32 // osRelaxMinNS indicates that sysmon shouldn't osRelax if the next @@ -431,7 +479,8 @@ // Only load winmm.dll if we need it. // This avoids a dependency on winmm.dll for Go programs // that run on new Windows versions. - m32 := windowsLoadSystemLib(winmmdll[:]) + var winmmdll = []byte("winmm.dll\000") + m32 := windowsLoadSystemLib(winmmdll) if m32 == 0 { print("runtime: LoadLibraryExW failed; errno=", getlasterror(), "\n") throw("winmm.dll not found") @@ -472,6 +521,28 @@ canUseLongPaths = true } +var osVersionInfo struct { + majorVersion uint32 + minorVersion uint32 + buildNumber uint32 +} + +func initOsVersionInfo() { + info := _OSVERSIONINFOW{} + info.osVersionInfoSize = uint32(unsafe.Sizeof(info)) + stdcall1(_RtlGetVersion, uintptr(unsafe.Pointer(&info))) + osVersionInfo.majorVersion = info.majorVersion + osVersionInfo.minorVersion = info.minorVersion + osVersionInfo.buildNumber = info.buildNumber +} + +//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) { + *majorVersion = osVersionInfo.majorVersion + *minorVersion = osVersionInfo.minorVersion + *buildNumber = osVersionInfo.buildNumber +} + func osinit() { asmstdcallAddr = unsafe.Pointer(abi.FuncPCABI0(asmstdcall)) @@ -484,8 +555,8 @@ initHighResTimer() timeBeginPeriodRetValue = osRelax(false) - initSysDirectory() initLongPathSupport() + initOsVersionInfo() ncpu = getproccount() @@ -501,7 +572,7 @@ //go:nosplit func readRandom(r []byte) int { n := 0 - if stdcall2(_ProcessPrng, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { + if stdcall2(_RtlGenRandom, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { n = len(r) } return n Index: src/net/hook_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/hook_windows.go b/src/net/hook_windows.go --- a/src/net/hook_windows.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) +++ b/src/net/hook_windows.go (revision 7b1fd7d39c6be0185fbe1d929578ab372ac5c632) @@ -13,6 +13,7 @@ hostsFilePath = windows.GetSystemDirectory() + "/Drivers/etc/hosts" // Placeholders for socket system calls. + socketFunc func(int, int, int) (syscall.Handle, error) = syscall.Socket wsaSocketFunc func(int32, int32, int32, *syscall.WSAProtocolInfo, uint32, uint32) (syscall.Handle, error) = windows.WSASocket connectFunc func(syscall.Handle, syscall.Sockaddr) error = syscall.Connect listenFunc func(syscall.Handle, int) error = syscall.Listen Index: src/net/internal/socktest/main_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_test.go b/src/net/internal/socktest/main_test.go --- a/src/net/internal/socktest/main_test.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) +++ b/src/net/internal/socktest/main_test.go (revision 7b1fd7d39c6be0185fbe1d929578ab372ac5c632) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !js && !plan9 && !wasip1 && !windows +//go:build !js && !plan9 && !wasip1 package socktest_test Index: src/net/internal/socktest/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_windows_test.go b/src/net/internal/socktest/main_windows_test.go new file mode 100644 --- /dev/null (revision 7b1fd7d39c6be0185fbe1d929578ab372ac5c632) +++ b/src/net/internal/socktest/main_windows_test.go (revision 7b1fd7d39c6be0185fbe1d929578ab372ac5c632) @@ -0,0 +1,22 @@ +// 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 socktest_test + +import "syscall" + +var ( + socketFunc func(int, int, int) (syscall.Handle, error) + closeFunc func(syscall.Handle) error +) + +func installTestHooks() { + socketFunc = sw.Socket + closeFunc = sw.Closesocket +} + +func uninstallTestHooks() { + socketFunc = syscall.Socket + closeFunc = syscall.Closesocket +} Index: src/net/internal/socktest/sys_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/sys_windows.go b/src/net/internal/socktest/sys_windows.go --- a/src/net/internal/socktest/sys_windows.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) +++ b/src/net/internal/socktest/sys_windows.go (revision 7b1fd7d39c6be0185fbe1d929578ab372ac5c632) @@ -9,6 +9,38 @@ "syscall" ) +// Socket wraps [syscall.Socket]. +func (sw *Switch) Socket(family, sotype, proto int) (s syscall.Handle, err error) { + sw.once.Do(sw.init) + + so := &Status{Cookie: cookie(family, sotype, proto)} + sw.fmu.RLock() + f, _ := sw.fltab[FilterSocket] + sw.fmu.RUnlock() + + af, err := f.apply(so) + if err != nil { + return syscall.InvalidHandle, err + } + s, so.Err = syscall.Socket(family, sotype, proto) + if err = af.apply(so); err != nil { + if so.Err == nil { + syscall.Closesocket(s) + } + return syscall.InvalidHandle, err + } + + sw.smu.Lock() + defer sw.smu.Unlock() + if so.Err != nil { + sw.stats.getLocked(so.Cookie).OpenFailed++ + return syscall.InvalidHandle, so.Err + } + nso := sw.addLocked(s, family, sotype, proto) + sw.stats.getLocked(nso.Cookie).Opened++ + return s, nil +} + // WSASocket wraps [syscall.WSASocket]. func (sw *Switch) WSASocket(family, sotype, proto int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (s syscall.Handle, err error) { sw.once.Do(sw.init) Index: src/net/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/main_windows_test.go b/src/net/main_windows_test.go --- a/src/net/main_windows_test.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) +++ b/src/net/main_windows_test.go (revision 7b1fd7d39c6be0185fbe1d929578ab372ac5c632) @@ -8,6 +8,7 @@ var ( // Placeholders for saving original socket system calls. + origSocket = socketFunc origWSASocket = wsaSocketFunc origClosesocket = poll.CloseFunc origConnect = connectFunc @@ -17,6 +18,7 @@ ) func installTestHooks() { + socketFunc = sw.Socket wsaSocketFunc = sw.WSASocket poll.CloseFunc = sw.Closesocket connectFunc = sw.Connect @@ -26,6 +28,7 @@ } func uninstallTestHooks() { + socketFunc = origSocket wsaSocketFunc = origWSASocket poll.CloseFunc = origClosesocket connectFunc = origConnect Index: src/net/sock_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/sock_windows.go b/src/net/sock_windows.go --- a/src/net/sock_windows.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) +++ b/src/net/sock_windows.go (revision 7b1fd7d39c6be0185fbe1d929578ab372ac5c632) @@ -20,6 +20,21 @@ func sysSocket(family, sotype, proto int) (syscall.Handle, error) { s, err := wsaSocketFunc(int32(family), int32(sotype), int32(proto), nil, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT) + if err == nil { + return s, nil + } + // WSA_FLAG_NO_HANDLE_INHERIT flag is not supported on some + // old versions of Windows, see + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms742212(v=vs.85).aspx + // for details. Just use syscall.Socket, if windows.WSASocket failed. + + // See ../syscall/exec_unix.go for description of ForkLock. + syscall.ForkLock.RLock() + s, err = socketFunc(family, sotype, proto) + if err == nil { + syscall.CloseOnExec(s) + } + syscall.ForkLock.RUnlock() if err != nil { return syscall.InvalidHandle, os.NewSyscallError("socket", err) } Index: src/syscall/exec_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go --- a/src/syscall/exec_windows.go (revision 2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8) +++ b/src/syscall/exec_windows.go (revision 979d6d8bab3823ff572ace26767fd2ce3cf351ae) @@ -14,7 +14,6 @@ "unsafe" ) -// ForkLock is not used on Windows. var ForkLock sync.RWMutex // EscapeArg rewrites command line argument s as prescribed @@ -254,6 +253,9 @@ var zeroProcAttr ProcAttr var zeroSysProcAttr SysProcAttr +//go:linkname rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) + func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) { if len(argv0) == 0 { return 0, 0, EWINDOWS @@ -317,6 +319,17 @@ } } + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + isWin7 := maj < 6 || (maj == 6 && min <= 1) + // NT kernel handles are divisible by 4, with the bottom 3 bits left as + // a tag. The fully set tag correlates with the types of handles we're + // concerned about here. Except, the kernel will interpret some + // special handle values, like -1, -2, and so forth, so kernelbase.dll + // checks to see that those bottom three bits are checked, but that top + // bit is not checked. + isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 } + p, _ := GetCurrentProcess() parentProcess := p if sys.ParentProcess != 0 { @@ -325,7 +338,15 @@ fd := make([]Handle, len(attr.Files)) for i := range attr.Files { if attr.Files[i] > 0 { - err := DuplicateHandle(p, Handle(attr.Files[i]), parentProcess, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) + destinationProcessHandle := parentProcess + + // On Windows 7, console handles aren't real handles, and can only be duplicated + // into the current process, not a parent one, which amounts to the same thing. + if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) { + destinationProcessHandle = p + } + + err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) if err != nil { return 0, 0, err } @@ -356,6 +377,14 @@ fd = append(fd, sys.AdditionalInheritedHandles...) + // On Windows 7, console handles aren't real handles, so don't pass them + // through to PROC_THREAD_ATTRIBUTE_HANDLE_LIST. + for i := range fd { + if isLegacyWin7ConsoleHandle(fd[i]) { + fd[i] = 0 + } + } + // The presence of a NULL handle in the list is enough to cause PROC_THREAD_ATTRIBUTE_HANDLE_LIST // to treat the entire list as empty, so remove NULL handles. j := 0 Index: src/runtime/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/syscall_windows.go b/src/runtime/syscall_windows.go --- a/src/runtime/syscall_windows.go (revision 979d6d8bab3823ff572ace26767fd2ce3cf351ae) +++ b/src/runtime/syscall_windows.go (revision ac3e93c061779dfefc0dd13a5b6e6f764a25621e) @@ -413,10 +413,20 @@ const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 +// When available, this function will use LoadLibraryEx with the filename +// parameter and the important SEARCH_SYSTEM32 argument. But on systems that +// do not have that option, absoluteFilepath should contain a fallback +// to the full path inside of system32 for use with vanilla LoadLibrary. +// //go:linkname syscall_loadsystemlibrary syscall.loadsystemlibrary -func syscall_loadsystemlibrary(filename *uint16) (handle, err uintptr) { - handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryExW)), uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +func syscall_loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle, err uintptr) { + if useLoadLibraryEx { + handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryExW)), uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryW)), uintptr(unsafe.Pointer(absoluteFilepath))) + } KeepAlive(filename) + KeepAlive(absoluteFilepath) if handle != 0 { err = 0 } Index: src/syscall/dll_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/dll_windows.go b/src/syscall/dll_windows.go --- a/src/syscall/dll_windows.go (revision 979d6d8bab3823ff572ace26767fd2ce3cf351ae) +++ b/src/syscall/dll_windows.go (revision ac3e93c061779dfefc0dd13a5b6e6f764a25621e) @@ -45,7 +45,7 @@ //go:noescape func SyscallN(trap uintptr, args ...uintptr) (r1, r2 uintptr, err Errno) func loadlibrary(filename *uint16) (handle uintptr, err Errno) -func loadsystemlibrary(filename *uint16) (handle uintptr, err Errno) +func loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle uintptr, err Errno) func getprocaddress(handle uintptr, procname *uint8) (proc uintptr, err Errno) // A DLL implements access to a single DLL. @@ -54,6 +54,9 @@ Handle Handle } +//go:linkname getSystemDirectory +func getSystemDirectory() string // Implemented in runtime package. + // LoadDLL loads the named DLL file into memory. // // If name is not an absolute path and is not a known system DLL used by @@ -70,7 +73,11 @@ var h uintptr var e Errno if sysdll.IsSystemDLL[name] { - h, e = loadsystemlibrary(namep) + absoluteFilepathp, err := UTF16PtrFromString(getSystemDirectory() + name) + if err != nil { + return nil, err + } + h, e = loadsystemlibrary(namep, absoluteFilepathp) } else { h, e = loadlibrary(namep) } ================================================ FILE: core/Clash.Meta/.github/patch/go1.25.patch ================================================ Subject: [PATCH] Revert "os: remove 5ms sleep on Windows in (*Process).Wait" Fix os.RemoveAll not working on Windows7 Revert "runtime: always use LoadLibraryEx to load system libraries" Revert "syscall: remove Windows 7 console handle workaround" Revert "net: remove sysSocket fallback for Windows 7" Revert "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" --- Index: src/crypto/internal/sysrand/rand_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/internal/sysrand/rand_windows.go b/src/crypto/internal/sysrand/rand_windows.go --- a/src/crypto/internal/sysrand/rand_windows.go (revision 439ff996f0ee506fc2eb84b7f11ffc360a6299f2) +++ b/src/crypto/internal/sysrand/rand_windows.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) @@ -7,5 +7,26 @@ import "internal/syscall/windows" func read(b []byte) error { - return windows.ProcessPrng(b) + // RtlGenRandom only returns 1<<32-1 bytes at a time. We only read at + // most 1<<31-1 bytes at a time so that this works the same on 32-bit + // and 64-bit systems. + return batched(windows.RtlGenRandom, 1<<31-1)(b) +} + +// batched returns a function that calls f to populate a []byte by chunking it +// into subslices of, at most, readMax bytes. +func batched(f func([]byte) error, readMax int) func([]byte) error { + return func(out []byte) error { + for len(out) > 0 { + read := len(out) + if read > readMax { + read = readMax + } + if err := f(out[:read]); err != nil { + return err + } + out = out[read:] + } + return nil + } } Index: src/crypto/rand/rand.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand.go b/src/crypto/rand/rand.go --- a/src/crypto/rand/rand.go (revision 439ff996f0ee506fc2eb84b7f11ffc360a6299f2) +++ b/src/crypto/rand/rand.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) @@ -22,7 +22,7 @@ // - On legacy Linux (< 3.17), Reader opens /dev/urandom on first use. // - On macOS, iOS, and OpenBSD Reader, uses arc4random_buf(3). // - On NetBSD, Reader uses the kern.arandom sysctl. -// - On Windows, Reader uses the ProcessPrng API. +// - On Windows systems, Reader uses the RtlGenRandom API. // - On js/wasm, Reader uses the Web Crypto API. // - On wasip1/wasm, Reader uses random_get. // Index: src/internal/syscall/windows/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go --- a/src/internal/syscall/windows/syscall_windows.go (revision 439ff996f0ee506fc2eb84b7f11ffc360a6299f2) +++ b/src/internal/syscall/windows/syscall_windows.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) @@ -419,7 +419,7 @@ //sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock //sys CreateEvent(eventAttrs *SecurityAttributes, manualReset uint32, initialState uint32, name *uint16) (handle syscall.Handle, err error) = kernel32.CreateEventW -//sys ProcessPrng(buf []byte) (err error) = bcryptprimitives.ProcessPrng +//sys RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036 type FILE_ID_BOTH_DIR_INFO struct { NextEntryOffset uint32 Index: src/internal/syscall/windows/zsyscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go --- a/src/internal/syscall/windows/zsyscall_windows.go (revision 439ff996f0ee506fc2eb84b7f11ffc360a6299f2) +++ b/src/internal/syscall/windows/zsyscall_windows.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) @@ -38,7 +38,6 @@ var ( modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) - modbcryptprimitives = syscall.NewLazyDLL(sysdll.Add("bcryptprimitives.dll")) modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) @@ -65,7 +64,7 @@ procSetEntriesInAclW = modadvapi32.NewProc("SetEntriesInAclW") procSetNamedSecurityInfoW = modadvapi32.NewProc("SetNamedSecurityInfoW") procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation") - procProcessPrng = modbcryptprimitives.NewProc("ProcessPrng") + procSystemFunction036 = modadvapi32.NewProc("SystemFunction036") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procCreateEventW = modkernel32.NewProc("CreateEventW") procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort") @@ -270,12 +269,12 @@ return } -func ProcessPrng(buf []byte) (err error) { +func RtlGenRandom(buf []byte) (err error) { var _p0 *byte if len(buf) > 0 { _p0 = &buf[0] } - r1, _, e1 := syscall.SyscallN(procProcessPrng.Addr(), uintptr(unsafe.Pointer(_p0)), uintptr(len(buf))) + r1, _, e1 := syscall.SyscallN(procSystemFunction036.Addr(), uintptr(unsafe.Pointer(_p0)), uintptr(len(buf))) if r1 == 0 { err = errnoErr(e1) } Index: src/runtime/os_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go --- a/src/runtime/os_windows.go (revision 439ff996f0ee506fc2eb84b7f11ffc360a6299f2) +++ b/src/runtime/os_windows.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) @@ -39,8 +39,8 @@ //go:cgo_import_dynamic runtime._GetSystemInfo GetSystemInfo%1 "kernel32.dll" //go:cgo_import_dynamic runtime._GetThreadContext GetThreadContext%2 "kernel32.dll" //go:cgo_import_dynamic runtime._SetThreadContext SetThreadContext%2 "kernel32.dll" -//go:cgo_import_dynamic runtime._LoadLibraryExW LoadLibraryExW%3 "kernel32.dll" //go:cgo_import_dynamic runtime._LoadLibraryW LoadLibraryW%1 "kernel32.dll" +//go:cgo_import_dynamic runtime._LoadLibraryA LoadLibraryA%1 "kernel32.dll" //go:cgo_import_dynamic runtime._PostQueuedCompletionStatus PostQueuedCompletionStatus%4 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceCounter QueryPerformanceCounter%1 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceFrequency QueryPerformanceFrequency%1 "kernel32.dll" @@ -74,7 +74,6 @@ // Following syscalls are available on every Windows PC. // All these variables are set by the Windows executable // loader before the Go program starts. - _AddVectoredContinueHandler, _AddVectoredExceptionHandler, _CloseHandle, _CreateEventA, @@ -97,8 +96,8 @@ _GetSystemInfo, _GetThreadContext, _SetThreadContext, - _LoadLibraryExW, _LoadLibraryW, + _LoadLibraryA, _PostQueuedCompletionStatus, _QueryPerformanceCounter, _QueryPerformanceFrequency, @@ -127,8 +126,23 @@ _WriteFile, _ stdFunction - // Use ProcessPrng to generate cryptographically random data. - _ProcessPrng stdFunction + // Following syscalls are only available on some Windows PCs. + // We will load syscalls, if available, before using them. + _AddDllDirectory, + _AddVectoredContinueHandler, + _LoadLibraryExA, + _LoadLibraryExW, + _ stdFunction + + // Use RtlGenRandom to generate cryptographically random data. + // This approach has been recommended by Microsoft (see issue + // 15589 for details). + // The RtlGenRandom is not listed in advapi32.dll, instead + // RtlGenRandom function can be found by searching for SystemFunction036. + // Also some versions of Mingw cannot link to SystemFunction036 + // when building executable as Cgo. So load SystemFunction036 + // manually during runtime startup. + _RtlGenRandom stdFunction // Load ntdll.dll manually during startup, otherwise Mingw // links wrong printf function to cgo executable (see issue @@ -145,13 +159,6 @@ _ stdFunction ) -var ( - bcryptprimitivesdll = [...]uint16{'b', 'c', 'r', 'y', 'p', 't', 'p', 'r', 'i', 'm', 'i', 't', 'i', 'v', 'e', 's', '.', 'd', 'l', 'l', 0} - ntdlldll = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0} - powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0} - winmmdll = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0} -) - // Function to be called by windows CreateThread // to start new os thread. func tstart_stdcall(newm *m) @@ -244,8 +251,18 @@ return unsafe.String(&sysDirectory[0], sysDirectoryLen) } -func windowsLoadSystemLib(name []uint16) uintptr { - return stdcall3(_LoadLibraryExW, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +//go:linkname syscall_getSystemDirectory syscall.getSystemDirectory +func syscall_getSystemDirectory() string { + return unsafe.String(&sysDirectory[0], sysDirectoryLen) +} + +func windowsLoadSystemLib(name []byte) uintptr { + if useLoadLibraryEx { + return stdcall3(_LoadLibraryExA, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + absName := append(sysDirectory[:sysDirectoryLen], name...) + return stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&absName[0]))) + } } //go:linkname windows_QueryPerformanceCounter internal/syscall/windows.QueryPerformanceCounter @@ -263,13 +280,28 @@ } func loadOptionalSyscalls() { - bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:]) - if bcryptPrimitives == 0 { - throw("bcryptprimitives.dll not found") + var kernel32dll = []byte("kernel32.dll\000") + k32 := stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&kernel32dll[0]))) + if k32 == 0 { + throw("kernel32.dll not found") } - _ProcessPrng = windowsFindfunc(bcryptPrimitives, []byte("ProcessPrng\000")) + _AddDllDirectory = windowsFindfunc(k32, []byte("AddDllDirectory\000")) + _AddVectoredContinueHandler = windowsFindfunc(k32, []byte("AddVectoredContinueHandler\000")) + _LoadLibraryExA = windowsFindfunc(k32, []byte("LoadLibraryExA\000")) + _LoadLibraryExW = windowsFindfunc(k32, []byte("LoadLibraryExW\000")) + useLoadLibraryEx = (_LoadLibraryExW != nil && _LoadLibraryExA != nil && _AddDllDirectory != nil) + + initSysDirectory() - n32 := windowsLoadSystemLib(ntdlldll[:]) + var advapi32dll = []byte("advapi32.dll\000") + a32 := windowsLoadSystemLib(advapi32dll) + if a32 == 0 { + throw("advapi32.dll not found") + } + _RtlGenRandom = windowsFindfunc(a32, []byte("SystemFunction036\000")) + + var ntdll = []byte("ntdll.dll\000") + n32 := windowsLoadSystemLib(ntdll) if n32 == 0 { throw("ntdll.dll not found") } @@ -298,7 +330,7 @@ context uintptr } - powrprof := windowsLoadSystemLib(powrprofdll[:]) + powrprof := windowsLoadSystemLib([]byte("powrprof.dll\000")) if powrprof == 0 { return // Running on Windows 7, where we don't need it anyway. } @@ -357,6 +389,22 @@ // in sys_windows_386.s and sys_windows_amd64.s: func getlasterror() uint32 +// When loading DLLs, we prefer to use LoadLibraryEx with +// LOAD_LIBRARY_SEARCH_* flags, if available. LoadLibraryEx is not +// available on old Windows, though, and the LOAD_LIBRARY_SEARCH_* +// flags are not available on some versions of Windows without a +// security patch. +// +// https://msdn.microsoft.com/en-us/library/ms684179(v=vs.85).aspx says: +// "Windows 7, Windows Server 2008 R2, Windows Vista, and Windows +// Server 2008: The LOAD_LIBRARY_SEARCH_* flags are available on +// systems that have KB2533623 installed. To determine whether the +// flags are available, use GetProcAddress to get the address of the +// AddDllDirectory, RemoveDllDirectory, or SetDefaultDllDirectories +// function. If GetProcAddress succeeds, the LOAD_LIBRARY_SEARCH_* +// flags can be used with LoadLibraryEx." +var useLoadLibraryEx bool + var timeBeginPeriodRetValue uint32 // osRelaxMinNS indicates that sysmon shouldn't osRelax if the next @@ -430,7 +478,8 @@ // Only load winmm.dll if we need it. // This avoids a dependency on winmm.dll for Go programs // that run on new Windows versions. - m32 := windowsLoadSystemLib(winmmdll[:]) + var winmmdll = []byte("winmm.dll\000") + m32 := windowsLoadSystemLib(winmmdll) if m32 == 0 { print("runtime: LoadLibraryExW failed; errno=", getlasterror(), "\n") throw("winmm.dll not found") @@ -471,6 +520,28 @@ canUseLongPaths = true } +var osVersionInfo struct { + majorVersion uint32 + minorVersion uint32 + buildNumber uint32 +} + +func initOsVersionInfo() { + info := _OSVERSIONINFOW{} + info.osVersionInfoSize = uint32(unsafe.Sizeof(info)) + stdcall1(_RtlGetVersion, uintptr(unsafe.Pointer(&info))) + osVersionInfo.majorVersion = info.majorVersion + osVersionInfo.minorVersion = info.minorVersion + osVersionInfo.buildNumber = info.buildNumber +} + +//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) { + *majorVersion = osVersionInfo.majorVersion + *minorVersion = osVersionInfo.minorVersion + *buildNumber = osVersionInfo.buildNumber +} + func osinit() { asmstdcallAddr = unsafe.Pointer(abi.FuncPCABI0(asmstdcall)) @@ -483,8 +554,8 @@ initHighResTimer() timeBeginPeriodRetValue = osRelax(false) - initSysDirectory() initLongPathSupport() + initOsVersionInfo() numCPUStartup = getCPUCount() @@ -500,7 +571,7 @@ //go:nosplit func readRandom(r []byte) int { n := 0 - if stdcall2(_ProcessPrng, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { + if stdcall2(_RtlGenRandom, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { n = len(r) } return n Index: src/net/hook_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/hook_windows.go b/src/net/hook_windows.go --- a/src/net/hook_windows.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) +++ b/src/net/hook_windows.go (revision 1bdabae205052afe1dadb2ad6f1ba612cdbc532a) @@ -13,6 +13,7 @@ hostsFilePath = windows.GetSystemDirectory() + "/Drivers/etc/hosts" // Placeholders for socket system calls. + socketFunc func(int, int, int) (syscall.Handle, error) = syscall.Socket wsaSocketFunc func(int32, int32, int32, *syscall.WSAProtocolInfo, uint32, uint32) (syscall.Handle, error) = windows.WSASocket connectFunc func(syscall.Handle, syscall.Sockaddr) error = syscall.Connect listenFunc func(syscall.Handle, int) error = syscall.Listen Index: src/net/internal/socktest/main_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_test.go b/src/net/internal/socktest/main_test.go --- a/src/net/internal/socktest/main_test.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) +++ b/src/net/internal/socktest/main_test.go (revision 1bdabae205052afe1dadb2ad6f1ba612cdbc532a) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !js && !plan9 && !wasip1 && !windows +//go:build !js && !plan9 && !wasip1 package socktest_test Index: src/net/internal/socktest/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_windows_test.go b/src/net/internal/socktest/main_windows_test.go new file mode 100644 --- /dev/null (revision 1bdabae205052afe1dadb2ad6f1ba612cdbc532a) +++ b/src/net/internal/socktest/main_windows_test.go (revision 1bdabae205052afe1dadb2ad6f1ba612cdbc532a) @@ -0,0 +1,22 @@ +// 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 socktest_test + +import "syscall" + +var ( + socketFunc func(int, int, int) (syscall.Handle, error) + closeFunc func(syscall.Handle) error +) + +func installTestHooks() { + socketFunc = sw.Socket + closeFunc = sw.Closesocket +} + +func uninstallTestHooks() { + socketFunc = syscall.Socket + closeFunc = syscall.Closesocket +} Index: src/net/internal/socktest/sys_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/sys_windows.go b/src/net/internal/socktest/sys_windows.go --- a/src/net/internal/socktest/sys_windows.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) +++ b/src/net/internal/socktest/sys_windows.go (revision 1bdabae205052afe1dadb2ad6f1ba612cdbc532a) @@ -9,6 +9,38 @@ "syscall" ) +// Socket wraps [syscall.Socket]. +func (sw *Switch) Socket(family, sotype, proto int) (s syscall.Handle, err error) { + sw.once.Do(sw.init) + + so := &Status{Cookie: cookie(family, sotype, proto)} + sw.fmu.RLock() + f, _ := sw.fltab[FilterSocket] + sw.fmu.RUnlock() + + af, err := f.apply(so) + if err != nil { + return syscall.InvalidHandle, err + } + s, so.Err = syscall.Socket(family, sotype, proto) + if err = af.apply(so); err != nil { + if so.Err == nil { + syscall.Closesocket(s) + } + return syscall.InvalidHandle, err + } + + sw.smu.Lock() + defer sw.smu.Unlock() + if so.Err != nil { + sw.stats.getLocked(so.Cookie).OpenFailed++ + return syscall.InvalidHandle, so.Err + } + nso := sw.addLocked(s, family, sotype, proto) + sw.stats.getLocked(nso.Cookie).Opened++ + return s, nil +} + // WSASocket wraps [syscall.WSASocket]. func (sw *Switch) WSASocket(family, sotype, proto int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (s syscall.Handle, err error) { sw.once.Do(sw.init) Index: src/net/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/main_windows_test.go b/src/net/main_windows_test.go --- a/src/net/main_windows_test.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) +++ b/src/net/main_windows_test.go (revision 1bdabae205052afe1dadb2ad6f1ba612cdbc532a) @@ -12,6 +12,7 @@ var ( // Placeholders for saving original socket system calls. + origSocket = socketFunc origWSASocket = wsaSocketFunc origClosesocket = poll.CloseFunc origConnect = connectFunc @@ -21,6 +22,7 @@ ) func installTestHooks() { + socketFunc = sw.Socket wsaSocketFunc = sw.WSASocket poll.CloseFunc = sw.Closesocket connectFunc = sw.Connect @@ -30,6 +32,7 @@ } func uninstallTestHooks() { + socketFunc = origSocket wsaSocketFunc = origWSASocket poll.CloseFunc = origClosesocket connectFunc = origConnect Index: src/net/sock_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/sock_windows.go b/src/net/sock_windows.go --- a/src/net/sock_windows.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) +++ b/src/net/sock_windows.go (revision 1bdabae205052afe1dadb2ad6f1ba612cdbc532a) @@ -20,6 +20,21 @@ func sysSocket(family, sotype, proto int) (syscall.Handle, error) { s, err := wsaSocketFunc(int32(family), int32(sotype), int32(proto), nil, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT) + if err == nil { + return s, nil + } + // WSA_FLAG_NO_HANDLE_INHERIT flag is not supported on some + // old versions of Windows, see + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms742212(v=vs.85).aspx + // for details. Just use syscall.Socket, if windows.WSASocket failed. + + // See ../syscall/exec_unix.go for description of ForkLock. + syscall.ForkLock.RLock() + s, err = socketFunc(family, sotype, proto) + if err == nil { + syscall.CloseOnExec(s) + } + syscall.ForkLock.RUnlock() if err != nil { return syscall.InvalidHandle, os.NewSyscallError("socket", err) } Index: src/syscall/exec_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go --- a/src/syscall/exec_windows.go (revision 466f6c7a29bc098b0d4c987b803c779222894a11) +++ b/src/syscall/exec_windows.go (revision a90777dcf692dd2168577853ba743b4338721b06) @@ -14,7 +14,6 @@ "unsafe" ) -// ForkLock is not used on Windows. var ForkLock sync.RWMutex // EscapeArg rewrites command line argument s as prescribed @@ -254,6 +253,9 @@ var zeroProcAttr ProcAttr var zeroSysProcAttr SysProcAttr +//go:linkname rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) + func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) { if len(argv0) == 0 { return 0, 0, EWINDOWS @@ -317,6 +319,17 @@ } } + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + isWin7 := maj < 6 || (maj == 6 && min <= 1) + // NT kernel handles are divisible by 4, with the bottom 3 bits left as + // a tag. The fully set tag correlates with the types of handles we're + // concerned about here. Except, the kernel will interpret some + // special handle values, like -1, -2, and so forth, so kernelbase.dll + // checks to see that those bottom three bits are checked, but that top + // bit is not checked. + isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 } + p, _ := GetCurrentProcess() parentProcess := p if sys.ParentProcess != 0 { @@ -325,7 +338,15 @@ fd := make([]Handle, len(attr.Files)) for i := range attr.Files { if attr.Files[i] > 0 { - err := DuplicateHandle(p, Handle(attr.Files[i]), parentProcess, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) + destinationProcessHandle := parentProcess + + // On Windows 7, console handles aren't real handles, and can only be duplicated + // into the current process, not a parent one, which amounts to the same thing. + if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) { + destinationProcessHandle = p + } + + err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) if err != nil { return 0, 0, err } @@ -356,6 +377,14 @@ fd = append(fd, sys.AdditionalInheritedHandles...) + // On Windows 7, console handles aren't real handles, so don't pass them + // through to PROC_THREAD_ATTRIBUTE_HANDLE_LIST. + for i := range fd { + if isLegacyWin7ConsoleHandle(fd[i]) { + fd[i] = 0 + } + } + // The presence of a NULL handle in the list is enough to cause PROC_THREAD_ATTRIBUTE_HANDLE_LIST // to treat the entire list as empty, so remove NULL handles. j := 0 Index: src/runtime/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/syscall_windows.go b/src/runtime/syscall_windows.go --- a/src/runtime/syscall_windows.go (revision a90777dcf692dd2168577853ba743b4338721b06) +++ b/src/runtime/syscall_windows.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) @@ -413,10 +413,20 @@ const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 +// When available, this function will use LoadLibraryEx with the filename +// parameter and the important SEARCH_SYSTEM32 argument. But on systems that +// do not have that option, absoluteFilepath should contain a fallback +// to the full path inside of system32 for use with vanilla LoadLibrary. +// //go:linkname syscall_loadsystemlibrary syscall.loadsystemlibrary -func syscall_loadsystemlibrary(filename *uint16) (handle, err uintptr) { - handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryExW)), uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +func syscall_loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle, err uintptr) { + if useLoadLibraryEx { + handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryExW)), uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + handle, _, err = syscall_SyscallN(uintptr(unsafe.Pointer(_LoadLibraryW)), uintptr(unsafe.Pointer(absoluteFilepath))) + } KeepAlive(filename) + KeepAlive(absoluteFilepath) if handle != 0 { err = 0 } Index: src/syscall/dll_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/dll_windows.go b/src/syscall/dll_windows.go --- a/src/syscall/dll_windows.go (revision a90777dcf692dd2168577853ba743b4338721b06) +++ b/src/syscall/dll_windows.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) @@ -45,7 +45,7 @@ //go:noescape func SyscallN(trap uintptr, args ...uintptr) (r1, r2 uintptr, err Errno) func loadlibrary(filename *uint16) (handle uintptr, err Errno) -func loadsystemlibrary(filename *uint16) (handle uintptr, err Errno) +func loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle uintptr, err Errno) func getprocaddress(handle uintptr, procname *uint8) (proc uintptr, err Errno) // A DLL implements access to a single DLL. @@ -54,6 +54,9 @@ Handle Handle } +//go:linkname getSystemDirectory +func getSystemDirectory() string // Implemented in runtime package. + // LoadDLL loads the named DLL file into memory. // // If name is not an absolute path and is not a known system DLL used by @@ -70,7 +73,11 @@ var h uintptr var e Errno if sysdll.IsSystemDLL[name] { - h, e = loadsystemlibrary(namep) + absoluteFilepathp, err := UTF16PtrFromString(getSystemDirectory() + name) + if err != nil { + return nil, err + } + h, e = loadsystemlibrary(namep, absoluteFilepathp) } else { h, e = loadlibrary(namep) } Index: src/os/removeall_at.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/removeall_at.go b/src/os/removeall_at.go --- a/src/os/removeall_at.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) +++ b/src/os/removeall_at.go (revision bed309eff415bcb3c77dd4bc3277b682b89a388d) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build unix || wasip1 || windows +//go:build unix || wasip1 package os @@ -175,3 +175,25 @@ } return newDirFile(fd, name) } + +func rootRemoveAll(r *Root, name string) error { + // Consistency with os.RemoveAll: Strip trailing /s from the name, + // so RemoveAll("not_a_directory/") succeeds. + for len(name) > 0 && IsPathSeparator(name[len(name)-1]) { + name = name[:len(name)-1] + } + if endsWithDot(name) { + // Consistency with os.RemoveAll: Return EINVAL when trying to remove . + return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} + } + _, err := doInRoot(r, name, nil, func(parent sysfdType, name string) (struct{}, error) { + return struct{}{}, removeAllFrom(parent, name) + }) + if IsNotExist(err) { + return nil + } + if err != nil { + return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} + } + return err +} Index: src/os/removeall_noat.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/removeall_noat.go b/src/os/removeall_noat.go --- a/src/os/removeall_noat.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) +++ b/src/os/removeall_noat.go (revision bed309eff415bcb3c77dd4bc3277b682b89a388d) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (js && wasm) || plan9 +//go:build (js && wasm) || plan9 || windows package os @@ -140,3 +140,22 @@ } return err } + +func rootRemoveAll(r *Root, name string) error { + if endsWithDot(name) { + // Consistency with os.RemoveAll: Return EINVAL when trying to remove . + return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} + } + if err := checkPathEscapesLstat(r, name); err != nil { + if err == syscall.ENOTDIR { + // Some intermediate path component is not a directory. + // RemoveAll treats this as success (since the target doesn't exist). + return nil + } + return &PathError{Op: "RemoveAll", Path: name, Err: err} + } + if err := RemoveAll(joinPath(r.root.name, name)); err != nil { + return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} + } + return nil +} Index: src/os/root_noopenat.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/root_noopenat.go b/src/os/root_noopenat.go --- a/src/os/root_noopenat.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) +++ b/src/os/root_noopenat.go (revision bed309eff415bcb3c77dd4bc3277b682b89a388d) @@ -11,7 +11,6 @@ "internal/filepathlite" "internal/stringslite" "sync/atomic" - "syscall" "time" ) @@ -185,25 +184,6 @@ } return nil } - -func rootRemoveAll(r *Root, name string) error { - if endsWithDot(name) { - // Consistency with os.RemoveAll: Return EINVAL when trying to remove . - return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} - } - if err := checkPathEscapesLstat(r, name); err != nil { - if err == syscall.ENOTDIR { - // Some intermediate path component is not a directory. - // RemoveAll treats this as success (since the target doesn't exist). - return nil - } - return &PathError{Op: "RemoveAll", Path: name, Err: err} - } - if err := RemoveAll(joinPath(r.root.name, name)); err != nil { - return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} - } - return nil -} func rootReadlink(r *Root, name string) (string, error) { if err := checkPathEscapesLstat(r, name); err != nil { Index: src/os/root_openat.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/root_openat.go b/src/os/root_openat.go --- a/src/os/root_openat.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) +++ b/src/os/root_openat.go (revision bed309eff415bcb3c77dd4bc3277b682b89a388d) @@ -196,28 +196,6 @@ return nil } -func rootRemoveAll(r *Root, name string) error { - // Consistency with os.RemoveAll: Strip trailing /s from the name, - // so RemoveAll("not_a_directory/") succeeds. - for len(name) > 0 && IsPathSeparator(name[len(name)-1]) { - name = name[:len(name)-1] - } - if endsWithDot(name) { - // Consistency with os.RemoveAll: Return EINVAL when trying to remove . - return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} - } - _, err := doInRoot(r, name, nil, func(parent sysfdType, name string) (struct{}, error) { - return struct{}{}, removeAllFrom(parent, name) - }) - if IsNotExist(err) { - return nil - } - if err != nil { - return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} - } - return err -} - func rootRename(r *Root, oldname, newname string) error { _, err := doInRoot(r, oldname, nil, func(oldparent sysfdType, oldname string) (struct{}, error) { _, err := doInRoot(r, newname, nil, func(newparent sysfdType, newname string) (struct{}, error) { Index: src/os/root_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/root_windows.go b/src/os/root_windows.go --- a/src/os/root_windows.go (revision f6bddda4e8ff58a957462a1a09562924d5f3d05c) +++ b/src/os/root_windows.go (revision bed309eff415bcb3c77dd4bc3277b682b89a388d) @@ -402,3 +402,14 @@ } return fi.Mode(), nil } + +func checkPathEscapes(r *Root, name string) error { + if !filepathlite.IsLocal(name) { + return errPathEscapes + } + return nil +} + +func checkPathEscapesLstat(r *Root, name string) error { + return checkPathEscapes(r, name) +} Index: src/os/exec_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/exec_windows.go b/src/os/exec_windows.go --- a/src/os/exec_windows.go (revision bed309eff415bcb3c77dd4bc3277b682b89a388d) +++ b/src/os/exec_windows.go (revision 34b899c2fb39b092db4fa67c4417e41dc046be4b) @@ -10,6 +10,7 @@ "runtime" "syscall" "time" + _ "unsafe" ) // Note that Process.handle is never nil because Windows always requires @@ -49,9 +50,23 @@ // than statusDone. p.doRelease(statusReleased) + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + if maj < 10 { + // NOTE(brainman): It seems that sometimes process is not dead + // when WaitForSingleObject returns. But we do not know any + // other way to wait for it. Sleeping for a while seems to do + // the trick sometimes. + // See https://golang.org/issue/25965 for details. + time.Sleep(5 * time.Millisecond) + } + return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil } +//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) + func (p *Process) signal(sig Signal) error { handle, status := p.handleTransientAcquire() switch status { ================================================ FILE: core/Clash.Meta/.github/patch/go1.26.patch ================================================ Subject: [PATCH] Revert "os: remove 5ms sleep on Windows in (*Process).Wait" Fix os.RemoveAll not working on Windows7 Revert "runtime: always use LoadLibraryEx to load system libraries" Revert "syscall: remove Windows 7 console handle workaround" Revert "net: remove sysSocket fallback for Windows 7" Revert "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" --- Index: src/crypto/internal/sysrand/rand_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/internal/sysrand/rand_windows.go b/src/crypto/internal/sysrand/rand_windows.go --- a/src/crypto/internal/sysrand/rand_windows.go (revision e87b10ea2a2c6c65b80c4374af42b9c02ac9fb20) +++ b/src/crypto/internal/sysrand/rand_windows.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) @@ -7,5 +7,26 @@ import "internal/syscall/windows" func read(b []byte) error { - return windows.ProcessPrng(b) + // RtlGenRandom only returns 1<<32-1 bytes at a time. We only read at + // most 1<<31-1 bytes at a time so that this works the same on 32-bit + // and 64-bit systems. + return batched(windows.RtlGenRandom, 1<<31-1)(b) +} + +// batched returns a function that calls f to populate a []byte by chunking it +// into subslices of, at most, readMax bytes. +func batched(f func([]byte) error, readMax int) func([]byte) error { + return func(out []byte) error { + for len(out) > 0 { + read := len(out) + if read > readMax { + read = readMax + } + if err := f(out[:read]); err != nil { + return err + } + out = out[read:] + } + return nil + } } Index: src/crypto/rand/rand.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/crypto/rand/rand.go b/src/crypto/rand/rand.go --- a/src/crypto/rand/rand.go (revision e87b10ea2a2c6c65b80c4374af42b9c02ac9fb20) +++ b/src/crypto/rand/rand.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) @@ -25,7 +25,7 @@ // - On legacy Linux (< 3.17), Reader opens /dev/urandom on first use. // - On macOS, iOS, and OpenBSD Reader, uses arc4random_buf(3). // - On NetBSD, Reader uses the kern.arandom sysctl. -// - On Windows, Reader uses the ProcessPrng API. +// - On Windows systems, Reader uses the RtlGenRandom API. // - On js/wasm, Reader uses the Web Crypto API. // - On wasip1/wasm, Reader uses random_get. // Index: src/internal/syscall/windows/syscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go --- a/src/internal/syscall/windows/syscall_windows.go (revision e87b10ea2a2c6c65b80c4374af42b9c02ac9fb20) +++ b/src/internal/syscall/windows/syscall_windows.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) @@ -421,7 +421,7 @@ //sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock //sys CreateEvent(eventAttrs *SecurityAttributes, manualReset uint32, initialState uint32, name *uint16) (handle syscall.Handle, err error) = kernel32.CreateEventW -//sys ProcessPrng(buf []byte) (err error) = bcryptprimitives.ProcessPrng +//sys RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036 type FILE_ID_BOTH_DIR_INFO struct { NextEntryOffset uint32 Index: src/internal/syscall/windows/zsyscall_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go --- a/src/internal/syscall/windows/zsyscall_windows.go (revision e87b10ea2a2c6c65b80c4374af42b9c02ac9fb20) +++ b/src/internal/syscall/windows/zsyscall_windows.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) @@ -38,7 +38,6 @@ var ( modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll")) - modbcryptprimitives = syscall.NewLazyDLL(sysdll.Add("bcryptprimitives.dll")) modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll")) modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll")) modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll")) @@ -65,7 +64,7 @@ procSetEntriesInAclW = modadvapi32.NewProc("SetEntriesInAclW") procSetNamedSecurityInfoW = modadvapi32.NewProc("SetNamedSecurityInfoW") procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation") - procProcessPrng = modbcryptprimitives.NewProc("ProcessPrng") + procSystemFunction036 = modadvapi32.NewProc("SystemFunction036") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procCreateEventW = modkernel32.NewProc("CreateEventW") procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort") @@ -271,12 +270,12 @@ return } -func ProcessPrng(buf []byte) (err error) { +func RtlGenRandom(buf []byte) (err error) { var _p0 *byte if len(buf) > 0 { _p0 = &buf[0] } - r1, _, e1 := syscall.SyscallN(procProcessPrng.Addr(), uintptr(unsafe.Pointer(_p0)), uintptr(len(buf))) + r1, _, e1 := syscall.SyscallN(procSystemFunction036.Addr(), uintptr(unsafe.Pointer(_p0)), uintptr(len(buf))) if r1 == 0 { err = errnoErr(e1) } Index: src/runtime/os_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go --- a/src/runtime/os_windows.go (revision e87b10ea2a2c6c65b80c4374af42b9c02ac9fb20) +++ b/src/runtime/os_windows.go (revision ce2e1a3d2c3c0d7277b4102841db1697147d2923) @@ -40,7 +40,8 @@ //go:cgo_import_dynamic runtime._GetSystemInfo GetSystemInfo%1 "kernel32.dll" //go:cgo_import_dynamic runtime._GetThreadContext GetThreadContext%2 "kernel32.dll" //go:cgo_import_dynamic runtime._SetThreadContext SetThreadContext%2 "kernel32.dll" -//go:cgo_import_dynamic runtime._LoadLibraryExW LoadLibraryExW%3 "kernel32.dll" +//go:cgo_import_dynamic runtime._LoadLibraryW LoadLibraryW%1 "kernel32.dll" +//go:cgo_import_dynamic runtime._LoadLibraryA LoadLibraryA%1 "kernel32.dll" //go:cgo_import_dynamic runtime._PostQueuedCompletionStatus PostQueuedCompletionStatus%4 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceCounter QueryPerformanceCounter%1 "kernel32.dll" //go:cgo_import_dynamic runtime._QueryPerformanceFrequency QueryPerformanceFrequency%1 "kernel32.dll" @@ -74,7 +75,6 @@ // Following syscalls are available on every Windows PC. // All these variables are set by the Windows executable // loader before the Go program starts. - _AddVectoredContinueHandler, _AddVectoredExceptionHandler, _CloseHandle, _CreateEventA, @@ -97,7 +97,8 @@ _GetSystemInfo, _GetThreadContext, _SetThreadContext, - _LoadLibraryExW, + _LoadLibraryW, + _LoadLibraryA, _PostQueuedCompletionStatus, _QueryPerformanceCounter, _QueryPerformanceFrequency, @@ -126,8 +127,23 @@ _WriteFile, _ stdFunction - // Use ProcessPrng to generate cryptographically random data. - _ProcessPrng stdFunction + // Following syscalls are only available on some Windows PCs. + // We will load syscalls, if available, before using them. + _AddDllDirectory, + _AddVectoredContinueHandler, + _LoadLibraryExA, + _LoadLibraryExW, + _ stdFunction + + // Use RtlGenRandom to generate cryptographically random data. + // This approach has been recommended by Microsoft (see issue + // 15589 for details). + // The RtlGenRandom is not listed in advapi32.dll, instead + // RtlGenRandom function can be found by searching for SystemFunction036. + // Also some versions of Mingw cannot link to SystemFunction036 + // when building executable as Cgo. So load SystemFunction036 + // manually during runtime startup. + _RtlGenRandom stdFunction // Load ntdll.dll manually during startup, otherwise Mingw // links wrong printf function to cgo executable (see issue @@ -144,13 +160,6 @@ _ stdFunction ) -var ( - bcryptprimitivesdll = [...]uint16{'b', 'c', 'r', 'y', 'p', 't', 'p', 'r', 'i', 'm', 'i', 't', 'i', 'v', 'e', 's', '.', 'd', 'l', 'l', 0} - ntdlldll = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0} - powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0} - winmmdll = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0} -) - // Function to be called by windows CreateThread // to start new os thread. func tstart_stdcall(newm *m) @@ -242,9 +251,40 @@ return unsafe.String(&sysDirectory[0], sysDirectoryLen) } -func windowsLoadSystemLib(name []uint16) uintptr { - const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 - return stdcall(_LoadLibraryExW, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) +//go:linkname syscall_getSystemDirectory syscall.getSystemDirectory +func syscall_getSystemDirectory() string { + return unsafe.String(&sysDirectory[0], sysDirectoryLen) +} + +func windowsLoadSystemLib(name []byte) uintptr { + if useLoadLibraryEx { + return stdcall(_LoadLibraryExA, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + absName := append(sysDirectory[:sysDirectoryLen], name...) + return stdcall(_LoadLibraryA, uintptr(unsafe.Pointer(&absName[0]))) + } +} + +const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 + +// When available, this function will use LoadLibraryEx with the filename +// parameter and the important SEARCH_SYSTEM32 argument. But on systems that +// do not have that option, absoluteFilepath should contain a fallback +// to the full path inside of system32 for use with vanilla LoadLibrary. +// +//go:linkname syscall_loadsystemlibrary syscall.loadsystemlibrary +func syscall_loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle, err uintptr) { + if useLoadLibraryEx { + handle, _, err = syscall_syscalln(uintptr(unsafe.Pointer(_LoadLibraryExW)), 3, uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + } else { + handle, _, err = syscall_syscalln(uintptr(unsafe.Pointer(_LoadLibraryW)), 1, uintptr(unsafe.Pointer(absoluteFilepath))) + } + KeepAlive(filename) + KeepAlive(absoluteFilepath) + if handle != 0 { + err = 0 + } + return } //go:linkname windows_QueryPerformanceCounter internal/syscall/windows.QueryPerformanceCounter @@ -262,13 +302,28 @@ } func loadOptionalSyscalls() { - bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:]) - if bcryptPrimitives == 0 { - throw("bcryptprimitives.dll not found") + var kernel32dll = []byte("kernel32.dll\000") + k32 := stdcall(_LoadLibraryA, uintptr(unsafe.Pointer(&kernel32dll[0]))) + if k32 == 0 { + throw("kernel32.dll not found") } - _ProcessPrng = windowsFindfunc(bcryptPrimitives, []byte("ProcessPrng\000")) + _AddDllDirectory = windowsFindfunc(k32, []byte("AddDllDirectory\000")) + _AddVectoredContinueHandler = windowsFindfunc(k32, []byte("AddVectoredContinueHandler\000")) + _LoadLibraryExA = windowsFindfunc(k32, []byte("LoadLibraryExA\000")) + _LoadLibraryExW = windowsFindfunc(k32, []byte("LoadLibraryExW\000")) + useLoadLibraryEx = (_LoadLibraryExW != nil && _LoadLibraryExA != nil && _AddDllDirectory != nil) + + initSysDirectory() - n32 := windowsLoadSystemLib(ntdlldll[:]) + var advapi32dll = []byte("advapi32.dll\000") + a32 := windowsLoadSystemLib(advapi32dll) + if a32 == 0 { + throw("advapi32.dll not found") + } + _RtlGenRandom = windowsFindfunc(a32, []byte("SystemFunction036\000")) + + var ntdll = []byte("ntdll.dll\000") + n32 := windowsLoadSystemLib(ntdll) if n32 == 0 { throw("ntdll.dll not found") } @@ -297,7 +352,7 @@ context uintptr } - powrprof := windowsLoadSystemLib(powrprofdll[:]) + powrprof := windowsLoadSystemLib([]byte("powrprof.dll\000")) if powrprof == 0 { return // Running on Windows 7, where we don't need it anyway. } @@ -351,6 +406,22 @@ // in sys_windows_386.s and sys_windows_amd64.s: func getlasterror() uint32 +// When loading DLLs, we prefer to use LoadLibraryEx with +// LOAD_LIBRARY_SEARCH_* flags, if available. LoadLibraryEx is not +// available on old Windows, though, and the LOAD_LIBRARY_SEARCH_* +// flags are not available on some versions of Windows without a +// security patch. +// +// https://msdn.microsoft.com/en-us/library/ms684179(v=vs.85).aspx says: +// "Windows 7, Windows Server 2008 R2, Windows Vista, and Windows +// Server 2008: The LOAD_LIBRARY_SEARCH_* flags are available on +// systems that have KB2533623 installed. To determine whether the +// flags are available, use GetProcAddress to get the address of the +// AddDllDirectory, RemoveDllDirectory, or SetDefaultDllDirectories +// function. If GetProcAddress succeeds, the LOAD_LIBRARY_SEARCH_* +// flags can be used with LoadLibraryEx." +var useLoadLibraryEx bool + var timeBeginPeriodRetValue uint32 // osRelaxMinNS indicates that sysmon shouldn't osRelax if the next @@ -417,7 +488,8 @@ // Only load winmm.dll if we need it. // This avoids a dependency on winmm.dll for Go programs // that run on new Windows versions. - m32 := windowsLoadSystemLib(winmmdll[:]) + var winmmdll = []byte("winmm.dll\000") + m32 := windowsLoadSystemLib(winmmdll) if m32 == 0 { print("runtime: LoadLibraryExW failed; errno=", getlasterror(), "\n") throw("winmm.dll not found") @@ -458,6 +530,28 @@ canUseLongPaths = true } +var osVersionInfo struct { + majorVersion uint32 + minorVersion uint32 + buildNumber uint32 +} + +func initOsVersionInfo() { + info := windows.OSVERSIONINFOW{} + info.OSVersionInfoSize = uint32(unsafe.Sizeof(info)) + stdcall(_RtlGetVersion, uintptr(unsafe.Pointer(&info))) + osVersionInfo.majorVersion = info.MajorVersion + osVersionInfo.minorVersion = info.MinorVersion + osVersionInfo.buildNumber = info.BuildNumber +} + +//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) { + *majorVersion = osVersionInfo.majorVersion + *minorVersion = osVersionInfo.minorVersion + *buildNumber = osVersionInfo.buildNumber +} + func osinit() { asmstdcallAddr = unsafe.Pointer(windows.AsmStdCallAddr()) @@ -470,8 +564,8 @@ initHighResTimer() timeBeginPeriodRetValue = osRelax(false) - initSysDirectory() initLongPathSupport() + initOsVersionInfo() numCPUStartup = getCPUCount() @@ -487,7 +581,7 @@ //go:nosplit func readRandom(r []byte) int { n := 0 - if stdcall(_ProcessPrng, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { + if stdcall(_RtlGenRandom, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 { n = len(r) } return n Index: src/net/hook_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/hook_windows.go b/src/net/hook_windows.go --- a/src/net/hook_windows.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) +++ b/src/net/hook_windows.go (revision 2263b05b2fa6ce228fde1899587baf109f1e2e0a) @@ -13,6 +13,7 @@ hostsFilePath = windows.GetSystemDirectory() + "/Drivers/etc/hosts" // Placeholders for socket system calls. + socketFunc func(int, int, int) (syscall.Handle, error) = syscall.Socket wsaSocketFunc func(int32, int32, int32, *syscall.WSAProtocolInfo, uint32, uint32) (syscall.Handle, error) = windows.WSASocket connectFunc func(syscall.Handle, syscall.Sockaddr) error = syscall.Connect listenFunc func(syscall.Handle, int) error = syscall.Listen Index: src/net/internal/socktest/main_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_test.go b/src/net/internal/socktest/main_test.go --- a/src/net/internal/socktest/main_test.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) +++ b/src/net/internal/socktest/main_test.go (revision 2263b05b2fa6ce228fde1899587baf109f1e2e0a) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !js && !plan9 && !wasip1 && !windows +//go:build !js && !plan9 && !wasip1 package socktest_test Index: src/net/internal/socktest/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/main_windows_test.go b/src/net/internal/socktest/main_windows_test.go new file mode 100644 --- /dev/null (revision 2263b05b2fa6ce228fde1899587baf109f1e2e0a) +++ b/src/net/internal/socktest/main_windows_test.go (revision 2263b05b2fa6ce228fde1899587baf109f1e2e0a) @@ -0,0 +1,22 @@ +// 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 socktest_test + +import "syscall" + +var ( + socketFunc func(int, int, int) (syscall.Handle, error) + closeFunc func(syscall.Handle) error +) + +func installTestHooks() { + socketFunc = sw.Socket + closeFunc = sw.Closesocket +} + +func uninstallTestHooks() { + socketFunc = syscall.Socket + closeFunc = syscall.Closesocket +} Index: src/net/internal/socktest/sys_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/internal/socktest/sys_windows.go b/src/net/internal/socktest/sys_windows.go --- a/src/net/internal/socktest/sys_windows.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) +++ b/src/net/internal/socktest/sys_windows.go (revision 2263b05b2fa6ce228fde1899587baf109f1e2e0a) @@ -9,6 +9,38 @@ "syscall" ) +// Socket wraps [syscall.Socket]. +func (sw *Switch) Socket(family, sotype, proto int) (s syscall.Handle, err error) { + sw.once.Do(sw.init) + + so := &Status{Cookie: cookie(family, sotype, proto)} + sw.fmu.RLock() + f, _ := sw.fltab[FilterSocket] + sw.fmu.RUnlock() + + af, err := f.apply(so) + if err != nil { + return syscall.InvalidHandle, err + } + s, so.Err = syscall.Socket(family, sotype, proto) + if err = af.apply(so); err != nil { + if so.Err == nil { + syscall.Closesocket(s) + } + return syscall.InvalidHandle, err + } + + sw.smu.Lock() + defer sw.smu.Unlock() + if so.Err != nil { + sw.stats.getLocked(so.Cookie).OpenFailed++ + return syscall.InvalidHandle, so.Err + } + nso := sw.addLocked(s, family, sotype, proto) + sw.stats.getLocked(nso.Cookie).Opened++ + return s, nil +} + // WSASocket wraps [syscall.WSASocket]. func (sw *Switch) WSASocket(family, sotype, proto int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (s syscall.Handle, err error) { sw.once.Do(sw.init) Index: src/net/main_windows_test.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/main_windows_test.go b/src/net/main_windows_test.go --- a/src/net/main_windows_test.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) +++ b/src/net/main_windows_test.go (revision 2263b05b2fa6ce228fde1899587baf109f1e2e0a) @@ -12,6 +12,7 @@ var ( // Placeholders for saving original socket system calls. + origSocket = socketFunc origWSASocket = wsaSocketFunc origClosesocket = poll.CloseFunc origConnect = connectFunc @@ -21,6 +22,7 @@ ) func installTestHooks() { + socketFunc = sw.Socket wsaSocketFunc = sw.WSASocket poll.CloseFunc = sw.Closesocket connectFunc = sw.Connect @@ -30,6 +32,7 @@ } func uninstallTestHooks() { + socketFunc = origSocket wsaSocketFunc = origWSASocket poll.CloseFunc = origClosesocket connectFunc = origConnect Index: src/net/sock_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/net/sock_windows.go b/src/net/sock_windows.go --- a/src/net/sock_windows.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) +++ b/src/net/sock_windows.go (revision 2263b05b2fa6ce228fde1899587baf109f1e2e0a) @@ -20,6 +20,21 @@ func sysSocket(family, sotype, proto int) (syscall.Handle, error) { s, err := wsaSocketFunc(int32(family), int32(sotype), int32(proto), nil, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT) + if err == nil { + return s, nil + } + // WSA_FLAG_NO_HANDLE_INHERIT flag is not supported on some + // old versions of Windows, see + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms742212(v=vs.85).aspx + // for details. Just use syscall.Socket, if windows.WSASocket failed. + + // See ../syscall/exec_unix.go for description of ForkLock. + syscall.ForkLock.RLock() + s, err = socketFunc(family, sotype, proto) + if err == nil { + syscall.CloseOnExec(s) + } + syscall.ForkLock.RUnlock() if err != nil { return syscall.InvalidHandle, os.NewSyscallError("socket", err) } Index: src/syscall/exec_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go --- a/src/syscall/exec_windows.go (revision 4b29590aa510e05686ea53de16e1e571d22203d8) +++ b/src/syscall/exec_windows.go (revision ae41f7abdd5d7b8b51db2c03bf819ac66b8e1eb1) @@ -15,7 +15,6 @@ "unsafe" ) -// ForkLock is not used on Windows. var ForkLock sync.RWMutex // EscapeArg rewrites command line argument s as prescribed @@ -304,6 +303,9 @@ var zeroProcAttr ProcAttr var zeroSysProcAttr SysProcAttr +//go:linkname rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) + func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) { if len(argv0) == 0 { return 0, 0, EWINDOWS @@ -367,6 +369,17 @@ } } + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + isWin7 := maj < 6 || (maj == 6 && min <= 1) + // NT kernel handles are divisible by 4, with the bottom 3 bits left as + // a tag. The fully set tag correlates with the types of handles we're + // concerned about here. Except, the kernel will interpret some + // special handle values, like -1, -2, and so forth, so kernelbase.dll + // checks to see that those bottom three bits are checked, but that top + // bit is not checked. + isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 } + p, _ := GetCurrentProcess() parentProcess := p if sys.ParentProcess != 0 { @@ -375,7 +388,15 @@ fd := make([]Handle, len(attr.Files)) for i := range attr.Files { if attr.Files[i] > 0 { - err := DuplicateHandle(p, Handle(attr.Files[i]), parentProcess, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) + destinationProcessHandle := parentProcess + + // On Windows 7, console handles aren't real handles, and can only be duplicated + // into the current process, not a parent one, which amounts to the same thing. + if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) { + destinationProcessHandle = p + } + + err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) if err != nil { return 0, 0, err } @@ -406,6 +427,14 @@ fd = append(fd, sys.AdditionalInheritedHandles...) + // On Windows 7, console handles aren't real handles, so don't pass them + // through to PROC_THREAD_ATTRIBUTE_HANDLE_LIST. + for i := range fd { + if isLegacyWin7ConsoleHandle(fd[i]) { + fd[i] = 0 + } + } + // The presence of a NULL handle in the list is enough to cause PROC_THREAD_ATTRIBUTE_HANDLE_LIST // to treat the entire list as empty, so remove NULL handles. j := 0 Index: src/syscall/dll_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/syscall/dll_windows.go b/src/syscall/dll_windows.go --- a/src/syscall/dll_windows.go (revision ae41f7abdd5d7b8b51db2c03bf819ac66b8e1eb1) +++ b/src/syscall/dll_windows.go (revision ce2e1a3d2c3c0d7277b4102841db1697147d2923) @@ -119,14 +119,7 @@ } //go:linkname loadsystemlibrary -func loadsystemlibrary(filename *uint16) (uintptr, Errno) { - const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 - handle, _, err := SyscallN(uintptr(__LoadLibraryExW), uintptr(unsafe.Pointer(filename)), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) - if handle != 0 { - err = 0 - } - return handle, err -} +func loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle uintptr, err Errno) //go:linkname getprocaddress func getprocaddress(handle uintptr, procname *uint8) (uintptr, Errno) { @@ -143,6 +136,9 @@ Handle Handle } +//go:linkname getSystemDirectory +func getSystemDirectory() string // Implemented in runtime package. + // LoadDLL loads the named DLL file into memory. // // If name is not an absolute path and is not a known system DLL used by @@ -159,7 +155,11 @@ var h uintptr var e Errno if sysdll.IsSystemDLL[name] { - h, e = loadsystemlibrary(namep) + absoluteFilepathp, err := UTF16PtrFromString(getSystemDirectory() + name) + if err != nil { + return nil, err + } + h, e = loadsystemlibrary(namep, absoluteFilepathp) } else { h, e = loadlibrary(namep) } Index: src/os/removeall_at.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/removeall_at.go b/src/os/removeall_at.go --- a/src/os/removeall_at.go (revision ce2e1a3d2c3c0d7277b4102841db1697147d2923) +++ b/src/os/removeall_at.go (revision 4ea1045cf3124221f055dbd2f3d2c9822934f661) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build unix || wasip1 || windows +//go:build unix || wasip1 package os @@ -175,3 +175,25 @@ } return newDirFile(fd, name) } + +func rootRemoveAll(r *Root, name string) error { + // Consistency with os.RemoveAll: Strip trailing /s from the name, + // so RemoveAll("not_a_directory/") succeeds. + for len(name) > 0 && IsPathSeparator(name[len(name)-1]) { + name = name[:len(name)-1] + } + if endsWithDot(name) { + // Consistency with os.RemoveAll: Return EINVAL when trying to remove . + return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} + } + _, err := doInRoot(r, name, nil, func(parent sysfdType, name string) (struct{}, error) { + return struct{}{}, removeAllFrom(parent, name) + }) + if IsNotExist(err) { + return nil + } + if err != nil { + return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} + } + return err +} Index: src/os/removeall_noat.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/removeall_noat.go b/src/os/removeall_noat.go --- a/src/os/removeall_noat.go (revision ce2e1a3d2c3c0d7277b4102841db1697147d2923) +++ b/src/os/removeall_noat.go (revision 4ea1045cf3124221f055dbd2f3d2c9822934f661) @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (js && wasm) || plan9 +//go:build (js && wasm) || plan9 || windows package os @@ -140,3 +140,22 @@ } return err } + +func rootRemoveAll(r *Root, name string) error { + if endsWithDot(name) { + // Consistency with os.RemoveAll: Return EINVAL when trying to remove . + return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} + } + if err := checkPathEscapesLstat(r, name); err != nil { + if err == syscall.ENOTDIR { + // Some intermediate path component is not a directory. + // RemoveAll treats this as success (since the target doesn't exist). + return nil + } + return &PathError{Op: "RemoveAll", Path: name, Err: err} + } + if err := RemoveAll(joinPath(r.root.name, name)); err != nil { + return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} + } + return nil +} Index: src/os/root_noopenat.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/root_noopenat.go b/src/os/root_noopenat.go --- a/src/os/root_noopenat.go (revision ce2e1a3d2c3c0d7277b4102841db1697147d2923) +++ b/src/os/root_noopenat.go (revision 4ea1045cf3124221f055dbd2f3d2c9822934f661) @@ -11,7 +11,6 @@ "internal/filepathlite" "internal/stringslite" "sync/atomic" - "syscall" "time" ) @@ -185,25 +184,6 @@ } return nil } - -func rootRemoveAll(r *Root, name string) error { - if endsWithDot(name) { - // Consistency with os.RemoveAll: Return EINVAL when trying to remove . - return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} - } - if err := checkPathEscapesLstat(r, name); err != nil { - if err == syscall.ENOTDIR { - // Some intermediate path component is not a directory. - // RemoveAll treats this as success (since the target doesn't exist). - return nil - } - return &PathError{Op: "RemoveAll", Path: name, Err: err} - } - if err := RemoveAll(joinPath(r.root.name, name)); err != nil { - return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} - } - return nil -} func rootReadlink(r *Root, name string) (string, error) { if err := checkPathEscapesLstat(r, name); err != nil { Index: src/os/root_openat.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/root_openat.go b/src/os/root_openat.go --- a/src/os/root_openat.go (revision ce2e1a3d2c3c0d7277b4102841db1697147d2923) +++ b/src/os/root_openat.go (revision 4ea1045cf3124221f055dbd2f3d2c9822934f661) @@ -196,28 +196,6 @@ return nil } -func rootRemoveAll(r *Root, name string) error { - // Consistency with os.RemoveAll: Strip trailing /s from the name, - // so RemoveAll("not_a_directory/") succeeds. - for len(name) > 0 && IsPathSeparator(name[len(name)-1]) { - name = name[:len(name)-1] - } - if endsWithDot(name) { - // Consistency with os.RemoveAll: Return EINVAL when trying to remove . - return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} - } - _, err := doInRoot(r, name, nil, func(parent sysfdType, name string) (struct{}, error) { - return struct{}{}, removeAllFrom(parent, name) - }) - if IsNotExist(err) { - return nil - } - if err != nil { - return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} - } - return err -} - func rootRename(r *Root, oldname, newname string) error { _, err := doInRoot(r, oldname, nil, func(oldparent sysfdType, oldname string) (struct{}, error) { _, err := doInRoot(r, newname, nil, func(newparent sysfdType, newname string) (struct{}, error) { Index: src/os/root_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/root_windows.go b/src/os/root_windows.go --- a/src/os/root_windows.go (revision ce2e1a3d2c3c0d7277b4102841db1697147d2923) +++ b/src/os/root_windows.go (revision 4ea1045cf3124221f055dbd2f3d2c9822934f661) @@ -402,3 +402,14 @@ } return fi.Mode(), nil } + +func checkPathEscapes(r *Root, name string) error { + if !filepathlite.IsLocal(name) { + return errPathEscapes + } + return nil +} + +func checkPathEscapesLstat(r *Root, name string) error { + return checkPathEscapes(r, name) +} Index: src/os/exec_windows.go IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/os/exec_windows.go b/src/os/exec_windows.go --- a/src/os/exec_windows.go (revision 4ea1045cf3124221f055dbd2f3d2c9822934f661) +++ b/src/os/exec_windows.go (revision 8149d992682ce76c6af804b507878e19fc966f7b) @@ -10,6 +10,7 @@ "runtime" "syscall" "time" + _ "unsafe" ) // Note that Process.handle is never nil because Windows always requires @@ -49,9 +50,23 @@ // than statusDone. p.doRelease(statusReleased) + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + if maj < 10 { + // NOTE(brainman): It seems that sometimes process is not dead + // when WaitForSingleObject returns. But we do not know any + // other way to wait for it. Sleeping for a while seems to do + // the trick sometimes. + // See https://golang.org/issue/25965 for details. + time.Sleep(5 * time.Millisecond) + } + return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil } +//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers +func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) + func (p *Process) signal(sig Signal) error { handle, status := p.handleTransientAcquire() switch status { ================================================ FILE: core/Clash.Meta/.github/patch/issue77731.patch ================================================ From b8f897a9da7a82ad8584a22284ceac61262fcb7e Mon Sep 17 00:00:00 2001 From: Jorropo Date: Sun, 22 Feb 2026 01:47:45 +0100 Subject: [PATCH] runtime: fix value of ENOSYS on mips from 38 to 89 Fixes #77731 Change-Id: Iaca444e2d5f9e19fd2de38414b357b41471a668c --- diff --git a/src/runtime/defs_linux_mips64x.go b/src/runtime/defs_linux_mips64x.go index 7449d2c..4d0f103 100644 --- a/src/runtime/defs_linux_mips64x.go +++ b/src/runtime/defs_linux_mips64x.go @@ -12,7 +12,7 @@ _EINTR = 0x4 _EAGAIN = 0xb _ENOMEM = 0xc - _ENOSYS = 0x26 + _ENOSYS = 0x59 _PROT_NONE = 0x0 _PROT_READ = 0x1 diff --git a/src/runtime/defs_linux_mipsx.go b/src/runtime/defs_linux_mipsx.go index 5a446e0..b8da4d0 100644 --- a/src/runtime/defs_linux_mipsx.go +++ b/src/runtime/defs_linux_mipsx.go @@ -12,7 +12,7 @@ _EINTR = 0x4 _EAGAIN = 0xb _ENOMEM = 0xc - _ENOSYS = 0x26 + _ENOSYS = 0x59 _PROT_NONE = 0x0 _PROT_READ = 0x1 ================================================ FILE: core/Clash.Meta/.github/patch/issue77930.patch ================================================ From f4de14a515221e27c0d79446b423849a6546e3a6 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 24 Mar 2026 23:02:09 +0000 Subject: [PATCH] runtime: use uname version check for 64-bit time on 32-bit arch codepaths The previous fallback-on-ENOSYS logic causes issues on forks of Linux. Android: #77621 (CL 750040 added a workaround with a TODO, this fixes that TODO) Causes the OS to terminate the program when running on Android versions <=10 since the seccomp jail does not know about the 64-bit time syscall and is configured to terminate the program on any unknown syscall. Synology's Linux: #77930 On old versions of Synology's Linux they added custom vendor syscalls without adding a gap in the syscall numbers, that means when we call the newer Linux syscall which was added later, Synology's Linux interprets it as a completely different vendor syscall. Originally by Jorropo in CL 751340. Fixes golang/go#77930 Updates tailscale/go#162 Originally https://go-review.googlesource.com/c/go/+/758902/2 Co-authored-by: Jorropo Change-Id: I90e15495d9249fd7f6e112f9e3ae8ad1322f56e0 --- .../runtime/syscall/linux/defs_linux_386.go | 1 + .../runtime/syscall/linux/defs_linux_amd64.go | 1 + .../runtime/syscall/linux/defs_linux_arm.go | 1 + .../runtime/syscall/linux/defs_linux_arm64.go | 1 + .../syscall/linux/defs_linux_loong64.go | 1 + .../syscall/linux/defs_linux_mips64x.go | 1 + .../runtime/syscall/linux/defs_linux_mipsx.go | 1 + .../syscall/linux/defs_linux_ppc64x.go | 1 + .../syscall/linux/defs_linux_riscv64.go | 1 + .../runtime/syscall/linux/defs_linux_s390x.go | 1 + .../runtime/syscall/linux/syscall_linux.go | 14 +++++ src/runtime/os_linux.go | 62 +++++++++++++++++++ src/runtime/os_linux32.go | 28 +++------ src/runtime/os_linux64.go | 2 + 14 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/internal/runtime/syscall/linux/defs_linux_386.go b/src/internal/runtime/syscall/linux/defs_linux_386.go index 7fdf5d3f8062fa..4e8e645dc49a66 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_386.go +++ b/src/internal/runtime/syscall/linux/defs_linux_386.go @@ -17,6 +17,7 @@ const ( SYS_OPENAT = 295 SYS_PREAD64 = 180 SYS_READ = 3 + SYS_UNAME = 122 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/defs_linux_amd64.go b/src/internal/runtime/syscall/linux/defs_linux_amd64.go index 2c8676e6e9b4d9..fa764d9ccd9b8e 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_amd64.go +++ b/src/internal/runtime/syscall/linux/defs_linux_amd64.go @@ -17,6 +17,7 @@ const ( SYS_OPENAT = 257 SYS_PREAD64 = 17 SYS_READ = 0 + SYS_UNAME = 63 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm.go b/src/internal/runtime/syscall/linux/defs_linux_arm.go index a0b395d6762734..cef556d5f6f986 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_arm.go +++ b/src/internal/runtime/syscall/linux/defs_linux_arm.go @@ -17,6 +17,7 @@ const ( SYS_OPENAT = 322 SYS_PREAD64 = 180 SYS_READ = 3 + SYS_UNAME = 122 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm64.go b/src/internal/runtime/syscall/linux/defs_linux_arm64.go index 223dce0c5b4281..eabddbac1bc063 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_arm64.go +++ b/src/internal/runtime/syscall/linux/defs_linux_arm64.go @@ -17,6 +17,7 @@ const ( SYS_OPENAT = 56 SYS_PREAD64 = 67 SYS_READ = 63 + SYS_UNAME = 160 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/defs_linux_loong64.go b/src/internal/runtime/syscall/linux/defs_linux_loong64.go index 8aa61c391dcdcb..08e5d49b83c9bd 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_loong64.go +++ b/src/internal/runtime/syscall/linux/defs_linux_loong64.go @@ -17,6 +17,7 @@ const ( SYS_OPENAT = 56 SYS_PREAD64 = 67 SYS_READ = 63 + SYS_UNAME = 160 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go index 84b760dc1b5545..b5794e5002af5e 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go +++ b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go @@ -19,6 +19,7 @@ const ( SYS_OPENAT = 5247 SYS_PREAD64 = 5016 SYS_READ = 5000 + SYS_UNAME = 5061 EFD_NONBLOCK = 0x80 diff --git a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go index a9be21414c26f9..1fb4d919d1a318 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go +++ b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go @@ -19,6 +19,7 @@ const ( SYS_OPENAT = 4288 SYS_PREAD64 = 4200 SYS_READ = 4003 + SYS_UNAME = 4122 EFD_NONBLOCK = 0x80 diff --git a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go index 63f4e5d7864de4..ee93ad345b810f 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go +++ b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go @@ -19,6 +19,7 @@ const ( SYS_OPENAT = 286 SYS_PREAD64 = 179 SYS_READ = 3 + SYS_UNAME = 122 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go index 8aa61c391dcdcb..08e5d49b83c9bd 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go +++ b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go @@ -17,6 +17,7 @@ const ( SYS_OPENAT = 56 SYS_PREAD64 = 67 SYS_READ = 63 + SYS_UNAME = 160 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/defs_linux_s390x.go b/src/internal/runtime/syscall/linux/defs_linux_s390x.go index 52945db0e5b72f..da11c704081abc 100644 --- a/src/internal/runtime/syscall/linux/defs_linux_s390x.go +++ b/src/internal/runtime/syscall/linux/defs_linux_s390x.go @@ -17,6 +17,7 @@ const ( SYS_OPENAT = 288 SYS_PREAD64 = 180 SYS_READ = 3 + SYS_UNAME = 122 EFD_NONBLOCK = 0x800 diff --git a/src/internal/runtime/syscall/linux/syscall_linux.go b/src/internal/runtime/syscall/linux/syscall_linux.go index 8201e7d1907444..b64f511b03c947 100644 --- a/src/internal/runtime/syscall/linux/syscall_linux.go +++ b/src/internal/runtime/syscall/linux/syscall_linux.go @@ -86,3 +86,17 @@ func Pread(fd int, p []byte, offset int64) (n int, errno uintptr) { } return int(r1), e } + +type Utsname struct { + Sysname [65]byte + Nodename [65]byte + Release [65]byte + Version [65]byte + Machine [65]byte + Domainname [65]byte +} + +func Uname(buf *Utsname) (errno uintptr) { + _, _, e := Syscall6(SYS_UNAME, uintptr(unsafe.Pointer(buf)), 0, 0, 0, 0, 0) + return e +} diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index 7e6af22d48a764..493567b5303673 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -354,6 +354,7 @@ func osinit() { numCPUStartup = getCPUCount() physHugePageSize = getHugePageSize() vgetrandomInit() + configure64bitsTimeOn32BitsArchitectures() } var urandom_dev = []byte("/dev/urandom\x00") @@ -935,3 +936,64 @@ func mprotect(addr unsafe.Pointer, n uintptr, prot int32) (ret int32, errno int3 r, _, err := linux.Syscall6(linux.SYS_MPROTECT, uintptr(addr), n, uintptr(prot), 0, 0, 0) return int32(r), int32(err) } + +type kernelVersion struct { + major int + minor int +} + +// getKernelVersion returns major and minor kernel version numbers +// parsed from the uname release field. +func getKernelVersion() kernelVersion { + var buf linux.Utsname + if e := linux.Uname(&buf); e != 0 { + throw("uname failed") + } + + rel := gostringnocopy(&buf.Release[0]) + major, minor, _, ok := parseRelease(rel) + if !ok { + throw("failed to parse kernel version from uname") + } + return kernelVersion{major: major, minor: minor} +} + +// parseRelease parses a dot-separated version number. It follows the +// semver syntax, but allows the minor and patch versions to be +// elided. +func parseRelease(rel string) (major, minor, patch int, ok bool) { + // Strip anything after a dash or plus. + for i := 0; i < len(rel); i++ { + if rel[i] == '-' || rel[i] == '+' { + rel = rel[:i] + break + } + } + + next := func() (int, bool) { + for i := 0; i < len(rel); i++ { + if rel[i] == '.' { + ver, err := strconv.Atoi(rel[:i]) + rel = rel[i+1:] + return ver, err == nil + } + } + ver, err := strconv.Atoi(rel) + rel = "" + return ver, err == nil + } + if major, ok = next(); !ok || rel == "" { + return + } + if minor, ok = next(); !ok || rel == "" { + return + } + patch, ok = next() + return +} + +// GE checks if the running kernel version +// is greater than or equal to the provided version. +func (kv kernelVersion) GE(x, y int) bool { + return kv.major > x || (kv.major == x && kv.minor >= y) +} diff --git a/src/runtime/os_linux32.go b/src/runtime/os_linux32.go index 16de6fb350f624..02cb18f32d57d2 100644 --- a/src/runtime/os_linux32.go +++ b/src/runtime/os_linux32.go @@ -7,27 +7,25 @@ package runtime import ( - "internal/runtime/atomic" "unsafe" ) +func configure64bitsTimeOn32BitsArchitectures() { + use64bitsTimeOn32bits = getKernelVersion().GE(5, 1) +} + //go:noescape func futex_time32(addr unsafe.Pointer, op int32, val uint32, ts *timespec32, addr2 unsafe.Pointer, val3 uint32) int32 //go:noescape func futex_time64(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 -var isFutexTime32bitOnly atomic.Bool +var use64bitsTimeOn32bits bool //go:nosplit func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 { - if !isFutexTime32bitOnly.Load() { - ret := futex_time64(addr, op, val, ts, addr2, val3) - // futex_time64 is only supported on Linux 5.0+ - if ret != -_ENOSYS { - return ret - } - isFutexTime32bitOnly.Store(true) + if use64bitsTimeOn32bits { + return futex_time64(addr, op, val, ts, addr2, val3) } // Downgrade ts. var ts32 timespec32 @@ -45,17 +43,10 @@ func timer_settime32(timerid int32, flags int32, new, old *itimerspec32) int32 //go:noescape func timer_settime64(timerid int32, flags int32, new, old *itimerspec) int32 -var isSetTime32bitOnly atomic.Bool - //go:nosplit func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 { - if !isSetTime32bitOnly.Load() { - ret := timer_settime64(timerid, flags, new, old) - // timer_settime64 is only supported on Linux 5.0+ - if ret != -_ENOSYS { - return ret - } - isSetTime32bitOnly.Store(true) + if use64bitsTimeOn32bits { + return timer_settime64(timerid, flags, new, old) } var newts, oldts itimerspec32 @@ -73,6 +64,5 @@ func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 { old32 = &oldts } - // Fall back to 32-bit timer return timer_settime32(timerid, flags, new32, old32) } diff --git a/src/runtime/os_linux64.go b/src/runtime/os_linux64.go index 7b70d80fbe5a89..f9571dd7586614 100644 --- a/src/runtime/os_linux64.go +++ b/src/runtime/os_linux64.go @@ -10,6 +10,8 @@ import ( "unsafe" ) +func configure64bitsTimeOn32BitsArchitectures() {} + //go:noescape func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 ================================================ FILE: core/Clash.Meta/.github/patch/issue77975.patch ================================================ From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001 From: database64128 Date: Mon, 9 Mar 2026 16:25:16 +0800 Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on windows According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom, the memory pointed to by lpFromlen must remain available during the overlapped I/O, and therefore cannot be allocated on the stack. CL 685417 moved the rsan field out of the operation struct and placed it on stack, which violates the above requirement and causes stack corruption. Unfortunately, it is no longer possible to cleanly revert CL 685417. Instead of attempting to revert it, this CL bundles rsan together with rsa in the same sync.Pool. The new wsaRsa struct is still in the same size class, so no additional overhead is introduced by this change. Fixes #78041. Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab Reviewed-on: https://go-review.googlesource.com/c/go/+/753040 Reviewed-by: Quim Muntal Reviewed-by: Cherry Mui Commit-Queue: Cherry Mui LUCI-TryBot-Result: Go LUCI Reviewed-by: Damien Neil Reviewed-on: https://go-review.googlesource.com/c/go/+/753480 Reviewed-by: Mark Freeman --- src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go index 2ba967f990982f..26319548e3c310 100644 --- a/src/internal/poll/fd_windows.go +++ b/src/internal/poll/fd_windows.go @@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{ // newWSAMsg creates a new WSAMsg with the provided parameters. // Use [freeWSAMsg] to free it. -func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg { +func newWSAMsg(p []byte, oob []byte, flags int, rsa *wsaRsa) *windows.WSAMsg { // The returned object can't be allocated in the stack because it is accessed asynchronously // by Windows in between several system calls. If the stack frame is moved while that happens, // then Windows may access invalid memory. @@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAM } msg.Flags = uint32(flags) - if unconnected { - msg.Name = wsaRsaPool.Get().(*syscall.RawSockaddrAny) - msg.Namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{})) + if rsa != nil { + msg.Name = &rsa.name + msg.Namelen = rsa.namelen } return msg } func freeWSAMsg(msg *windows.WSAMsg) { // Clear pointers to buffers so they can be released by garbage collector. + msg.Name = nil msg.Buffers = nil msg.Control.Buf = nil - if msg.Name != nil { - wsaRsaPool.Put(msg.Name) - msg.Name = nil - } wsaMsgPool.Put(msg) } -var wsaRsaPool = sync.Pool{ +// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching. +// +// When used by WSARecvFrom, wsaRsa must be on the heap. See +// https://go.dev/issue/77975. type wsaRsa struct { + name syscall.RawSockaddrAny + namelen int32 +} + +var wsaRsaPool = sync.Pool{ New: func() any { - return new(syscall.RawSockaddrAny) + return new(wsaRsa) }, } +func newWSARsa() *wsaRsa { + rsa := wsaRsaPool.Get().(*wsaRsa) + rsa.name = syscall.RawSockaddrAny{} + rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{})) + return rsa +} + var operationPool = sync.Pool{ New: func() any { return new(operation) @@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) { fd.pin('r', &buf[0]) - rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) + rsa := newWSARsa() defer wsaRsaPool.Put(rsa) - rsai := syscall.RawSockaddrAny{} o := operationPool.Get().(*operation) defer operationPool.Put(o) o.prepared = false - o.msg = newWSAMsg(buf, nil, 0, true) + o.msg = newWSAMsg(buf, nil, 0, rsa) n, err := fd.execIO(o, func(o *operation) error { o.qty = 0 return windows.WSARecvFrom(fd.Sysfd, &o.msg.Buffers[0], 1, &o.qty, &o.msg.Flags, - rsa, &rsai, &o.o, nil) + &rsa.name, &rsa.namelen, &o.o, nil) }) if err != nil { return n, nil, err @@ -760,7 +771,7 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) { return n, nil, nil } var sa syscall.Sockaddr - sa, _ = rsa.Sockaddr() + sa, _ = rsa.name.Sockaddr() return n, sa, nil } @@ -781,19 +792,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) fd.pin('r', &buf[0]) - rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) + rsa := newWSARsa() defer wsaRsaPool.Put(rsa) - rsai := syscall.RawSockaddrAny{} o := operationPool.Get().(*operation) defer operationPool.Put(o) o.prepared = false - o.msg = newWSAMsg(buf, nil, 0, true) + o.msg = newWSAMsg(buf, nil, 0, rsa) n, err := fd.execIO(o, func(o *operation) error { o.qty = 0 return windows.WSARecvFrom(fd.Sysfd, &o.msg.Buffers[0], 1, &o.qty, &o.msg.Flags, - rsa, &rsai, &o.o, nil) + &rsa.name, &rsa.namelen, &o.o, nil) }) if err != nil { return n, err @@ -801,7 +811,7 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) if n == 0 { return n, nil } - rawToSockaddrInet4(rsa, sa4) + rawToSockaddrInet4(&rsa.name, sa4) return n, nil } @@ -822,19 +832,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) fd.pin('r', &buf[0]) - rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) + rsa := newWSARsa() defer wsaRsaPool.Put(rsa) - rsai := syscall.RawSockaddrAny{} o := operationPool.Get().(*operation) defer operationPool.Put(o) o.prepared = false - o.msg = newWSAMsg(buf, nil, 0, true) + o.msg = newWSAMsg(buf, nil, 0, rsa) n, err := fd.execIO(o, func(o *operation) error { o.qty = 0 return windows.WSARecvFrom(fd.Sysfd, &o.msg.Buffers[0], 1, &o.qty, &o.msg.Flags, - rsa, &rsai, &o.o, nil) + &rsa.name, &rsa.namelen, &o.o, nil) }) if err != nil { return n, err @@ -842,6 +851,6 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) if n == 0 { return n, nil } - rawToSockaddrInet6(rsa, sa6) + rawToSockaddrInet6(&rsa.name, sa6) return n, nil } ================================================ FILE: core/Clash.Meta/.github/release/.fpm_systemd ================================================ -s dir --name mihomo --category net --license GPL-3.0-or-later --description "The universal proxy platform." --url "https://wiki.metacubex.one/" --maintainer "MetaCubeX " --deb-field "Bug: https://github.com/MetaCubeX/mihomo/issues" --no-deb-generate-changes --config-files /etc/mihomo/config.yaml .github/release/config.yaml=/etc/mihomo/config.yaml .github/release/mihomo.service=/usr/lib/systemd/system/mihomo.service .github/release/mihomo@.service=/usr/lib/systemd/system/mihomo@.service LICENSE=/usr/share/licenses/mihomo/LICENSE ================================================ FILE: core/Clash.Meta/.github/release/config.yaml ================================================ mixed-port: 7890 dns: enable: true ipv6: true enhanced-mode: fake-ip fake-ip-filter: - "*" - "+.lan" - "+.local" nameserver: - system rules: - MATCH,DIRECT ================================================ FILE: core/Clash.Meta/.github/release/mihomo.service ================================================ [Unit] Description=mihomo Daemon, Another Clash Kernel. Documentation=https://wiki.metacubex.one After=network.target nss-lookup.target network-online.target [Service] Type=simple CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE ExecStart=/usr/bin/mihomo -d /etc/mihomo ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=10 LimitNOFILE=infinity [Install] WantedBy=multi-user.target ================================================ FILE: core/Clash.Meta/.github/release/mihomo@.service ================================================ [Unit] Description=mihomo Daemon, Another Clash Kernel. Documentation=https://wiki.metacubex.one After=network.target nss-lookup.target network-online.target [Service] Type=simple CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE ExecStart=/usr/bin/mihomo -d /etc/mihomo ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=10 LimitNOFILE=infinity [Install] WantedBy=multi-user.target ================================================ FILE: core/Clash.Meta/.github/release.sh ================================================ #!/bin/bash FILENAMES=$(ls) for FILENAME in $FILENAMES do if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then gzip -S ".gz" $FILENAME elif [[ $FILENAME =~ ".exe" ]];then zip -m ${FILENAME%.*}.zip $FILENAME else echo "skip $FILENAME" fi done FILENAMES=$(ls) for FILENAME in $FILENAMES do if [[ $FILENAME =~ ".zip" ]];then echo "rename $FILENAME" mv $FILENAME ${FILENAME%.*}-${VERSION}.zip elif [[ $FILENAME =~ ".gz" ]];then echo "rename $FILENAME" mv $FILENAME ${FILENAME%.*}-${VERSION}.gz else echo "skip $FILENAME" fi done ================================================ FILE: core/Clash.Meta/.github/rename-cgo.sh ================================================ #!/bin/bash FILENAMES=$(ls) for FILENAME in $FILENAMES do if [[ $FILENAME =~ "darwin-10.16-arm64" ]];then echo "rename darwin-10.16-arm64 $FILENAME" mv $FILENAME mihomo-darwin-arm64-cgo elif [[ $FILENAME =~ "darwin-10.16-amd64" ]];then echo "rename darwin-10.16-amd64 $FILENAME" mv $FILENAME mihomo-darwin-amd64-cgo elif [[ $FILENAME =~ "windows-4.0-386" ]];then echo "rename windows 386 $FILENAME" mv $FILENAME mihomo-windows-386-cgo.exe elif [[ $FILENAME =~ "windows-4.0-amd64" ]];then echo "rename windows amd64 $FILENAME" mv $FILENAME mihomo-windows-amd64-cgo.exe elif [[ $FILENAME =~ "mihomo-linux-arm-5" ]];then echo "rename mihomo-linux-arm-5 $FILENAME" mv $FILENAME mihomo-linux-armv5-cgo elif [[ $FILENAME =~ "mihomo-linux-arm-6" ]];then echo "rename mihomo-linux-arm-6 $FILENAME" mv $FILENAME mihomo-linux-armv6-cgo elif [[ $FILENAME =~ "mihomo-linux-arm-7" ]];then echo "rename mihomo-linux-arm-7 $FILENAME" mv $FILENAME mihomo-linux-armv7-cgo elif [[ $FILENAME =~ "linux" ]];then echo "rename linux $FILENAME" mv $FILENAME $FILENAME-cgo elif [[ $FILENAME =~ "android" ]];then echo "rename android $FILENAME" mv $FILENAME $FILENAME-cgo else echo "skip $FILENAME" fi done ================================================ FILE: core/Clash.Meta/.github/rename-go120.sh ================================================ #!/bin/bash FILENAMES=$(ls) for FILENAME in $FILENAMES do if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then mv $FILENAME ${FILENAME}-go120 elif [[ $FILENAME =~ ".exe" ]];then mv $FILENAME ${FILENAME%.*}-go120.exe else echo "skip $FILENAME" fi done ================================================ FILE: core/Clash.Meta/.github/workflows/build.yml ================================================ name: Build on: workflow_dispatch: inputs: version: description: "Tag version to release" required: true push: paths-ignore: - "docs/**" - "README.md" - ".github/ISSUE_TEMPLATE/**" branches: - Alpha tags: - "v*" pull_request: branches: - Alpha concurrency: group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: REGISTRY: docker.io jobs: build: runs-on: ubuntu-latest strategy: matrix: jobs: - { goos: linux, goarch: '386', go386: sse2, output: '386', debian: i386, rpm: i386} - { goos: linux, goarch: '386', go386: softfloat, output: '386-softfloat' } - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible} # old style file name will be removed in next released - { goos: linux, goarch: amd64, goamd64: v3, output: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, tarball: tarball} - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1, debian: amd64, rpm: x86_64, pacman: x86_64, test: test } - { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2, debian: amd64, rpm: x86_64, pacman: x86_64} - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3, debian: amd64, rpm: x86_64, pacman: x86_64} - { goos: linux, goarch: arm64, output: arm64, debian: arm64, rpm: aarch64, pacman: aarch64} - { goos: linux, goarch: arm, goarm: '5', output: armv5 } - { goos: linux, goarch: arm, goarm: '6', output: armv6, debian: armel, rpm: armv6hl} - { goos: linux, goarch: arm, goarm: '7', output: armv7, debian: armhf, rpm: armv7hl, pacman: armv7hl} - { goos: linux, goarch: mips, gomips: hardfloat, output: mips-hardfloat } - { goos: linux, goarch: mips, gomips: softfloat, output: mips-softfloat } - { goos: linux, goarch: mipsle, gomips: hardfloat, output: mipsle-hardfloat } - { goos: linux, goarch: mipsle, gomips: softfloat, output: mipsle-softfloat } - { goos: linux, goarch: mips64, output: mips64 } - { goos: linux, goarch: mips64le, output: mips64le, debian: mips64el, rpm: mips64el } - { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1', debian: loongarch64, rpm: loongarch64, goversion: 'custom' } - { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2', debian: loong64, rpm: loong64 } - { goos: linux, goarch: riscv64, output: riscv64, debian: riscv64, rpm: riscv64 } - { goos: linux, goarch: s390x, output: s390x, debian: s390x, rpm: s390x } - { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le } # Go 1.26 with special patch can work on macOS 10.13 High Sierra # https://github.com/MetaCubeX/go/commits/release-branch.go1.26/ - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 } - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1 } - { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2 } - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3 } - { goos: darwin, goarch: arm64, output: arm64 } # Go 1.26 with special patch can work on Windows 7 # https://github.com/MetaCubeX/go/commits/release-branch.go1.26/ - { goos: windows, goarch: '386', output: '386' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released - { goos: windows, goarch: amd64, goamd64: v3, output: amd64 } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1 } - { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2 } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3 } - { goos: windows, goarch: arm64, output: arm64 } - { goos: freebsd, goarch: '386', output: '386' } - { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released - { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64 } - { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-v1 } - { goos: freebsd, goarch: amd64, goamd64: v2, output: amd64-v2 } - { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64-v3 } - { goos: freebsd, goarch: arm64, output: arm64 } - { goos: android, goarch: '386', ndk: i686-linux-android34, output: '386' } - { goos: android, goarch: amd64, ndk: x86_64-linux-android34, output: amd64 } - { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 } - { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 } # Go 1.25 with special patch can work on Windows 7 # https://github.com/MetaCubeX/go/commits/release-branch.go1.25/ - { goos: windows, goarch: '386', output: '386-go125', goversion: '1.25' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go125, goversion: '1.25' } - { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go125, goversion: '1.25' } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go125, goversion: '1.25' } # Go 1.24 with special patch can work on Windows 7 # https://github.com/MetaCubeX/go/commits/release-branch.go1.24/ - { goos: windows, goarch: '386', output: '386-go124', goversion: '1.24' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go124, goversion: '1.24' } - { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go124, goversion: '1.24' } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go124, goversion: '1.24' } # Go 1.23 with special patch can work on Windows 7 # https://github.com/MetaCubeX/go/commits/release-branch.go1.23/ - { goos: windows, goarch: '386', output: '386-go123', goversion: '1.23' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go123, goversion: '1.23' } - { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go123, goversion: '1.23' } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go123, goversion: '1.23' } # Go 1.22 with special patch can work on Windows 7 # https://github.com/MetaCubeX/go/commits/release-branch.go1.22/ - { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go122, goversion: '1.22' } - { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go122, goversion: '1.22' } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go122, goversion: '1.22' } # Go 1.21 can revert commit `9e4385` to work on Windows 7 # https://github.com/golang/go/issues/64622#issuecomment-1847475161 # (OR we can just use golang1.21.4 which unneeded any patch) - { goos: windows, goarch: '386', output: '386-go121', goversion: '1.21' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go121, goversion: '1.21' } - { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go121, goversion: '1.21' } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go121, goversion: '1.21' } # Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016. - { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20' } - { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' } # Go 1.24 is the last release that will run on macOS 11 Big Sur. Go 1.25 will require macOS 12 Monterey or later. - { goos: darwin, goarch: arm64, output: arm64-go124, goversion: '1.24' } - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go124, goversion: '1.24' } - { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go124, goversion: '1.24' } - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go124, goversion: '1.24' } # Go 1.22 is the last release that will run on macOS 10.15 Catalina. Go 1.23 will require macOS 11 Big Sur or later. - { goos: darwin, goarch: arm64, output: arm64-go122, goversion: '1.22' } - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go122, goversion: '1.22' } - { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go122, goversion: '1.22' } - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go122, goversion: '1.22' } # Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later. - { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' } - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20' } - { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' } - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' } # Go 1.23 is the last release that requires Linux kernel version 2.6.32 or later. Go 1.24 will require Linux kernel version 3.2 or later. - { goos: linux, goarch: '386', output: '386-go123', goversion: '1.23' } - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1-go123, goversion: '1.23', test: test } - { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2-go123, goversion: '1.23' } - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3-go123, goversion: '1.23' } # only for test - { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' } - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20', test: test } - { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' } - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' } steps: - uses: actions/checkout@v6 - name: Set up Go if: ${{ matrix.jobs.goversion == '' }} uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: go-download-base-url: 'https://github.com/MetaCubeX/go/releases/download/build' go-version: '1.26' - name: Set up Go if: ${{ matrix.jobs.goversion != '' && matrix.jobs.goversion != 'custom' }} uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: go-download-base-url: 'https://github.com/MetaCubeX/go/releases/download/build' go-version: ${{ matrix.jobs.goversion }} - name: Set up Go1.26 loongarch abi1 if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }} uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: go-download-base-url: 'https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0' go-version: 1.26.0 - name: Verify Go installation run: go version - name: Verify Go env run: go env # TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930 - name: Fix issue77930 for Golang1.26 if: ${{ matrix.jobs.goversion == '' }} run: | cd $(go env GOROOT) patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77930.patch - name: Set variables run: | VERSION="${GITHUB_REF_NAME,,}-$(git rev-parse --short HEAD)" VERSION="${VERSION//\//-}" PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}" if [ -n "${{ github.event.inputs.version }}" ]; then VERSION=${{ github.event.inputs.version }} PackageVersion="${VERSION#v}" fi echo "VERSION=${VERSION}" >> $GITHUB_ENV echo "PackageVersion=${PackageVersion}" >> $GITHUB_ENV echo "BUILDTIME=$(date)" >> $GITHUB_ENV echo "CGO_ENABLED=0" >> $GITHUB_ENV echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV echo "GOTOOLCHAIN=local" >> $GITHUB_ENV - name: Setup NDK if: ${{ matrix.jobs.goos == 'android' }} uses: nttld/setup-ndk@v1 id: setup-ndk with: ndk-version: r29-beta1 - name: Set NDK path if: ${{ matrix.jobs.goos == 'android' }} run: | echo "CC=${{steps.setup-ndk.outputs.ndk-path}}/toolchains/llvm/prebuilt/linux-x86_64/bin/${{matrix.jobs.ndk}}-clang" >> $GITHUB_ENV echo "CGO_ENABLED=1" >> $GITHUB_ENV echo "BUILDTAG=" >> $GITHUB_ENV - name: Test if: ${{ matrix.jobs.test == 'test' }} run: | export SKIP_CONCURRENT_TEST=1 go test ./... echo "---test with_gvisor---" go test ./... -tags "with_gvisor" -count=1 - name: Update CA run: | sudo apt-get update && sudo apt-get install ca-certificates sudo update-ca-certificates cp -f /etc/ssl/certs/ca-certificates.crt component/ca/ca-certificates.crt - name: Build core env: GOOS: ${{matrix.jobs.goos}} GOARCH: ${{matrix.jobs.goarch}} GOAMD64: ${{matrix.jobs.goamd64}} GO386: ${{matrix.jobs.go386}} GOARM: ${{matrix.jobs.goarm}} GOMIPS: ${{matrix.jobs.gomips}} run: | go env go build -v -tags "with_gvisor" -trimpath -ldflags "${BUILDTAG} -X 'github.com/metacubex/mihomo/constant.Version=${VERSION}' -X 'github.com/metacubex/mihomo/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" if [ "${{matrix.jobs.goos}}" = "windows" ]; then cp mihomo.exe mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe zip -r mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.zip mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe else cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} gzip -c mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.gz rm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} fi - name: Package DEB if: matrix.jobs.debian != '' run: | set -xeuo pipefail sudo gem install fpm cp .github/release/.fpm_systemd .fpm fpm -t deb \ -v "${PackageVersion}" \ -p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb" \ --architecture ${{ matrix.jobs.debian }} \ mihomo=/usr/bin/mihomo - name: Package RPM if: matrix.jobs.rpm != '' run: | set -xeuo pipefail sudo gem install fpm cp .github/release/.fpm_systemd .fpm fpm -t rpm \ -v "${PackageVersion}" \ -p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm" \ --architecture ${{ matrix.jobs.rpm }} \ mihomo=/usr/bin/mihomo - name: Package Pacman if: matrix.jobs.pacman != '' run: | set -xeuo pipefail sudo gem install fpm sudo apt-get update && sudo apt-get install -y libarchive-tools cp .github/release/.fpm_systemd .fpm fpm -t pacman \ -v "${PackageVersion}" \ -p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst" \ --architecture ${{ matrix.jobs.pacman }} \ mihomo=/usr/bin/mihomo - name: Pack Golang Toolchain if: ${{ matrix.jobs.goversion == '' && matrix.jobs.tarball == 'tarball' }} run: | mkdir -p $GITHUB_WORKSPACE/toolchain/go cp -r $(go env GOROOT)/* $GITHUB_WORKSPACE/toolchain/go/ cd $GITHUB_WORKSPACE/toolchain/ tar -czf $GITHUB_WORKSPACE/toolchain.tar.gz . rm -rf $GITHUB_WORKSPACE/toolchain - name: Pack GoMod Vendor if: ${{ matrix.jobs.goversion == '' && matrix.jobs.tarball == 'tarball' }} run: | go mod vendor tar -czf $GITHUB_WORKSPACE/vendor.tar.gz vendor - name: Save version run: | echo ${VERSION} > version.txt shell: bash - name: Archive production artifacts uses: actions/upload-artifact@v7 with: name: "${{ matrix.jobs.goos }}-${{ matrix.jobs.output }}" path: | mihomo*.gz mihomo*.deb mihomo*.rpm mihomo*.pkg.tar.zst mihomo*.zip toolchain.tar.gz vendor.tar.gz version.txt checksums.txt Upload-Prerelease: permissions: write-all if: ${{ github.event_name != 'workflow_dispatch' && github.ref_type == 'branch' && !startsWith(github.event_name, 'pull_request') }} needs: [build] runs-on: ubuntu-latest steps: - name: Download all workflow run artifacts uses: actions/download-artifact@v8 with: path: bin/ merge-multiple: true - name: Calculate checksums run: | cd bin/ find . -type f -not -name "checksums.*" -not -name "version.txt" | sort | xargs sha256sum > checksums.txt cat checksums.txt shell: bash - name: Delete current release assets uses: 8Mi-Tech/delete-release-assets-action@main with: github_token: ${{ secrets.GITHUB_TOKEN }} tag: Prerelease-${{ github.ref_name }} deleteOnlyFromDrafts: false - name: Set Env run: | echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV shell: bash - name: Tag Repo uses: richardsimko/update-tag@v1 with: tag_name: Prerelease-${{ github.ref_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | cat > release.txt << 'EOF' Release created at ${{ env.BUILDTIME }} Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version
[我应该下载哪个文件? / Which file should I download?](https://github.com/MetaCubeX/mihomo/wiki/FAQ) [二进制文件筛选 / Binary file selector](https://metacubex.github.io/Meta-Docs/startup/#_1) [查看文档 / Docs](https://metacubex.github.io/Meta-Docs/) EOF - name: Upload Prerelease uses: softprops/action-gh-release@v2 if: ${{ success() }} with: tag_name: Prerelease-${{ github.ref_name }} files: | bin/* prerelease: true generate_release_notes: true body_path: release.txt Upload-Release: permissions: write-all if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} needs: [build] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: ref: Meta fetch-depth: '0' fetch-tags: 'true' - name: Get tags run: | echo "CURRENTVERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV git fetch --tags echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD)" >> $GITHUB_ENV - name: Force push Alpha branch to Meta run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" git fetch origin Alpha:Alpha git push origin Alpha:Meta --force env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Tag the commit on Alpha run: | git checkout Alpha git tag ${{ github.event.inputs.version }} git push origin ${{ github.event.inputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Generate release notes run: | cp ./.github/genReleaseNote.sh ./ bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION} rm ./genReleaseNote.sh - uses: actions/download-artifact@v8 with: path: bin/ merge-multiple: true - name: Display structure of downloaded files run: ls -R working-directory: bin - name: Upload Release uses: softprops/action-gh-release@v2 if: ${{ success() }} with: tag_name: ${{ github.event.inputs.version }} files: bin/* body_path: release.md Docker: if: ${{ !startsWith(github.event_name, 'pull_request') }} permissions: write-all needs: [build] runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/download-artifact@v8 with: path: bin/ merge-multiple: true - name: Display structure of downloaded files run: ls -R working-directory: bin - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Setup Docker buildx uses: docker/setup-buildx-action@v4 with: version: latest # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata if: ${{ github.event_name != 'workflow_dispatch' }} id: meta_alpha uses: docker/metadata-action@v6 with: images: '${{ env.REGISTRY }}/${{ github.repository }}' # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} id: meta_release uses: docker/metadata-action@v6 with: images: '${{ env.REGISTRY }}/${{ github.repository }}' tags: | ${{ github.event.inputs.version }} flavor: | latest=true labels: org.opencontainers.image.version=${{ github.event.inputs.version }} - name: Show files run: | ls . ls bin/ - name: login to docker REGISTRY uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_TOKEN }} # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image if: ${{ github.event_name != 'workflow_dispatch' }} uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile push: ${{ github.event_name != 'pull_request' }} platforms: | linux/386 linux/amd64 linux/arm64 linux/arm/v7 tags: ${{ steps.meta_alpha.outputs.tags }} labels: ${{ steps.meta_alpha.outputs.labels }} - name: Build and push Docker image if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile push: ${{ github.event_name != 'pull_request' }} platforms: | linux/386 linux/amd64 linux/arm64 linux/arm/v7 tags: ${{ steps.meta_release.outputs.tags }} labels: ${{ steps.meta_release.outputs.labels }} ================================================ FILE: core/Clash.Meta/.github/workflows/test.yml ================================================ name: Test on: push: paths-ignore: - "docs/**" - "README.md" - ".github/ISSUE_TEMPLATE/**" branches: - Alpha tags: - "v*" pull_request: branches: - Alpha jobs: test: strategy: matrix: os: - 'ubuntu-latest' # amd64 linux - 'windows-latest' # amd64 windows - 'macos-latest' # arm64 macos - 'ubuntu-24.04-arm' # arm64 linux - 'windows-11-arm' # arm64 windows - 'macos-15-intel' # amd64 macos go-version: - '1.26' - '1.25' - '1.24' - '1.23' - '1.22' - '1.21' - '1.20' fail-fast: false runs-on: ${{ matrix.os }} defaults: run: shell: bash env: CGO_ENABLED: 0 GOTOOLCHAIN: local # Fix mingw trying to be smart and converting paths https://github.com/moby/moby/issues/24029#issuecomment-250412919 MSYS_NO_PATHCONV: true steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: go-download-base-url: 'https://github.com/MetaCubeX/go/releases/download/build' go-version: ${{ matrix.go-version }} - name: Verify Go installation run: go version - name: Verify Go env run: go env - name: Remove inbound test for macOS if: ${{ runner.os == 'macOS' }} run: | rm -rf listener/inbound/*_test.go - name: Test run: go test ./... -v -count=1 - name: Test with tag with_gvisor run: go test ./... -v -count=1 -tags "with_gvisor" ================================================ FILE: core/Clash.Meta/.github/workflows/trigger-cmfa-update.yml ================================================ name: Trigger CMFA Update on: workflow_dispatch: push: paths-ignore: - "docs/**" - "README.md" - ".github/ISSUE_TEMPLATE/**" branches: - Alpha tags: - "v*" jobs: # Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies trigger-CMFA-update: runs-on: ubuntu-latest steps: - uses: actions/create-github-app-token@v3 id: generate-token with: app-id: ${{ secrets.MAINTAINER_APPID }} private-key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} - name: Trigger update-dependencies run: | curl -X POST https://api.github.com/repos/MetaCubeX/ClashMetaForAndroid/dispatches \ -H "Accept: application/vnd.github.everest-preview+json" \ -H "Authorization: token ${{ steps.generate-token.outputs.token }}" \ -d '{"event_type": "core-updated"}' ================================================ FILE: core/Clash.Meta/.gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin/* # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # go mod vendor vendor # GoLand .idea/* # macOS file .DS_Store # test suite test/config/cache* /output .vscode/ .fleet/ ================================================ FILE: core/Clash.Meta/.golangci.yaml ================================================ linters: disable-all: true enable: - gofumpt - staticcheck - govet - gci linters-settings: gci: custom-order: true sections: - standard - prefix(github.com/metacubex/mihomo) - default staticcheck: go: '1.19' ================================================ FILE: core/Clash.Meta/Dockerfile ================================================ FROM alpine:latest as builder ARG TARGETPLATFORM RUN echo "I'm building for $TARGETPLATFORM" RUN apk add --no-cache gzip && \ mkdir /mihomo-config && \ wget -O /mihomo-config/geoip.metadb https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb && \ wget -O /mihomo-config/geosite.dat https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat && \ wget -O /mihomo-config/geoip.dat https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat COPY docker/file-name.sh /mihomo/file-name.sh WORKDIR /mihomo COPY bin/ bin/ RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \ FILE_NAME=`ls bin/ | egrep "$FILE_NAME.gz"|awk NR==1` && echo $FILE_NAME && \ mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && chmod +x mihomo && echo "$FILE_NAME" > /mihomo-config/test FROM alpine:latest LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo" RUN apk add --no-cache ca-certificates tzdata iptables VOLUME ["/root/.config/mihomo/"] COPY --from=builder /mihomo-config/ /root/.config/mihomo/ COPY --from=builder /mihomo/mihomo /mihomo ENTRYPOINT [ "/mihomo" ] ================================================ FILE: core/Clash.Meta/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: core/Clash.Meta/Makefile ================================================ NAME=mihomo BINDIR=bin BRANCH=$(shell git branch --show-current) ifeq ($(BRANCH),Alpha) VERSION=alpha-$(shell git rev-parse --short HEAD) else ifeq ($(BRANCH),Beta) VERSION=beta-$(shell git rev-parse --short HEAD) else ifeq ($(BRANCH),) VERSION=$(shell git describe --tags) else VERSION=$(shell git rev-parse --short HEAD) endif BUILDTIME=$(shell date -u) GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github.com/metacubex/mihomo/constant.Version=$(VERSION)" \ -X "github.com/metacubex/mihomo/constant.BuildTime=$(BUILDTIME)" \ -w -s -buildid=' PLATFORM_LIST = \ darwin-386 \ darwin-amd64-compatible \ darwin-amd64 \ darwin-amd64-v1 \ darwin-amd64-v2 \ darwin-amd64-v3 \ darwin-arm64 \ linux-386 \ linux-amd64-compatible \ linux-amd64 \ linux-amd64-v1 \ linux-amd64-v2 \ linux-amd64-v3 \ linux-armv5 \ linux-armv6 \ linux-armv7 \ linux-arm64 \ linux-mips64 \ linux-mips64le \ linux-mips-softfloat \ linux-mips-hardfloat \ linux-mipsle-softfloat \ linux-mipsle-hardfloat \ linux-riscv64 \ linux-loong64 \ android-arm64 \ freebsd-386 \ freebsd-amd64 \ freebsd-arm64 WINDOWS_ARCH_LIST = \ windows-386 \ windows-amd64-compatible \ windows-amd64 \ windows-amd64-v1 \ windows-amd64-v2 \ windows-amd64-v3 \ windows-arm64 \ windows-arm32v7 all:linux-amd64-v3 linux-arm64\ darwin-amd64-v3 darwin-arm64\ windows-amd64-v3 windows-arm64\ darwin-all: darwin-amd64-v3 darwin-arm64 docker: GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ darwin-386: GOARCH=386 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ darwin-amd64-compatible: GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ darwin-amd64: GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ darwin-amd64-v1: GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ darwin-amd64-v2: GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ darwin-amd64-v3: GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ darwin-arm64: GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-386: GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-amd64-compatible: GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-amd64: GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-amd64-v1: GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-amd64-v2: GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-amd64-v3: GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-arm64: GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-armv5: GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-armv6: GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-armv7: GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-mips-softfloat: GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-mips-hardfloat: GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-mipsle-softfloat: GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-mipsle-hardfloat: GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-mips64: GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-mips64le: GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-riscv64: GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ linux-loong64: GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ android-arm64: GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ freebsd-386: GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ freebsd-amd64: GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ freebsd-arm64: GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ windows-386: GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe windows-amd64-compatible: GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe windows-amd64: GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe windows-amd64-v1: GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe windows-amd64-v2: GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe windows-amd64-v3: GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe windows-arm64: GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe windows-arm32v7: GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe gz_releases=$(addsuffix .gz, $(PLATFORM_LIST)) zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST)) $(gz_releases): %.gz : % chmod +x $(BINDIR)/$(NAME)-$(basename $@) gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@) $(zip_releases): %.zip : % zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST) releases: $(gz_releases) $(zip_releases) vet: go test ./... lint: golangci-lint run ./... clean: rm $(BINDIR)/* CLANG ?= clang-14 CFLAGS := -O2 -g -Wall -Werror $(CFLAGS) ================================================ FILE: core/Clash.Meta/adapter/adapter.go ================================================ package adapter import ( "context" "encoding/json" "fmt" "net" "net/url" "strings" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/queue" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/xsync" "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/http" ) var UnifiedDelay = atomic.NewBool(false) const ( defaultHistoriesNum = 10 ) type internalProxyState struct { alive atomic.Bool history *queue.Queue[C.DelayHistory] } type Proxy struct { C.ProxyAdapter alive atomic.Bool history *queue.Queue[C.DelayHistory] extra xsync.Map[string, *internalProxyState] } // Adapter implements C.Proxy func (p *Proxy) Adapter() C.ProxyAdapter { return p.ProxyAdapter } // AliveForTestUrl implements C.Proxy func (p *Proxy) AliveForTestUrl(url string) bool { if state, ok := p.extra.Load(url); ok { return state.alive.Load() } return p.alive.Load() } // DialContext implements C.ProxyAdapter func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { conn, err := p.ProxyAdapter.DialContext(ctx, metadata) return conn, err } // ListenPacketContext implements C.ProxyAdapter func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata) return pc, err } // DelayHistory implements C.Proxy func (p *Proxy) DelayHistory() []C.DelayHistory { queueM := p.history.Copy() histories := []C.DelayHistory{} for _, item := range queueM { histories = append(histories, item) } return histories } // DelayHistoryForTestUrl implements C.Proxy func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory { var queueM []C.DelayHistory if state, ok := p.extra.Load(url); ok { queueM = state.history.Copy() } histories := []C.DelayHistory{} for _, item := range queueM { histories = append(histories, item) } return histories } // ExtraDelayHistories return all delay histories for each test URL // implements C.Proxy func (p *Proxy) ExtraDelayHistories() map[string]C.ProxyState { histories := map[string]C.ProxyState{} p.extra.Range(func(k string, v *internalProxyState) bool { testUrl := k state := v queueM := state.history.Copy() var history []C.DelayHistory for _, item := range queueM { history = append(history, item) } histories[testUrl] = C.ProxyState{ Alive: state.alive.Load(), History: history, } return true }) return histories } // LastDelayForTestUrl return last history record of the specified URL. if proxy is not alive, return the max value of uint16. // implements C.Proxy func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) { var maxDelay uint16 = 0xffff alive := false var history C.DelayHistory if state, ok := p.extra.Load(url); ok { alive = state.alive.Load() history = state.history.Last() } if !alive || history.Delay == 0 { return maxDelay } return history.Delay } // MarshalJSON implements C.ProxyAdapter func (p *Proxy) MarshalJSON() ([]byte, error) { inner, err := p.ProxyAdapter.MarshalJSON() if err != nil { return inner, err } mapping := map[string]any{} _ = json.Unmarshal(inner, &mapping) mapping["history"] = p.DelayHistory() mapping["extra"] = p.ExtraDelayHistories() mapping["alive"] = p.alive.Load() mapping["name"] = p.Name() mapping["udp"] = p.SupportUDP() mapping["uot"] = p.SupportUOT() proxyInfo := p.ProxyInfo() mapping["xudp"] = proxyInfo.XUDP mapping["tfo"] = proxyInfo.TFO mapping["mptcp"] = proxyInfo.MPTCP mapping["smux"] = proxyInfo.SMUX mapping["interface"] = proxyInfo.Interface mapping["routing-mark"] = proxyInfo.RoutingMark mapping["provider-name"] = proxyInfo.ProviderName mapping["dialer-proxy"] = proxyInfo.DialerProxy return json.Marshal(mapping) } // URLTest get the delay for the specified URL // implements C.Proxy func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (t uint16, err error) { var satisfied bool defer func() { if UrlTestHook != nil { UrlTestHook(url, p.Name(), t) } alive := err == nil record := C.DelayHistory{Time: time.Now()} if alive { record.Delay = t } p.alive.Store(alive) p.history.Put(record) if p.history.Len() > defaultHistoriesNum { p.history.Pop() } state, _ := p.extra.LoadOrStoreFn(url, func() *internalProxyState { return &internalProxyState{ history: queue.New[C.DelayHistory](defaultHistoriesNum), alive: atomic.NewBool(true), } }) if !satisfied { record.Delay = 0 alive = false } state.alive.Store(alive) state.history.Put(record) if state.history.Len() > defaultHistoriesNum { state.history.Pop() } }() unifiedDelay := UnifiedDelay.Load() addr, err := urlToMetadata(url) if err != nil { return } start := time.Now() instance, err := p.DialContext(ctx, &addr) if err != nil { return } defer func() { _ = instance.Close() }() req, err := http.NewRequest(http.MethodHead, url, nil) if err != nil { return } req = req.WithContext(ctx) tlsConfig, err := ca.GetTLSConfig(ca.Option{}) if err != nil { return } transport := &http.Transport{ DialContext: func(context.Context, string, string) (net.Conn, error) { return instance, nil }, // from http.DefaultTransport MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, TLSClientConfig: tlsConfig, } client := http.Client{ Timeout: 30 * time.Second, Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } defer client.CloseIdleConnections() resp, err := client.Do(req) if err != nil { return } _ = resp.Body.Close() if unifiedDelay { second := time.Now() var ignoredErr error var secondResp *http.Response secondResp, ignoredErr = client.Do(req) if ignoredErr == nil { resp = secondResp _ = resp.Body.Close() start = second } else { if strings.HasPrefix(url, "http://") { log.Errorln("%s failed to get the second response from %s: %v", p.Name(), url, ignoredErr) log.Warnln("It is recommended to use HTTPS for provider.health-check.url and group.url to ensure better reliability. Due to some proxy providers hijacking test addresses and not being compatible with repeated HEAD requests, using HTTP may result in failed tests.") } } } satisfied = resp != nil && (expectedStatus == nil || expectedStatus.Check(uint16(resp.StatusCode))) t = uint16(time.Since(start) / time.Millisecond) return } func NewProxy(adapter C.ProxyAdapter) *Proxy { return &Proxy{ ProxyAdapter: adapter, history: queue.New[C.DelayHistory](defaultHistoriesNum), alive: atomic.NewBool(true), } } func urlToMetadata(rawURL string) (addr C.Metadata, err error) { u, err := url.Parse(rawURL) if err != nil { return } port := u.Port() if port == "" { switch u.Scheme { case "https": port = "443" case "http": port = "80" default: err = fmt.Errorf("%s scheme not Support", rawURL) return } } err = addr.SetRemoteAddress(net.JoinHostPort(u.Hostname(), port)) return } ================================================ FILE: core/Clash.Meta/adapter/inbound/addition.go ================================================ package inbound import ( "net" C "github.com/metacubex/mihomo/constant" ) type Addition func(metadata *C.Metadata) func ApplyAdditions(metadata *C.Metadata, additions ...Addition) { for _, addition := range additions { addition(metadata) } } func WithInName(name string) Addition { return func(metadata *C.Metadata) { metadata.InName = name } } func WithInUser(user string) Addition { return func(metadata *C.Metadata) { metadata.InUser = user } } func WithSpecialRules(specialRules string) Addition { return func(metadata *C.Metadata) { metadata.SpecialRules = specialRules } } func WithSpecialProxy(specialProxy string) Addition { return func(metadata *C.Metadata) { metadata.SpecialProxy = specialProxy } } func WithDstAddr(addr net.Addr) Addition { return func(metadata *C.Metadata) { _ = metadata.SetRemoteAddr(addr) } } func WithSrcAddr(addr net.Addr) Addition { return func(metadata *C.Metadata) { m := C.Metadata{} if err := m.SetRemoteAddr(addr); err == nil { metadata.SrcIP = m.DstIP metadata.SrcPort = m.DstPort } } } func WithInAddr(addr net.Addr) Addition { return func(metadata *C.Metadata) { m := C.Metadata{} if err := m.SetRemoteAddr(addr); err == nil { metadata.InIP = m.DstIP metadata.InPort = m.DstPort } } } func WithDSCP(dscp uint8) Addition { return func(metadata *C.Metadata) { metadata.DSCP = dscp } } func Placeholder(metadata *C.Metadata) {} ================================================ FILE: core/Clash.Meta/adapter/inbound/auth.go ================================================ package inbound import ( "net" "net/netip" C "github.com/metacubex/mihomo/constant" ) var skipAuthPrefixes []netip.Prefix func SetSkipAuthPrefixes(prefixes []netip.Prefix) { skipAuthPrefixes = prefixes } func SkipAuthPrefixes() []netip.Prefix { return skipAuthPrefixes } func SkipAuthRemoteAddr(addr net.Addr) bool { m := C.Metadata{} if err := m.SetRemoteAddr(addr); err != nil { return false } return skipAuth(m.AddrPort().Addr()) } func SkipAuthRemoteAddress(addr string) bool { m := C.Metadata{} if err := m.SetRemoteAddress(addr); err != nil { return false } return skipAuth(m.AddrPort().Addr()) } func skipAuth(addr netip.Addr) bool { return prefixesContains(skipAuthPrefixes, addr) } ================================================ FILE: core/Clash.Meta/adapter/inbound/http.go ================================================ package inbound import ( "net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) // NewHTTP receive normal http request and return HTTPContext func NewHTTP(target socks5.Addr, srcConn net.Conn, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) { metadata := parseSocksAddr(target) metadata.NetWork = C.TCP metadata.Type = C.HTTP metadata.RawSrcAddr = srcConn.RemoteAddr() metadata.RawDstAddr = srcConn.LocalAddr() ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(srcConn.LocalAddr())) ApplyAdditions(metadata, additions...) return conn, metadata } ================================================ FILE: core/Clash.Meta/adapter/inbound/https.go ================================================ package inbound import ( "net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/http" ) // NewHTTPS receive CONNECT request and return ConnContext func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) { metadata := parseHTTPAddr(request) metadata.Type = C.HTTPS metadata.RawSrcAddr = conn.RemoteAddr() metadata.RawDstAddr = conn.LocalAddr() ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) ApplyAdditions(metadata, additions...) return conn, metadata } ================================================ FILE: core/Clash.Meta/adapter/inbound/ipfilter.go ================================================ package inbound import ( "net" "net/netip" C "github.com/metacubex/mihomo/constant" ) var lanAllowedIPs []netip.Prefix var lanDisAllowedIPs []netip.Prefix func SetAllowedIPs(prefixes []netip.Prefix) { lanAllowedIPs = prefixes } func SetDisAllowedIPs(prefixes []netip.Prefix) { lanDisAllowedIPs = prefixes } func AllowedIPs() []netip.Prefix { return lanAllowedIPs } func DisAllowedIPs() []netip.Prefix { return lanDisAllowedIPs } func IsRemoteAddrDisAllowed(addr net.Addr) bool { m := C.Metadata{} if err := m.SetRemoteAddr(addr); err != nil { return false } ipAddr := m.AddrPort().Addr() if ipAddr.IsValid() { return isAllowed(ipAddr) && !isDisAllowed(ipAddr) } return false } func isAllowed(addr netip.Addr) bool { return prefixesContains(lanAllowedIPs, addr) } func isDisAllowed(addr netip.Addr) bool { return prefixesContains(lanDisAllowedIPs, addr) } ================================================ FILE: core/Clash.Meta/adapter/inbound/listen.go ================================================ package inbound import ( "context" "fmt" "net" "net/netip" "sync" "github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/mptcp" "github.com/metacubex/tfo-go" ) var ( lc = tfo.ListenConfig{ DisableTFO: true, } mutex sync.RWMutex ) func SetTfo(open bool) { mutex.Lock() defer mutex.Unlock() lc.DisableTFO = !open } func Tfo() bool { mutex.RLock() defer mutex.RUnlock() return !lc.DisableTFO } func SetMPTCP(open bool) { mutex.Lock() defer mutex.Unlock() mptcp.SetNetListenConfig(&lc.ListenConfig, open) } func MPTCP() bool { mutex.RLock() defer mutex.RUnlock() return mptcp.GetNetListenConfig(&lc.ListenConfig) } func preResolve(network, address string) (string, error) { switch network { // like net.Resolver.internetAddrList but filter domain to avoid call net.Resolver.lookupIPAddr case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6": if host, port, err := net.SplitHostPort(address); err == nil { switch host { case "localhost": switch network { case "tcp6", "udp6", "ip6": address = net.JoinHostPort("::1", port) default: address = net.JoinHostPort("127.0.0.1", port) } case "": // internetAddrList can handle this special case break default: if _, err := netip.ParseAddr(host); err != nil { // not ip return "", fmt.Errorf("invalid network address: %s", address) } } } } return address, nil } func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { address, err := preResolve(network, address) if err != nil { return nil, err } mutex.RLock() defer mutex.RUnlock() return lc.Listen(ctx, network, address) } func Listen(network, address string) (net.Listener, error) { return ListenContext(context.Background(), network, address) } func ListenPacketContext(ctx context.Context, network, address string) (net.PacketConn, error) { address, err := preResolve(network, address) if err != nil { return nil, err } mutex.RLock() defer mutex.RUnlock() return lc.ListenPacket(ctx, network, address) } func ListenPacket(network, address string) (net.PacketConn, error) { return ListenPacketContext(context.Background(), network, address) } func init() { keepalive.SetDisableKeepAliveCallback.Register(func(b bool) { mutex.Lock() defer mutex.Unlock() keepalive.SetNetListenConfig(&lc.ListenConfig) }) } ================================================ FILE: core/Clash.Meta/adapter/inbound/listen_notwindows.go ================================================ //go:build !windows package inbound import ( "net" "os" ) const SupportNamedPipe = false func ListenNamedPipe(path string) (net.Listener, error) { return nil, os.ErrInvalid } ================================================ FILE: core/Clash.Meta/adapter/inbound/listen_windows.go ================================================ package inbound import ( "net" "os" "github.com/metacubex/wireguard-go/ipc/namedpipe" "golang.org/x/sys/windows" ) const SupportNamedPipe = true // windowsSDDL is the Security Descriptor set on the namedpipe. // It provides read/write access to all users and the local system. const windowsSDDL = "D:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)" func ListenNamedPipe(path string) (net.Listener, error) { sddl := os.Getenv("LISTEN_NAMEDPIPE_SDDL") if sddl == "" { sddl = windowsSDDL } securityDescriptor, err := windows.SecurityDescriptorFromString(sddl) if err != nil { return nil, err } namedpipeLC := namedpipe.ListenConfig{ SecurityDescriptor: securityDescriptor, InputBufferSize: 256 * 1024, OutputBufferSize: 256 * 1024, } return namedpipeLC.Listen(path) } ================================================ FILE: core/Clash.Meta/adapter/inbound/packet.go ================================================ package inbound import ( C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) // NewPacket is PacketAdapter generator func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) (C.UDPPacket, *C.Metadata) { metadata := parseSocksAddr(target) metadata.NetWork = C.UDP metadata.Type = source metadata.RawSrcAddr = packet.LocalAddr() metadata.RawDstAddr = metadata.UDPAddr() ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr())) if p, ok := packet.(C.UDPPacketInAddr); ok { ApplyAdditions(metadata, WithInAddr(p.InAddr())) } ApplyAdditions(metadata, additions...) return packet, metadata } ================================================ FILE: core/Clash.Meta/adapter/inbound/socket.go ================================================ package inbound import ( "net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) // NewSocket receive TCP inbound and return ConnContext func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) (net.Conn, *C.Metadata) { metadata := parseSocksAddr(target) metadata.NetWork = C.TCP metadata.Type = source ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) ApplyAdditions(metadata, additions...) return conn, metadata } ================================================ FILE: core/Clash.Meta/adapter/inbound/util.go ================================================ package inbound import ( "net" "net/netip" "strings" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/http" ) func parseSocksAddr(target socks5.Addr) *C.Metadata { metadata := &C.Metadata{} switch target[0] { case socks5.AtypDomainName: // trim for FQDN metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) case socks5.AtypIPv4: metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv4len]) metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) case socks5.AtypIPv6: metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv6len]) metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) } metadata.DstIP = metadata.DstIP.Unmap() return metadata } func parseHTTPAddr(request *http.Request) *C.Metadata { host := request.URL.Hostname() port := request.URL.Port() if port == "" { port = "80" } // trim FQDN (#737) host = strings.TrimRight(host, ".") metadata := &C.Metadata{} _ = metadata.SetRemoteAddress(net.JoinHostPort(host, port)) return metadata } func prefixesContains(prefixes []netip.Prefix, addr netip.Addr) bool { if len(prefixes) == 0 { return false } if !addr.IsValid() { return false } addr = addr.Unmap().WithZone("") // netip.Prefix.Contains returns false if ip has an IPv6 zone for _, prefix := range prefixes { if prefix.Contains(addr) { return true } } return false } ================================================ FILE: core/Clash.Meta/adapter/outbound/anytls.go ================================================ package outbound import ( "context" "net" "strconv" "time" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/anytls" "github.com/metacubex/mihomo/transport/vmess" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/uot" ) type AnyTLS struct { *Base client *anytls.Client option *AnyTLSOption } type AnyTLSOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Password string `proxy:"password"` ALPN []string `proxy:"alpn,omitempty"` SNI string `proxy:"sni,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` UDP bool `proxy:"udp,omitempty"` IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"` IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"` MinIdleSession int `proxy:"min-idle-session,omitempty"` } func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := t.client.CreateProxy(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) if err != nil { return nil, err } return NewConn(c, t), nil } func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = t.ResolveUDP(ctx, metadata); err != nil { return nil, err } // create tcp c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2)) if err != nil { return nil, err } // create uot on tcp destination := M.SocksaddrFromNet(metadata.UDPAddr()) return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil } // SupportUOT implements C.ProxyAdapter func (t *AnyTLS) SupportUOT() bool { return true } // ProxyInfo implements C.ProxyAdapter func (t *AnyTLS) ProxyInfo() C.ProxyInfo { info := t.Base.ProxyInfo() info.DialerProxy = t.option.DialerProxy return info } // Close implements C.ProxyAdapter func (t *AnyTLS) Close() error { return t.client.Close() } func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) outbound := &AnyTLS{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.AnyTLS, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, } outbound.dialer = option.NewDialer(outbound.DialOptions()) singDialer := proxydialer.NewSingDialer(outbound.dialer) tOption := anytls.ClientConfig{ Password: option.Password, Server: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), Dialer: singDialer, IdleSessionCheckInterval: time.Duration(option.IdleSessionCheckInterval) * time.Second, IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second, MinIdleSession: option.MinIdleSession, } echConfig, err := option.ECHOpts.Parse() if err != nil { return nil, err } tlsConfig := &vmess.TLSConfig{ Host: option.SNI, SkipCertVerify: option.SkipCertVerify, NextProtos: option.ALPN, FingerPrint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, ClientFingerprint: option.ClientFingerprint, ECH: echConfig, } if tlsConfig.Host == "" { tlsConfig.Host = option.Server } tOption.TLSConfig = tlsConfig client := anytls.NewClient(context.TODO(), tOption) outbound.client = client return outbound, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/base.go ================================================ package outbound import ( "context" "encoding/json" "fmt" "net" "runtime" "sync" "syscall" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/gofrs/uuid/v5" ) type ProxyAdapter interface { C.ProxyAdapter DialOptions() []dialer.Option ResolveUDP(ctx context.Context, metadata *C.Metadata) error } type Base struct { name string addr string tp C.AdapterType pdName string udp bool xudp bool tfo bool mpTcp bool iface string rmark int prefer C.DNSPrefer dialer C.Dialer id uuid.UUID } type BaseOption struct { Name string Addr string Type C.AdapterType ProviderName string UDP bool XUDP bool TFO bool MPTCP bool Interface string RoutingMark int Prefer C.DNSPrefer } func NewBase(opt BaseOption) *Base { return &Base{ name: opt.Name, addr: opt.Addr, tp: opt.Type, pdName: opt.ProviderName, udp: opt.UDP, xudp: opt.XUDP, tfo: opt.TFO, mpTcp: opt.MPTCP, iface: opt.Interface, rmark: opt.RoutingMark, prefer: opt.Prefer, id: utils.NewUUIDV4(), } } // Name implements C.ProxyAdapter func (b *Base) Name() string { return b.name } // Id implements C.ProxyAdapter func (b *Base) Id() string { return b.id.String() } // Type implements C.ProxyAdapter func (b *Base) Type() C.AdapterType { return b.tp } func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { return nil, C.ErrNotSupport } // ListenPacketContext implements C.ProxyAdapter func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { return nil, C.ErrNotSupport } // SupportUOT implements C.ProxyAdapter func (b *Base) SupportUOT() bool { return false } // SupportUDP implements C.ProxyAdapter func (b *Base) SupportUDP() bool { return b.udp } // ProxyInfo implements C.ProxyAdapter func (b *Base) ProxyInfo() (info C.ProxyInfo) { info.XUDP = b.xudp info.TFO = b.tfo info.MPTCP = b.mpTcp info.SMUX = false info.Interface = b.iface info.RoutingMark = b.rmark info.ProviderName = b.pdName return } // IsL3Protocol implements C.ProxyAdapter func (b *Base) IsL3Protocol(metadata *C.Metadata) bool { return false } // MarshalJSON implements C.ProxyAdapter func (b *Base) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]string{ "type": b.Type().String(), "id": b.Id(), }) } // Addr implements C.ProxyAdapter func (b *Base) Addr() string { return b.addr } // Unwrap implements C.ProxyAdapter func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { return nil } // DialOptions return []dialer.Option from struct func (b *Base) DialOptions() (opts []dialer.Option) { if b.iface != "" { opts = append(opts, dialer.WithInterface(b.iface)) } if b.rmark != 0 { opts = append(opts, dialer.WithRoutingMark(b.rmark)) } switch b.prefer { case C.IPv4Only: opts = append(opts, dialer.WithOnlySingleStack(true)) case C.IPv6Only: opts = append(opts, dialer.WithOnlySingleStack(false)) case C.IPv4Prefer: opts = append(opts, dialer.WithPreferIPv4()) case C.IPv6Prefer: opts = append(opts, dialer.WithPreferIPv6()) default: } if b.tfo { opts = append(opts, dialer.WithTFO(true)) } if b.mpTcp { opts = append(opts, dialer.WithMPTCP(true)) } return opts } func (b *Base) ResolveUDP(ctx context.Context, metadata *C.Metadata) error { if !metadata.Resolved() { ip, err := resolver.ResolveIP(ctx, metadata.Host) if err != nil { return fmt.Errorf("can't resolve ip: %w", err) } metadata.DstIP = ip } return nil } func (b *Base) Close() error { return nil } type BasicOption struct { TFO bool `proxy:"tfo,omitempty"` MPTCP bool `proxy:"mptcp,omitempty"` Interface string `proxy:"interface-name,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty"` IPVersion C.DNSPrefer `proxy:"ip-version,omitempty"` DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy // // The following parameters are used internally, assign value by the structure decoder are disallowed // DialerForAPI C.Dialer `proxy:"-"` // the dialer used for API usage has higher priority than all the above configurations. ProviderName string `proxy:"-"` } func (b *BasicOption) NewDialer(opts []dialer.Option) C.Dialer { cDialer := b.DialerForAPI if cDialer == nil { if b.DialerProxy != "" { cDialer = proxydialer.NewByName(b.DialerProxy) } else { cDialer = dialer.NewDialer(opts...) } } return cDialer } type conn struct { N.ExtendedConn chain C.Chain pdChain C.Chain adapterAddr string } func (c *conn) RemoteDestination() string { if remoteAddr := c.RemoteAddr(); remoteAddr != nil { m := C.Metadata{} if err := m.SetRemoteAddr(remoteAddr); err == nil { if m.Valid() { return m.String() } } } host, _, _ := net.SplitHostPort(c.adapterAddr) return host } // Chains implements C.Connection func (c *conn) Chains() C.Chain { return c.chain } // ProviderChains implements C.Connection func (c *conn) ProviderChains() C.Chain { return c.pdChain } // AppendToChains implements C.Connection func (c *conn) AppendToChains(a C.ProxyAdapter) { c.chain = append(c.chain, a.Name()) c.pdChain = append(c.pdChain, a.ProxyInfo().ProviderName) } func (c *conn) Upstream() any { return c.ExtendedConn } func (c *conn) WriterReplaceable() bool { return true } func (c *conn) ReaderReplaceable() bool { return true } func (c *conn) AddRef(ref any) { c.ExtendedConn = N.NewRefConn(c.ExtendedConn, ref) // add ref for autoCloseProxyAdapter } func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly } cc := &conn{N.NewExtendedConn(c), nil, nil, a.Addr()} cc.AppendToChains(a) return cc } type packetConn struct { N.EnhancePacketConn chain C.Chain pdChain C.Chain adapterName string connID string adapterAddr string resolveUDP func(ctx context.Context, metadata *C.Metadata) error } func (c *packetConn) ResolveUDP(ctx context.Context, metadata *C.Metadata) error { return c.resolveUDP(ctx, metadata) } func (c *packetConn) RemoteDestination() string { host, _, _ := net.SplitHostPort(c.adapterAddr) return host } // Chains implements C.Connection func (c *packetConn) Chains() C.Chain { return c.chain } // ProviderChains implements C.Connection func (c *packetConn) ProviderChains() C.Chain { return c.pdChain } // AppendToChains implements C.Connection func (c *packetConn) AppendToChains(a C.ProxyAdapter) { c.chain = append(c.chain, a.Name()) c.pdChain = append(c.pdChain, a.ProxyInfo().ProviderName) } func (c *packetConn) LocalAddr() net.Addr { lAddr := c.EnhancePacketConn.LocalAddr() return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy } func (c *packetConn) Upstream() any { return c.EnhancePacketConn } func (c *packetConn) WriterReplaceable() bool { return true } func (c *packetConn) ReaderReplaceable() bool { return true } func (c *packetConn) AddRef(ref any) { c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter } func newPacketConn(pc net.PacketConn, a ProxyAdapter) C.PacketConn { epc := N.NewEnhancePacketConn(pc) if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly } cpc := &packetConn{epc, nil, nil, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP} cpc.AppendToChains(a) return cpc } type AddRef interface { AddRef(ref any) } type autoCloseProxyAdapter struct { ProxyAdapter closeOnce sync.Once closeErr error } func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := p.ProxyAdapter.DialContext(ctx, metadata) if err != nil { return nil, err } if c, ok := c.(AddRef); ok { c.AddRef(p) } return c, nil } func (p *autoCloseProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata) if err != nil { return nil, err } if pc, ok := pc.(AddRef); ok { pc.AddRef(p) } return pc, nil } func (p *autoCloseProxyAdapter) Close() error { p.closeOnce.Do(func() { log.Debugln("Closing outdated proxy [%s]", p.Name()) runtime.SetFinalizer(p, nil) p.closeErr = p.ProxyAdapter.Close() }) return p.closeErr } func NewAutoCloseProxyAdapter(adapter ProxyAdapter) ProxyAdapter { proxy := &autoCloseProxyAdapter{ ProxyAdapter: adapter, } // auto close ProxyAdapter runtime.SetFinalizer(proxy, (*autoCloseProxyAdapter).Close) return proxy } ================================================ FILE: core/Clash.Meta/adapter/outbound/direct.go ================================================ package outbound import ( "context" "fmt" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/loopback" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" ) type Direct struct { *Base loopBack *loopback.Detector } type DirectOption struct { BasicOption Name string `proxy:"name"` } // DialContext implements C.ProxyAdapter func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { if err := d.loopBack.CheckConn(metadata); err != nil { return nil, err } opts := d.DialOptions() opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver)) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), opts...) if err != nil { return nil, err } return d.loopBack.NewConn(NewConn(c, d)), nil } // ListenPacketContext implements C.ProxyAdapter func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { if err := d.loopBack.CheckPacketConn(metadata); err != nil { return nil, err } if err := d.ResolveUDP(ctx, metadata); err != nil { return nil, err } pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort()) if err != nil { return nil, err } return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil } func (d *Direct) ResolveUDP(ctx context.Context, metadata *C.Metadata) error { if (!metadata.Resolved() || resolver.DirectHostResolver != resolver.DefaultResolver) && metadata.Host != "" { ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver) if err != nil { return fmt.Errorf("can't resolve ip: %w", err) } metadata.DstIP = ip } return nil } func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool { return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver } func NewDirectWithOption(option DirectOption) *Direct { return &Direct{ Base: NewBase(BaseOption{ Name: option.Name, Type: C.Direct, ProviderName: option.ProviderName, UDP: true, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), loopBack: loopback.NewDetector(), } } func NewDirect() *Direct { return &Direct{ Base: NewBase(BaseOption{ Name: "DIRECT", Type: C.Direct, UDP: true, Prefer: C.DualStack, }), loopBack: loopback.NewDetector(), } } func NewCompatible() *Direct { return &Direct{ Base: NewBase(BaseOption{ Name: "COMPATIBLE", Type: C.Compatible, UDP: true, Prefer: C.DualStack, }), loopBack: loopback.NewDetector(), } } ================================================ FILE: core/Clash.Meta/adapter/outbound/dns.go ================================================ package outbound import ( "context" "net" "net/netip" "time" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) type Dns struct { *Base } type DnsOption struct { BasicOption Name string `proxy:"name"` } // DialContext implements C.ProxyAdapter func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { left, right := N.Pipe() go resolver.RelayDnsConn(context.Background(), right, 0) return NewConn(left, d), nil } // ListenPacketContext implements C.ProxyAdapter func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort()) if err := d.ResolveUDP(ctx, metadata); err != nil { return nil, err } ctx, cancel := context.WithCancel(context.Background()) return newPacketConn(&dnsPacketConn{ response: make(chan dnsPacket, 1), ctx: ctx, cancel: cancel, }, d), nil } func (d *Dns) ResolveUDP(ctx context.Context, metadata *C.Metadata) error { if !metadata.Resolved() { metadata.DstIP = netip.AddrFrom4([4]byte{127, 0, 0, 2}) } return nil } type dnsPacket struct { data []byte put func() addr net.Addr } // dnsPacketConn implements net.PacketConn type dnsPacketConn struct { response chan dnsPacket ctx context.Context cancel context.CancelFunc } func (d *dnsPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { select { case packet := <-d.response: return packet.data, packet.put, packet.addr, nil case <-d.ctx.Done(): return nil, nil, nil, net.ErrClosed } } func (d *dnsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { select { case packet := <-d.response: n = copy(p, packet.data) if packet.put != nil { packet.put() } return n, packet.addr, nil case <-d.ctx.Done(): return 0, nil, net.ErrClosed } } func (d *dnsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { select { case <-d.ctx.Done(): return 0, net.ErrClosed default: } if len(p) > resolver.SafeDnsPacketSize { // wtf??? return len(p), nil } buf := pool.Get(resolver.SafeDnsPacketSize) put := func() { _ = pool.Put(buf) } copy(buf, p) // avoid p be changed after WriteTo returned go func() { // don't block the WriteTo function ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout) defer cancel() buf, err = resolver.RelayDnsPacket(ctx, buf[:len(p)], buf) if err != nil { put() return } packet := dnsPacket{ data: buf, put: put, addr: addr, } select { case d.response <- packet: break case <-d.ctx.Done(): put() } }() return len(p), nil } func (d *dnsPacketConn) Close() error { d.cancel() return nil } func (*dnsPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 53, Zone: "", } } func (*dnsPacketConn) SetDeadline(t time.Time) error { return nil } func (*dnsPacketConn) SetReadDeadline(t time.Time) error { return nil } func (*dnsPacketConn) SetWriteDeadline(t time.Time) error { return nil } func NewDnsWithOption(option DnsOption) *Dns { return &Dns{ Base: NewBase(BaseOption{ Name: option.Name, Type: C.Dns, ProviderName: option.ProviderName, UDP: true, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), } } ================================================ FILE: core/Clash.Meta/adapter/outbound/ech.go ================================================ package outbound import ( "context" "encoding/base64" "fmt" "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/component/resolver" ) type ECHOptions struct { Enable bool `proxy:"enable,omitempty" obfs:"enable,omitempty"` Config string `proxy:"config,omitempty" obfs:"config,omitempty"` QueryServerName string `proxy:"query-server-name,omitempty" obfs:"query-server-name,omitempty"` } func (o ECHOptions) Parse() (*ech.Config, error) { if !o.Enable { return nil, nil } echConfig := &ech.Config{} if o.Config != "" { list, err := base64.StdEncoding.DecodeString(o.Config) if err != nil { return nil, fmt.Errorf("base64 decode ech config string failed: %v", err) } echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) { return list, nil } } else { echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) { if o.QueryServerName != "" { // overrides the domain name used for ECH HTTPS record queries serverName = o.QueryServerName } return resolver.ResolveECHWithResolver(ctx, serverName, resolver.ProxyServerHostResolver) } } return echConfig, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/http.go ================================================ package outbound import ( "bufio" "context" "encoding/base64" "errors" "fmt" "net" "strconv" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/http" "github.com/metacubex/tls" ) type Http struct { *Base user string pass string tlsConfig *tls.Config option *HttpOption } type HttpOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` UserName string `proxy:"username,omitempty"` Password string `proxy:"password,omitempty"` TLS bool `proxy:"tls,omitempty"` SNI string `proxy:"sni,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` Headers map[string]string `proxy:"headers,omitempty"` } // StreamConnContext implements C.ProxyAdapter func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { if h.tlsConfig != nil { cc := tls.Client(c, h.tlsConfig) err := cc.HandshakeContext(ctx) c = cc if err != nil { return nil, fmt.Errorf("%s connect error: %w", h.addr, err) } } if err := h.shakeHandContext(ctx, c, metadata); err != nil { return nil, err } return c, nil } // DialContext implements C.ProxyAdapter func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := h.dialer.DialContext(ctx, "tcp", h.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", h.addr, err) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = h.StreamConnContext(ctx, c, metadata) if err != nil { return nil, err } return NewConn(c, h), nil } // ProxyInfo implements C.ProxyAdapter func (h *Http) ProxyInfo() C.ProxyInfo { info := h.Base.ProxyInfo() info.DialerProxy = h.option.DialerProxy return info } func (h *Http) shakeHandContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } addr := metadata.RemoteAddress() HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n" tempHeaders := map[string]string{ "Host": addr, "User-Agent": "Go-http-client/1.1", "Proxy-Connection": "Keep-Alive", } for key, value := range h.option.Headers { tempHeaders[key] = value } if h.user != "" && h.pass != "" { auth := h.user + ":" + h.pass tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) } for key, value := range tempHeaders { HeaderString += key + ": " + value + "\r\n" } HeaderString += "\r\n" _, err = c.Write([]byte(HeaderString)) if err != nil { return err } resp, err := http.ReadResponse(bufio.NewReader(c), nil) if err != nil { return err } if resp.StatusCode == http.StatusOK { return nil } if resp.StatusCode == http.StatusProxyAuthRequired { return errors.New("HTTP need auth") } if resp.StatusCode == http.StatusMethodNotAllowed { return errors.New("CONNECT method not allowed by proxy") } if resp.StatusCode >= http.StatusInternalServerError { return errors.New(resp.Status) } return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode) } func NewHttp(option HttpOption) (*Http, error) { var tlsConfig *tls.Config if option.TLS { sni := option.Server if option.SNI != "" { sni = option.SNI } var err error tlsConfig, err = ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ InsecureSkipVerify: option.SkipCertVerify, ServerName: sni, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } } outbound := &Http{ Base: NewBase(BaseOption{ Name: option.Name, Addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), Type: C.Http, ProviderName: option.ProviderName, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), user: option.UserName, pass: option.Password, tlsConfig: tlsConfig, option: &option, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/hysteria.go ================================================ package outbound import ( "context" "encoding/base64" "fmt" "net" "net/netip" "strconv" "time" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" hyCongestion "github.com/metacubex/mihomo/transport/hysteria/congestion" "github.com/metacubex/mihomo/transport/hysteria/core" "github.com/metacubex/mihomo/transport/hysteria/obfs" "github.com/metacubex/mihomo/transport/hysteria/pmtud_fix" "github.com/metacubex/mihomo/transport/hysteria/transport" "github.com/metacubex/mihomo/transport/hysteria/utils" "github.com/metacubex/tls" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/congestion" M "github.com/metacubex/sing/common/metadata" ) const ( mbpsToBps = 125000 DefaultStreamReceiveWindow = 15728640 // 15 MB/s DefaultConnectionReceiveWindow = 67108864 // 64 MB/s DefaultALPN = "hysteria" DefaultProtocol = "udp" DefaultHopInterval = 10 ) type Hysteria struct { *Base option *HysteriaOption client *core.Client tlsConfig *tls.Config echConfig *ech.Config } func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx)) if err != nil { return nil, err } return NewConn(tcpConn, h), nil } func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { if err := h.ResolveUDP(ctx, metadata); err != nil { return nil, err } udpConn, err := h.client.DialUDP(h.genHdc(ctx)) if err != nil { return nil, err } return newPacketConn(&hyPacketConn{udpConn}, h), nil } func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer { return &hyDialerWithContext{ ctx: context.Background(), hyDialer: func(network string, rAddr net.Addr) (net.PacketConn, error) { rAddrPort, _ := netip.ParseAddrPort(rAddr.String()) return h.dialer.ListenPacket(ctx, network, "", rAddrPort) }, remoteAddr: func(addr string) (net.Addr, error) { udpAddr, err := resolveUDPAddr(ctx, "udp", addr, h.prefer) if err != nil { return nil, err } err = h.echConfig.ClientHandle(ctx, h.tlsConfig) if err != nil { return nil, err } return udpAddr, nil }, } } // ProxyInfo implements C.ProxyAdapter func (h *Hysteria) ProxyInfo() C.ProxyInfo { info := h.Base.ProxyInfo() info.DialerProxy = h.option.DialerProxy return info } type HysteriaOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port,omitempty"` Ports string `proxy:"ports,omitempty"` Protocol string `proxy:"protocol,omitempty"` ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash Up string `proxy:"up"` UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash Down string `proxy:"down"` DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash Auth string `proxy:"auth,omitempty"` AuthString string `proxy:"auth-str,omitempty"` Obfs string `proxy:"obfs,omitempty"` SNI string `proxy:"sni,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` ALPN []string `proxy:"alpn,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` FastOpen bool `proxy:"fast-open,omitempty"` HopInterval int `proxy:"hop-interval,omitempty"` } func (c *HysteriaOption) Speed() (uint64, uint64, error) { var up, down uint64 up = StringToBps(c.Up) if up == 0 { return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) } down = StringToBps(c.Down) if down == 0 { return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) } return up, down, nil } func NewHysteria(option HysteriaOption) (*Hysteria, error) { clientTransport := &transport.ClientTransport{} addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) ports := option.Ports serverName := option.Server if option.SNI != "" { serverName = option.SNI } tlsConfig, err := ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: serverName, InsecureSkipVerify: option.SkipCertVerify, MinVersion: tls.VersionTLS13, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array tlsConfig.NextProtos = option.ALPN } else { tlsConfig.NextProtos = []string{DefaultALPN} } echConfig, err := option.ECHOpts.Parse() if err != nil { return nil, err } tlsClientConfig := tlsConfig quicConfig := &quic.Config{ InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), InitialConnectionReceiveWindow: uint64(option.ReceiveWindow), MaxConnectionReceiveWindow: uint64(option.ReceiveWindow), KeepAlivePeriod: 10 * time.Second, DisablePathMTUDiscovery: option.DisableMTUDiscovery, EnableDatagrams: true, } if option.ObfsProtocol != "" { option.Protocol = option.ObfsProtocol } if option.Protocol == "" { option.Protocol = DefaultProtocol } if option.HopInterval == 0 { option.HopInterval = DefaultHopInterval } hopInterval := time.Duration(int64(option.HopInterval)) * time.Second if option.ReceiveWindow == 0 { quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10 quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow } if option.ReceiveWindow == 0 { quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow / 10 quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow } if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery { log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform") } var auth = []byte(option.AuthString) if option.Auth != "" { auth, err = base64.StdEncoding.DecodeString(option.Auth) if err != nil { return nil, err } } var obfuscator obfs.Obfuscator if len(option.Obfs) > 0 { obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs)) } up, down, err := option.Speed() if err != nil { return nil, err } if option.UpSpeed != 0 { up = uint64(option.UpSpeed * mbpsToBps) } if option.DownSpeed != 0 { down = uint64(option.DownSpeed * mbpsToBps) } client, err := core.NewClient( addr, ports, option.Protocol, auth, tlsClientConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) }, obfuscator, hopInterval, option.FastOpen, ) if err != nil { return nil, fmt.Errorf("hysteria %s create error: %w", addr, err) } outbound := &Hysteria{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Hysteria, ProviderName: option.ProviderName, UDP: true, TFO: option.FastOpen, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, client: client, tlsConfig: tlsClientConfig, echConfig: echConfig, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } // Close implements C.ProxyAdapter func (h *Hysteria) Close() error { if h.client != nil { return h.client.Close() } return nil } type hyPacketConn struct { core.UDPConn } func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { b, addrStr, err := c.UDPConn.ReadFrom() if err != nil { return } n = copy(p, b) addr = M.ParseSocksaddr(addrStr).UDPAddr() return } func (c *hyPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { b, addrStr, err := c.UDPConn.ReadFrom() if err != nil { return } data = b addr = M.ParseSocksaddr(addrStr).UDPAddr() return } func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String()) if err != nil { return } n = len(p) return } type hyDialerWithContext struct { hyDialer func(network string, rAddr net.Addr) (net.PacketConn, error) ctx context.Context remoteAddr func(host string) (net.Addr, error) } func (h *hyDialerWithContext) ListenPacket(rAddr net.Addr) (net.PacketConn, error) { network := "udp" if addrPort, err := netip.ParseAddrPort(rAddr.String()); err == nil { network = dialer.ParseNetwork(network, addrPort.Addr()) } return h.hyDialer(network, rAddr) } func (h *hyDialerWithContext) Context() context.Context { return h.ctx } func (h *hyDialerWithContext) RemoteAddr(host string) (net.Addr, error) { return h.remoteAddr(host) } ================================================ FILE: core/Clash.Meta/adapter/outbound/hysteria2.go ================================================ package outbound import ( "context" "errors" "fmt" "net" "strconv" "time" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/quic-go" qtls "github.com/metacubex/sing-quic" "github.com/metacubex/sing-quic/hysteria2" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/tls" ) const minHopInterval = 5 const defaultHopInterval = 30 type Hysteria2 struct { *Base option *Hysteria2Option client *hysteria2.Client } type Hysteria2Option struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port,omitempty"` Ports string `proxy:"ports,omitempty"` HopInterval string `proxy:"hop-interval,omitempty"` Up string `proxy:"up,omitempty"` Down string `proxy:"down,omitempty"` Password string `proxy:"password,omitempty"` Obfs string `proxy:"obfs,omitempty"` ObfsPassword string `proxy:"obfs-password,omitempty"` SNI string `proxy:"sni,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` ALPN []string `proxy:"alpn,omitempty"` CWND int `proxy:"cwnd,omitempty"` BBRProfile string `proxy:"bbr-profile,omitempty"` UdpMTU int `proxy:"udp-mtu,omitempty"` // quic-go special config InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"` MaxStreamReceiveWindow uint64 `proxy:"max-stream-receive-window,omitempty"` InitialConnectionReceiveWindow uint64 `proxy:"initial-connection-receive-window,omitempty"` MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"` } func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) if err != nil { return nil, err } return NewConn(c, h), nil } func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = h.ResolveUDP(ctx, metadata); err != nil { return nil, err } pc, err := h.client.ListenPacket(ctx) if err != nil { return nil, err } if pc == nil { return nil, errors.New("packetConn is nil") } return newPacketConn(N.NewThreadSafePacketConn(pc), h), nil } // Close implements C.ProxyAdapter func (h *Hysteria2) Close() error { if h.client != nil { return h.client.CloseWithError(errors.New("proxy removed")) } return nil } // ProxyInfo implements C.ProxyAdapter func (h *Hysteria2) ProxyInfo() C.ProxyInfo { info := h.Base.ProxyInfo() info.DialerProxy = h.option.DialerProxy return info } func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) outbound := &Hysteria2{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Hysteria2, ProviderName: option.ProviderName, UDP: true, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, } outbound.dialer = option.NewDialer(outbound.DialOptions()) var salamanderPassword string if len(option.Obfs) > 0 { if option.ObfsPassword == "" { return nil, errors.New("missing obfs password") } switch option.Obfs { case hysteria2.ObfsTypeSalamander: salamanderPassword = option.ObfsPassword default: return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs) } } serverName := option.Server if option.SNI != "" { serverName = option.SNI } tlsConfig, err := ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: serverName, InsecureSkipVerify: option.SkipCertVerify, MinVersion: tls.VersionTLS13, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array tlsConfig.NextProtos = option.ALPN } tlsClientConfig := tlsConfig echConfig, err := option.ECHOpts.Parse() if err != nil { return nil, err } if option.UdpMTU == 0 { // "1200" from quic-go's MaxDatagramSize // "-3" from quic-go's DatagramFrame.MaxDataLen option.UdpMTU = 1200 - 3 } quicConfig := &quic.Config{ InitialStreamReceiveWindow: option.InitialStreamReceiveWindow, MaxStreamReceiveWindow: option.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: option.InitialConnectionReceiveWindow, MaxConnectionReceiveWindow: option.MaxConnectionReceiveWindow, } clientOptions := hysteria2.ClientOptions{ Context: context.TODO(), Logger: log.SingLogger, SendBPS: StringToBps(option.Up), ReceiveBPS: StringToBps(option.Down), SalamanderPassword: salamanderPassword, Password: option.Password, TLSConfig: tlsClientConfig, QUICConfig: quicConfig, UDPDisabled: false, UdpMTU: option.UdpMTU, ServerAddress: M.ParseSocksaddr(addr), PacketListener: outbound.dialer, QuicDialer: qtls.QuicDialerFunc(func(ctx context.Context, addr string, dialer qtls.PacketDialer, tlsCfg *tls.Config, cfg *quic.Config, early bool) (net.PacketConn, *quic.Conn, error) { err := echConfig.ClientHandle(ctx, tlsCfg) if err != nil { return nil, nil, err } return common.DialQuic(ctx, addr, outbound.DialOptions(), dialer, tlsCfg, cfg, early) }), SetBBRCongestion: func(quicConn *quic.Conn) { common.SetCongestionController(quicConn, "bbr", option.CWND, option.BBRProfile) }, } var serverPorts []uint16 if option.Ports != "" { ranges, err := utils.NewUnsignedRanges[uint16](option.Ports) if err != nil { return nil, err } ranges.Range(func(port uint16) bool { serverPorts = append(serverPorts, port) return true }) if len(serverPorts) > 0 { hopRange, err := utils.NewUnsignedRange[uint64](option.HopInterval) if err != nil { return nil, err } start, end := hopRange.Start(), hopRange.End() if start == 0 { start = defaultHopInterval } else if start < minHopInterval { start = minHopInterval } if end < start { end = start } clientOptions.HopInterval = time.Duration(start) * time.Second clientOptions.HopIntervalMax = time.Duration(end) * time.Second clientOptions.ServerPorts = serverPorts } } if option.Port == 0 && len(serverPorts) == 0 { return nil, errors.New("invalid port") } client, err := hysteria2.NewClient(clientOptions) if err != nil { return nil, err } outbound.client = client return outbound, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/masque.go ================================================ package outbound import ( "context" "crypto/ecdsa" "crypto/x509" "encoding/base64" "errors" "fmt" "io" "net" "net/netip" "strconv" "strings" "sync" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/contextutils" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/masque" "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/http" "github.com/metacubex/quic-go" wireguard "github.com/metacubex/sing-wireguard" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/tls" ) type Masque struct { *Base tlsConfig *tls.Config quicConfig *quic.Config tunDevice wireguard.Device resolver resolver.Resolver uri string h2Transport *http.Transport runCtx context.Context runCancel context.CancelFunc runMutex sync.Mutex running atomic.Bool runDevice atomic.Bool option MasqueOption } type MasqueOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` PrivateKey string `proxy:"private-key"` PublicKey string `proxy:"public-key"` Ip string `proxy:"ip,omitempty"` Ipv6 string `proxy:"ipv6,omitempty"` URI string `proxy:"uri,omitempty"` SNI string `proxy:"sni,omitempty"` MTU int `proxy:"mtu,omitempty"` UDP bool `proxy:"udp,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Network string `proxy:"network,omitempty"` CongestionController string `proxy:"congestion-controller,omitempty"` CWND int `proxy:"cwnd,omitempty"` BBRProfile string `proxy:"bbr-profile,omitempty"` RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"` Dns []string `proxy:"dns,omitempty"` } func (option MasqueOption) Prefixes() ([]netip.Prefix, error) { localPrefixes := make([]netip.Prefix, 0, 2) if len(option.Ip) > 0 { if !strings.Contains(option.Ip, "/") { option.Ip = option.Ip + "/32" } if prefix, err := netip.ParsePrefix(option.Ip); err == nil { localPrefixes = append(localPrefixes, prefix) } else { return nil, fmt.Errorf("ip address parse error: %w", err) } } if len(option.Ipv6) > 0 { if !strings.Contains(option.Ipv6, "/") { option.Ipv6 = option.Ipv6 + "/128" } if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil { localPrefixes = append(localPrefixes, prefix) } else { return nil, fmt.Errorf("ipv6 address parse error: %w", err) } } if len(localPrefixes) == 0 { return nil, errors.New("missing local address") } return localPrefixes, nil } func NewMasque(option MasqueOption) (*Masque, error) { outbound := &Masque{ Base: NewBase(BaseOption{ Name: option.Name, Addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), Type: C.Masque, ProviderName: option.ProviderName, UDP: option.UDP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), } outbound.dialer = option.NewDialer(outbound.DialOptions()) ctx, cancel := context.WithCancel(context.Background()) outbound.runCtx = ctx outbound.runCancel = cancel privKeyB64, err := base64.StdEncoding.DecodeString(option.PrivateKey) if err != nil { return nil, fmt.Errorf("failed to decode private key: %v", err) } privKey, err := x509.ParseECPrivateKey(privKeyB64) if err != nil { return nil, fmt.Errorf("failed to parse private key: %v", err) } endpointPubKeyB64, err := base64.StdEncoding.DecodeString(option.PublicKey) if err != nil { return nil, fmt.Errorf("failed to decode public key: %v", err) } pubKey, err := x509.ParsePKIXPublicKey(endpointPubKeyB64) if err != nil { return nil, fmt.Errorf("failed to parse public key: %v", err) } ecPubKey, ok := pubKey.(*ecdsa.PublicKey) if !ok { return nil, fmt.Errorf("failed to assert public key as ECDSA") } uri := option.URI if uri == "" { uri = masque.ConnectURI } outbound.uri = uri sni := option.SNI if sni == "" { sni = masque.ConnectSNI } tlsConfig, err := masque.PrepareTlsConfig(privKey, ecPubKey, sni, option.SkipCertVerify) if err != nil { return nil, fmt.Errorf("failed to prepare TLS config: %v\n", err) } outbound.tlsConfig = tlsConfig if option.Network == "h2" { tlsConfig.NextProtos = []string{"h2"} // use h2c mode to disallow the net/http fallback to http1.1 when server returns a not h2 ALPN protocols := new(http.Protocols) protocols.SetUnencryptedHTTP2(true) outbound.h2Transport = &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { c, err := outbound.dialer.DialContext(ctx, "tcp", outbound.addr) if err != nil { return nil, err } tlsConn := tls.Client(c, tlsConfig) err = tlsConn.HandshakeContext(ctx) if err != nil { _ = c.Close() return nil, err } return tlsConn, nil }, Protocols: protocols, HTTP2: &http.HTTP2Config{ SendPingTimeout: 30 * time.Second, }, } } outbound.quicConfig = &quic.Config{ EnableDatagrams: true, InitialPacketSize: 1242, KeepAlivePeriod: 30 * time.Second, } prefixes, err := option.Prefixes() if err != nil { return nil, err } outbound.option = option mtu := option.MTU if mtu == 0 { mtu = 1280 } if len(prefixes) == 0 { return nil, errors.New("missing local address") } outbound.tunDevice, err = wireguard.NewStackDevice(prefixes, uint32(mtu)) if err != nil { return nil, fmt.Errorf("create device: %w", err) } var has6 bool for _, address := range prefixes { if !address.Addr().Unmap().Is4() { has6 = true break } } if option.RemoteDnsResolve && len(option.Dns) > 0 { nss, err := dns.ParseNameServer(option.Dns) if err != nil { return nil, err } for i := range nss { nss[i].ProxyAdapter = outbound } outbound.resolver = dns.NewResolver(dns.Config{ Main: nss, IPv6: has6, }) } return outbound, nil } func (w *Masque) run(ctx context.Context) error { if w.running.Load() { return nil } w.runMutex.Lock() defer w.runMutex.Unlock() // double-check like sync.Once if w.running.Load() { return nil } if w.runCtx.Err() != nil { return w.runCtx.Err() } if !w.runDevice.Load() { err := w.tunDevice.Start() if err != nil { return err } w.runDevice.Store(true) } var pc net.PacketConn var closer io.Closer var ipConn masque.IpConn var err error if w.h2Transport != nil { closer, ipConn, err = masque.ConnectTunnelH2(ctx, w.h2Transport, w.uri) if err != nil { return err } } else { var quicConn *quic.Conn pc, quicConn, err = common.DialQuic(ctx, w.addr, w.DialOptions(), w.dialer, w.tlsConfig, w.quicConfig, false) if err != nil { return err } common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND, w.option.BBRProfile) closer, ipConn, err = masque.ConnectTunnel(ctx, quicConn, w.uri) if err != nil { _ = pc.Close() return err } } w.running.Store(true) runCtx, runCancel := context.WithCancel(w.runCtx) contextutils.AfterFunc(runCtx, func() { w.running.Store(false) _ = ipConn.Close() _ = closer.Close() if pc != nil { _ = pc.Close() } }) go func() { defer runCancel() buf := pool.Get(pool.UDPBufferSize) defer pool.Put(buf) bufs := [][]byte{buf} sizes := []int{0} for runCtx.Err() == nil { _, err := w.tunDevice.Read(bufs, sizes, 0) if err != nil { log.Errorln("[Masque](%s) error reading from TUN device: %v", w.name, err) return } icmp, err := ipConn.WritePacket(buf[:sizes[0]]) if err != nil { if errors.Is(err, net.ErrClosed) { log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err) return } log.Warnln("[Masque](%s) error writing to IP connection: %v, continuing...", w.name, err) continue } if len(icmp) > 0 { if _, err := w.tunDevice.Write([][]byte{icmp}, 0); err != nil { log.Warnln("[Masque](%s) error writing ICMP to TUN device: %v, continuing...", w.name, err) } } } }() go func() { defer runCancel() for runCtx.Err() == nil { buf, err := ipConn.ReadPacket() if err != nil { if errors.Is(err, net.ErrClosed) { log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err) return } log.Warnln("[Masque](%s) error reading from IP connection: %v, continuing...", w.name, err) continue } if _, err := w.tunDevice.Write([][]byte{buf}, 0); err != nil { log.Errorln("[Masque](%s) error writing to TUN device: %v", w.name, err) return } } }() return nil } // Close implements C.ProxyAdapter func (w *Masque) Close() error { w.runCancel() if w.tunDevice != nil { w.tunDevice.Close() } return nil } func (w *Masque) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { var conn net.Conn if err = w.run(ctx); err != nil { return nil, err } if !metadata.Resolved() || w.resolver != nil { r := resolver.DefaultResolver if w.resolver != nil { r = w.resolver } options := w.DialOptions() options = append(options, dialer.WithResolver(r)) options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice})) conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress()) } else { conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) } if err != nil { return nil, err } if conn == nil { return nil, errors.New("conn is nil") } return NewConn(conn, w), nil } func (w *Masque) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { var pc net.PacketConn if err = w.run(ctx); err != nil { return nil, err } if err = w.ResolveUDP(ctx, metadata); err != nil { return nil, err } pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) if err != nil { return nil, err } if pc == nil { return nil, errors.New("packetConn is nil") } return newPacketConn(pc, w), nil } func (w *Masque) ResolveUDP(ctx context.Context, metadata *C.Metadata) error { if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { r := resolver.DefaultResolver if w.resolver != nil { r = w.resolver } ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) if err != nil { return fmt.Errorf("can't resolve ip: %w", err) } metadata.DstIP = ip } return nil } // ProxyInfo implements C.ProxyAdapter func (w *Masque) ProxyInfo() C.ProxyInfo { info := w.Base.ProxyInfo() info.DialerProxy = w.option.DialerProxy return info } // IsL3Protocol implements C.ProxyAdapter func (w *Masque) IsL3Protocol(metadata *C.Metadata) bool { return true } ================================================ FILE: core/Clash.Meta/adapter/outbound/mieru.go ================================================ package outbound import ( "context" "fmt" "net" "net/netip" "strconv" "sync" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" mieruclient "github.com/enfein/mieru/v3/apis/client" mierucommon "github.com/enfein/mieru/v3/apis/common" mierumodel "github.com/enfein/mieru/v3/apis/model" mierutp "github.com/enfein/mieru/v3/apis/trafficpattern" mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb" "google.golang.org/protobuf/proto" ) type Mieru struct { *Base option *MieruOption client mieruclient.Client mu sync.Mutex } type MieruOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port,omitempty"` PortRange string `proxy:"port-range,omitempty"` Transport string `proxy:"transport"` UDP bool `proxy:"udp,omitempty"` UserName string `proxy:"username"` Password string `proxy:"password"` Multiplexing string `proxy:"multiplexing,omitempty"` HandshakeMode string `proxy:"handshake-mode,omitempty"` TrafficPattern string `proxy:"traffic-pattern,omitempty"` } type mieruPacketDialer struct { C.Dialer } var _ mierucommon.PacketDialer = (*mieruPacketDialer)(nil) func (pd mieruPacketDialer) ListenPacket(ctx context.Context, network, laddr, raddr string) (net.PacketConn, error) { rAddrPort, err := netip.ParseAddrPort(raddr) if err != nil { return nil, fmt.Errorf("invalid address %s: %w", raddr, err) } return pd.Dialer.ListenPacket(ctx, network, laddr, rAddrPort) } type mieruDNSResolver struct { prefer C.DNSPrefer } var _ mierucommon.DNSResolver = (*mieruDNSResolver)(nil) func (dr mieruDNSResolver) LookupIP(ctx context.Context, network, host string) (_ []net.IP, err error) { var ip netip.Addr switch dr.prefer { case C.IPv4Only: ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver) case C.IPv6Only: ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver) case C.IPv6Prefer: ip, err = resolver.ResolveIPPrefer6WithResolver(ctx, host, resolver.ProxyServerHostResolver) default: ip, err = resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver) } if err != nil { return nil, fmt.Errorf("can't resolve ip: %w", err) } // TODO: handle IP4P (due to interface limitations, it's currently impossible to modify the port here) return []net.IP{ip.AsSlice()}, nil } // DialContext implements C.ProxyAdapter func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { if err := m.ensureClientIsRunning(); err != nil { return nil, err } addr := metadataToMieruNetAddrSpec(metadata) c, err := m.client.DialContext(ctx, addr) if err != nil { return nil, fmt.Errorf("dial to %s failed: %w", addr, err) } return NewConn(c, m), nil } // ListenPacketContext implements C.ProxyAdapter func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = m.ResolveUDP(ctx, metadata); err != nil { return nil, err } if err := m.ensureClientIsRunning(); err != nil { return nil, err } c, err := m.client.DialContext(ctx, metadata.UDPAddr()) if err != nil { return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err) } return newPacketConn(N.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil } // SupportUOT implements C.ProxyAdapter func (m *Mieru) SupportUOT() bool { return true } // ProxyInfo implements C.ProxyAdapter func (m *Mieru) ProxyInfo() C.ProxyInfo { info := m.Base.ProxyInfo() info.DialerProxy = m.option.DialerProxy return info } func (m *Mieru) ensureClientIsRunning() error { m.mu.Lock() defer m.mu.Unlock() if m.client.IsRunning() { return nil } // Create a dialer and add it to the client config, before starting the client. config, err := m.client.Load() if err != nil { return err } config.Dialer = m.dialer config.PacketDialer = mieruPacketDialer{Dialer: m.dialer} config.Resolver = mieruDNSResolver{prefer: m.prefer} if err := m.client.Store(config); err != nil { return err } if err := m.client.Start(); err != nil { return fmt.Errorf("failed to start mieru client: %w", err) } return nil } func NewMieru(option MieruOption) (*Mieru, error) { config, err := buildMieruClientConfig(option) if err != nil { return nil, fmt.Errorf("failed to build mieru client config: %w", err) } c := mieruclient.NewClient() if err := c.Store(config); err != nil { return nil, fmt.Errorf("failed to store mieru client config: %w", err) } // Client is started lazily on the first use. var addr string if option.Port != 0 { addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) } else { beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange) addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort)) } outbound := &Mieru{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Mieru, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, client: c, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } // Close implements C.ProxyAdapter func (m *Mieru) Close() error { m.mu.Lock() defer m.mu.Unlock() if m.client != nil && m.client.IsRunning() { return m.client.Stop() } return nil } func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec { spec := mierumodel.NetAddrSpec{ Net: metadata.NetWork.String(), } if metadata.Host != "" { spec.AddrSpec = mierumodel.AddrSpec{ FQDN: metadata.Host, Port: int(metadata.DstPort), } } else { spec.AddrSpec = mierumodel.AddrSpec{ IP: metadata.DstIP.AsSlice(), Port: int(metadata.DstPort), } } return spec } func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) { if err := validateMieruOption(option); err != nil { return nil, fmt.Errorf("failed to validate mieru option: %w", err) } var transportProtocol = mierupb.TransportProtocol_UNKNOWN_TRANSPORT_PROTOCOL.Enum() switch option.Transport { case "TCP": transportProtocol = mierupb.TransportProtocol_TCP.Enum() case "UDP": transportProtocol = mierupb.TransportProtocol_UDP.Enum() } var server *mierupb.ServerEndpoint if net.ParseIP(option.Server) != nil { // server is an IP address if option.PortRange != "" { server = &mierupb.ServerEndpoint{ IpAddress: proto.String(option.Server), PortBindings: []*mierupb.PortBinding{ { PortRange: proto.String(option.PortRange), Protocol: transportProtocol, }, }, } } else { server = &mierupb.ServerEndpoint{ IpAddress: proto.String(option.Server), PortBindings: []*mierupb.PortBinding{ { Port: proto.Int32(int32(option.Port)), Protocol: transportProtocol, }, }, } } } else { // server is a domain name if option.PortRange != "" { server = &mierupb.ServerEndpoint{ DomainName: proto.String(option.Server), PortBindings: []*mierupb.PortBinding{ { PortRange: proto.String(option.PortRange), Protocol: transportProtocol, }, }, } } else { server = &mierupb.ServerEndpoint{ DomainName: proto.String(option.Server), PortBindings: []*mierupb.PortBinding{ { Port: proto.Int32(int32(option.Port)), Protocol: transportProtocol, }, }, } } } config := &mieruclient.ClientConfig{ Profile: &mierupb.ClientProfile{ ProfileName: proto.String(option.Name), User: &mierupb.User{ Name: proto.String(option.UserName), Password: proto.String(option.Password), }, Servers: []*mierupb.ServerEndpoint{server}, }, DNSConfig: &mierucommon.ClientDNSConfig{ BypassDialerDNS: true, }, } if multiplexing, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; ok { config.Profile.Multiplexing = &mierupb.MultiplexingConfig{ Level: mierupb.MultiplexingLevel(multiplexing).Enum(), } } if handshakeMode, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; ok { config.Profile.HandshakeMode = (*mierupb.HandshakeMode)(&handshakeMode) } if option.TrafficPattern != "" { trafficPattern, _ := mierutp.Decode(option.TrafficPattern) config.Profile.TrafficPattern = trafficPattern } return config, nil } func validateMieruOption(option MieruOption) error { if option.Name == "" { return fmt.Errorf("name is empty") } if option.Server == "" { return fmt.Errorf("server is empty") } if option.Port == 0 && option.PortRange == "" { return fmt.Errorf("either port or port-range must be set") } if option.Port != 0 && option.PortRange != "" { return fmt.Errorf("port and port-range cannot be set at the same time") } if option.Port != 0 && (option.Port < 1 || option.Port > 65535) { return fmt.Errorf("port must be between 1 and 65535") } if option.PortRange != "" { begin, end, err := beginAndEndPortFromPortRange(option.PortRange) if err != nil { return fmt.Errorf("invalid port-range format") } if begin < 1 || begin > 65535 { return fmt.Errorf("begin port must be between 1 and 65535") } if end < 1 || end > 65535 { return fmt.Errorf("end port must be between 1 and 65535") } if begin > end { return fmt.Errorf("begin port must be less than or equal to end port") } } if option.Transport != "TCP" && option.Transport != "UDP" { return fmt.Errorf("transport must be TCP or UDP") } if option.UserName == "" { return fmt.Errorf("username is empty") } if option.Password == "" { return fmt.Errorf("password is empty") } if option.Multiplexing != "" { if _, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; !ok { return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing) } } if option.HandshakeMode != "" { if _, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; !ok { return fmt.Errorf("invalid handshake mode: %s", option.HandshakeMode) } } if option.TrafficPattern != "" { trafficPattern, err := mierutp.Decode(option.TrafficPattern) if err != nil { return fmt.Errorf("failed to decode traffic pattern %q: %w", option.TrafficPattern, err) } if err := mierutp.Validate(trafficPattern); err != nil { return fmt.Errorf("invalid traffic pattern %q: %w", option.TrafficPattern, err) } } return nil } func beginAndEndPortFromPortRange(portRange string) (int, int, error) { var begin, end int _, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end) return begin, end, err } ================================================ FILE: core/Clash.Meta/adapter/outbound/mieru_test.go ================================================ package outbound import "testing" func TestNewMieru(t *testing.T) { testCases := []struct { option MieruOption wantBaseAddr string }{ { option: MieruOption{ Name: "test", Server: "1.2.3.4", Port: 10000, Transport: "TCP", UserName: "test", Password: "test", }, wantBaseAddr: "1.2.3.4:10000", }, { option: MieruOption{ Name: "test", Server: "2001:db8::1", PortRange: "10001-10002", Transport: "TCP", UserName: "test", Password: "test", }, wantBaseAddr: "[2001:db8::1]:10001", }, { option: MieruOption{ Name: "test", Server: "example.com", Port: 10003, Transport: "UDP", UserName: "test", Password: "test", TrafficPattern: "GgQIARAK", }, wantBaseAddr: "example.com:10003", }, } for _, testCase := range testCases { mieru, err := NewMieru(testCase.option) if err != nil { t.Error(err) } if mieru.addr != testCase.wantBaseAddr { t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr) } } } func TestBeginAndEndPortFromPortRange(t *testing.T) { testCases := []struct { input string begin int end int hasErr bool }{ {"1-10", 1, 10, false}, {"1000-2000", 1000, 2000, false}, {"65535-65535", 65535, 65535, false}, {"1", 0, 0, true}, {"1-", 0, 0, true}, {"-10", 0, 0, true}, {"a-b", 0, 0, true}, {"1-b", 0, 0, true}, {"a-10", 0, 0, true}, } for _, testCase := range testCases { begin, end, err := beginAndEndPortFromPortRange(testCase.input) if testCase.hasErr { if err == nil { t.Errorf("beginAndEndPortFromPortRange(%s) should return an error", testCase.input) } } else { if err != nil { t.Errorf("beginAndEndPortFromPortRange(%s) should not return an error, but got %v", testCase.input, err) } if begin != testCase.begin { t.Errorf("beginAndEndPortFromPortRange(%s) begin port mismatch, got %d, want %d", testCase.input, begin, testCase.begin) } if end != testCase.end { t.Errorf("beginAndEndPortFromPortRange(%s) end port mismatch, got %d, want %d", testCase.input, end, testCase.end) } } } } ================================================ FILE: core/Clash.Meta/adapter/outbound/reality.go ================================================ package outbound import ( "crypto/ecdh" "encoding/base64" "encoding/hex" "errors" "fmt" tlsC "github.com/metacubex/mihomo/component/tls" ) type RealityOptions struct { PublicKey string `proxy:"public-key"` ShortID string `proxy:"short-id,omitempty"` SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768,omitempty"` } func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) { if o.PublicKey != "" { config := new(tlsC.RealityConfig) config.SupportX25519MLKEM768 = o.SupportX25519MLKEM768 const x25519ScalarSize = 32 publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey) if err != nil || len(publicKey) != x25519ScalarSize { return nil, errors.New("invalid REALITY public key") } config.PublicKey, err = ecdh.X25519().NewPublicKey(publicKey) if err != nil { return nil, fmt.Errorf("fail to create REALITY public key: %w", err) } n := hex.DecodedLen(len(o.ShortID)) if n > tlsC.RealityMaxShortIDLen { return nil, errors.New("invalid REALITY short id") } n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID)) if err != nil || n > tlsC.RealityMaxShortIDLen { return nil, errors.New("invalid REALITY short ID") } return config, nil } return nil, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/reject.go ================================================ package outbound import ( "context" "io" "net" "net/netip" "time" "github.com/metacubex/mihomo/common/buf" C "github.com/metacubex/mihomo/constant" ) type Reject struct { *Base drop bool } type RejectOption struct { BasicOption Name string `proxy:"name"` } // DialContext implements C.ProxyAdapter func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { if r.drop { return NewConn(dropConn{}, r), nil } return NewConn(nopConn{}, r), nil } // ListenPacketContext implements C.ProxyAdapter func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { if err := r.ResolveUDP(ctx, metadata); err != nil { return nil, err } return newPacketConn(&nopPacketConn{}, r), nil } func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error { if !metadata.Resolved() { metadata.DstIP = netip.IPv4Unspecified() } return nil } func NewRejectWithOption(option RejectOption) *Reject { return &Reject{ Base: NewBase(BaseOption{ Name: option.Name, Type: C.Reject, UDP: true, }), } } func NewReject() *Reject { return &Reject{ Base: NewBase(BaseOption{ Name: "REJECT", Type: C.Reject, UDP: true, Prefer: C.DualStack, }), } } func NewRejectDrop() *Reject { return &Reject{ Base: NewBase(BaseOption{ Name: "REJECT-DROP", Type: C.RejectDrop, UDP: true, Prefer: C.DualStack, }), drop: true, } } func NewPass() *Reject { return &Reject{ Base: &Base{ name: "PASS", tp: C.Pass, udp: true, prefer: C.DualStack, }, } } type nopConn struct{} func (rw nopConn) Read(b []byte) (int, error) { return 0, io.EOF } func (rw nopConn) ReadBuffer(buffer *buf.Buffer) error { return io.EOF } func (rw nopConn) Write(b []byte) (int, error) { return 0, io.EOF } func (rw nopConn) WriteBuffer(buffer *buf.Buffer) error { return io.EOF } func (rw nopConn) Close() error { return nil } func (rw nopConn) LocalAddr() net.Addr { return nil } func (rw nopConn) RemoteAddr() net.Addr { return nil } func (rw nopConn) SetDeadline(time.Time) error { return nil } func (rw nopConn) SetReadDeadline(time.Time) error { return nil } func (rw nopConn) SetWriteDeadline(time.Time) error { return nil } var udpAddrIPv4Unspecified = &net.UDPAddr{IP: net.IPv4zero, Port: 0} type nopPacketConn struct{} func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil } func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF } func (npc nopPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) { return nil, nil, nil, io.EOF } func (npc nopPacketConn) Close() error { return nil } func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified } func (npc nopPacketConn) SetDeadline(time.Time) error { return nil } func (npc nopPacketConn) SetReadDeadline(time.Time) error { return nil } func (npc nopPacketConn) SetWriteDeadline(time.Time) error { return nil } type dropConn struct{} func (rw dropConn) Read(b []byte) (int, error) { return 0, io.EOF } func (rw dropConn) ReadBuffer(buffer *buf.Buffer) error { time.Sleep(C.DefaultDropTime) return io.EOF } func (rw dropConn) Write(b []byte) (int, error) { return 0, io.EOF } func (rw dropConn) WriteBuffer(buffer *buf.Buffer) error { return io.EOF } func (rw dropConn) Close() error { return nil } func (rw dropConn) LocalAddr() net.Addr { return nil } func (rw dropConn) RemoteAddr() net.Addr { return nil } func (rw dropConn) SetDeadline(time.Time) error { return nil } func (rw dropConn) SetReadDeadline(time.Time) error { return nil } func (rw dropConn) SetWriteDeadline(time.Time) error { return nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/shadowsocks.go ================================================ package outbound import ( "context" "fmt" "net" "strconv" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/structure" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/ntp" gost "github.com/metacubex/mihomo/transport/gost-plugin" "github.com/metacubex/mihomo/transport/kcptun" "github.com/metacubex/mihomo/transport/restls" obfs "github.com/metacubex/mihomo/transport/simple-obfs" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" v2rayObfs "github.com/metacubex/mihomo/transport/v2ray-plugin" shadowsocks "github.com/metacubex/sing-shadowsocks2" "github.com/metacubex/sing/common/bufio" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/uot" ) type ShadowSocks struct { *Base method shadowsocks.Method option *ShadowSocksOption // obfs obfsMode string obfsOption *simpleObfsOption v2rayOption *v2rayObfs.Option gostOption *gost.Option shadowTLSOption *shadowtls.ShadowTLSOption restlsConfig *restls.Config kcptunClient *kcptun.Client } type ShadowSocksOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Password string `proxy:"password"` Cipher string `proxy:"cipher"` UDP bool `proxy:"udp,omitempty"` Plugin string `proxy:"plugin,omitempty"` PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"` UDPOverTCPVersion int `proxy:"udp-over-tcp-version,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } type simpleObfsOption struct { Mode string `obfs:"mode,omitempty"` Host string `obfs:"host,omitempty"` } type v2rayObfsOption struct { Mode string `obfs:"mode"` Host string `obfs:"host,omitempty"` Path string `obfs:"path,omitempty"` TLS bool `obfs:"tls,omitempty"` ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` Certificate string `obfs:"certificate,omitempty"` PrivateKey string `obfs:"private-key,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Mux bool `obfs:"mux,omitempty"` V2rayHttpUpgrade bool `obfs:"v2ray-http-upgrade,omitempty"` V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"` } type gostObfsOption struct { Mode string `obfs:"mode"` Host string `obfs:"host,omitempty"` Path string `obfs:"path,omitempty"` TLS bool `obfs:"tls,omitempty"` ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` Certificate string `obfs:"certificate,omitempty"` PrivateKey string `obfs:"private-key,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Mux bool `obfs:"mux,omitempty"` } type shadowTLSOption struct { Password string `obfs:"password,omitempty"` Host string `obfs:"host"` Fingerprint string `obfs:"fingerprint,omitempty"` Certificate string `obfs:"certificate,omitempty"` PrivateKey string `obfs:"private-key,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Version int `obfs:"version,omitempty"` ALPN []string `obfs:"alpn,omitempty"` } type restlsOption struct { Password string `obfs:"password"` Host string `obfs:"host"` VersionHint string `obfs:"version-hint"` RestlsScript string `obfs:"restls-script,omitempty"` } type kcpTunOption struct { Key string `obfs:"key,omitempty"` Crypt string `obfs:"crypt,omitempty"` Mode string `obfs:"mode,omitempty"` Conn int `obfs:"conn,omitempty"` AutoExpire int `obfs:"autoexpire,omitempty"` ScavengeTTL int `obfs:"scavengettl,omitempty"` MTU int `obfs:"mtu,omitempty"` RateLimit int `obfs:"ratelimit,omitempty"` SndWnd int `obfs:"sndwnd,omitempty"` RcvWnd int `obfs:"rcvwnd,omitempty"` DataShard int `obfs:"datashard,omitempty"` ParityShard int `obfs:"parityshard,omitempty"` DSCP int `obfs:"dscp,omitempty"` NoComp bool `obfs:"nocomp,omitempty"` AckNodelay bool `obfs:"acknodelay,omitempty"` NoDelay int `obfs:"nodelay,omitempty"` Interval int `obfs:"interval,omitempty"` Resend int `obfs:"resend,omitempty"` NoCongestion int `obfs:"nc,omitempty"` SockBuf int `obfs:"sockbuf,omitempty"` SmuxVer int `obfs:"smuxver,omitempty"` SmuxBuf int `obfs:"smuxbuf,omitempty"` FrameSize int `obfs:"framesize,omitempty"` StreamBuf int `obfs:"streambuf,omitempty"` KeepAlive int `obfs:"keepalive,omitempty"` } // StreamConnContext implements C.ProxyAdapter func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { useEarly := false switch ss.obfsMode { case "tls": c = obfs.NewTLSObfs(c, ss.obfsOption.Host) case "http": _, port, _ := net.SplitHostPort(ss.addr) c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) case "websocket": if ss.v2rayOption != nil { c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) } else if ss.gostOption != nil { c, err = gost.NewGostWebsocket(ctx, c, ss.gostOption) } else { return nil, fmt.Errorf("plugin options is required") } if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } case shadowtls.Mode: c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption) if err != nil { return nil, err } useEarly = true case restls.Mode: c, err = restls.NewRestls(ctx, c, ss.restlsConfig) if err != nil { return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err) } useEarly = true } useEarly = useEarly || N.NeedHandshake(c) if !useEarly { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } } if metadata.NetWork == C.UDP && ss.option.UDPOverTCP { uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion)) if useEarly { return ss.method.DialEarlyConn(c, uotDestination), nil } else { return ss.method.DialConn(c, uotDestination) } } if useEarly { return ss.method.DialEarlyConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)), nil } else { return ss.method.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) } } func (ss *ShadowSocks) dialContext(ctx context.Context) (c net.Conn, err error) { if ss.kcptunClient != nil { return ss.kcptunClient.OpenStream(ctx, ss.listenPacketContext) } return ss.dialer.DialContext(ctx, "tcp", ss.addr) } // DialContext implements C.ProxyAdapter func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := ss.dialContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = ss.StreamConnContext(ctx, c, metadata) return NewConn(c, ss), err } func (ss *ShadowSocks) listenPacketContext(ctx context.Context) (net.PacketConn, net.Addr, error) { addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer) if err != nil { return nil, nil, err } pc, err := ss.dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) if err != nil { return nil, nil, err } return pc, addr, nil } // ListenPacketContext implements C.ProxyAdapter func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if ss.option.UDPOverTCP { var c net.Conn c, err = ss.DialContext(ctx, metadata) if err != nil { return nil, err } defer func(c net.Conn) { safeConnClose(c, err) }(c) if err = ss.ResolveUDP(ctx, metadata); err != nil { return nil, err } destination := M.SocksaddrFromNet(metadata.UDPAddr()) if ss.option.UDPOverTCPVersion == uot.LegacyVersion { return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil } else { return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), ss), nil } } if err := ss.ResolveUDP(ctx, metadata); err != nil { return nil, err } pc, addr, err := ss.listenPacketContext(ctx) if err != nil { return nil, err } pc = ss.method.DialPacketConn(bufio.NewBindPacketConn(pc, addr)) return newPacketConn(pc, ss), nil } // ProxyInfo implements C.ProxyAdapter func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo { info := ss.Base.ProxyInfo() info.DialerProxy = ss.option.DialerProxy return info } // SupportUOT implements C.ProxyAdapter func (ss *ShadowSocks) SupportUOT() bool { return ss.option.UDPOverTCP } func (ss *ShadowSocks) Close() error { if ss.kcptunClient != nil { return ss.kcptunClient.Close() } return nil } func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) method, err := shadowsocks.CreateMethod(option.Cipher, shadowsocks.MethodOptions{ Password: option.Password, TimeFunc: ntp.Now, }) if err != nil { return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) } var v2rayOption *v2rayObfs.Option var gostOption *gost.Option var obfsOption *simpleObfsOption var shadowTLSOpt *shadowtls.ShadowTLSOption var restlsConfig *restls.Config var kcptunClient *kcptun.Client obfsMode := "" decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) if option.Plugin == "obfs" { opts := simpleObfsOption{Host: "bing.com"} if err := decoder.Decode(option.PluginOpts, &opts); err != nil { return nil, fmt.Errorf("ss %s initialize obfs error: %w", addr, err) } if opts.Mode != "tls" && opts.Mode != "http" { return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) } obfsMode = opts.Mode obfsOption = &opts } else if option.Plugin == "v2ray-plugin" { opts := v2rayObfsOption{Host: "bing.com", Mux: true} if err := decoder.Decode(option.PluginOpts, &opts); err != nil { return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err) } if opts.Mode != "websocket" { return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) } obfsMode = opts.Mode v2rayOption = &v2rayObfs.Option{ Host: opts.Host, Path: opts.Path, Headers: opts.Headers, Mux: opts.Mux, V2rayHttpUpgrade: opts.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: opts.V2rayHttpUpgradeFastOpen, } if opts.TLS { v2rayOption.TLS = true v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.Fingerprint = opts.Fingerprint v2rayOption.Certificate = opts.Certificate v2rayOption.PrivateKey = opts.PrivateKey echConfig, err := opts.ECHOpts.Parse() if err != nil { return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err) } v2rayOption.ECHConfig = echConfig } } else if option.Plugin == "gost-plugin" { opts := gostObfsOption{Host: "bing.com", Mux: true} if err := decoder.Decode(option.PluginOpts, &opts); err != nil { return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err) } if opts.Mode != "websocket" { return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) } obfsMode = opts.Mode gostOption = &gost.Option{ Host: opts.Host, Path: opts.Path, Headers: opts.Headers, Mux: opts.Mux, } if opts.TLS { gostOption.TLS = true gostOption.SkipCertVerify = opts.SkipCertVerify gostOption.Fingerprint = opts.Fingerprint gostOption.Certificate = opts.Certificate gostOption.PrivateKey = opts.PrivateKey echConfig, err := opts.ECHOpts.Parse() if err != nil { return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err) } gostOption.ECHConfig = echConfig } } else if option.Plugin == shadowtls.Mode { obfsMode = shadowtls.Mode opt := &shadowTLSOption{ Version: 2, } if err := decoder.Decode(option.PluginOpts, opt); err != nil { return nil, fmt.Errorf("ss %s initialize shadow-tls-plugin error: %w", addr, err) } shadowTLSOpt = &shadowtls.ShadowTLSOption{ Password: opt.Password, Host: opt.Host, Fingerprint: opt.Fingerprint, Certificate: opt.Certificate, PrivateKey: opt.PrivateKey, ClientFingerprint: option.ClientFingerprint, SkipCertVerify: opt.SkipCertVerify, Version: opt.Version, } if opt.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array shadowTLSOpt.ALPN = opt.ALPN } else { shadowTLSOpt.ALPN = shadowtls.DefaultALPN } } else if option.Plugin == restls.Mode { obfsMode = restls.Mode restlsOpt := &restlsOption{} if err := decoder.Decode(option.PluginOpts, restlsOpt); err != nil { return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) } restlsConfig, err = restls.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint) if err != nil { return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) } } else if option.Plugin == kcptun.Mode { obfsMode = kcptun.Mode kcptunOpt := &kcpTunOption{} if err := decoder.Decode(option.PluginOpts, kcptunOpt); err != nil { return nil, fmt.Errorf("ss %s initialize kcptun-plugin error: %w", addr, err) } kcptunClient = kcptun.NewClient(kcptun.Config{ Key: kcptunOpt.Key, Crypt: kcptunOpt.Crypt, Mode: kcptunOpt.Mode, Conn: kcptunOpt.Conn, AutoExpire: kcptunOpt.AutoExpire, ScavengeTTL: kcptunOpt.ScavengeTTL, MTU: kcptunOpt.MTU, RateLimit: kcptunOpt.RateLimit, SndWnd: kcptunOpt.SndWnd, RcvWnd: kcptunOpt.RcvWnd, DataShard: kcptunOpt.DataShard, ParityShard: kcptunOpt.ParityShard, DSCP: kcptunOpt.DSCP, NoComp: kcptunOpt.NoComp, AckNodelay: kcptunOpt.AckNodelay, NoDelay: kcptunOpt.NoDelay, Interval: kcptunOpt.Interval, Resend: kcptunOpt.Resend, NoCongestion: kcptunOpt.NoCongestion, SockBuf: kcptunOpt.SockBuf, SmuxVer: kcptunOpt.SmuxVer, SmuxBuf: kcptunOpt.SmuxBuf, FrameSize: kcptunOpt.FrameSize, StreamBuf: kcptunOpt.StreamBuf, KeepAlive: kcptunOpt.KeepAlive, }) option.UDPOverTCP = true // must open uot } switch option.UDPOverTCPVersion { case uot.Version, uot.LegacyVersion: case 0: option.UDPOverTCPVersion = uot.LegacyVersion default: return nil, fmt.Errorf("ss %s unknown udp over tcp protocol version: %d", addr, option.UDPOverTCPVersion) } outbound := &ShadowSocks{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Shadowsocks, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), method: method, option: &option, obfsMode: obfsMode, v2rayOption: v2rayOption, gostOption: gostOption, obfsOption: obfsOption, shadowTLSOption: shadowTLSOpt, restlsConfig: restlsConfig, kcptunClient: kcptunClient, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/shadowsocksr.go ================================================ package outbound import ( "context" "errors" "fmt" "net" "strconv" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" "github.com/metacubex/mihomo/transport/shadowsocks/shadowstream" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/ssr/obfs" "github.com/metacubex/mihomo/transport/ssr/protocol" ) type ShadowSocksR struct { *Base option *ShadowSocksROption cipher core.Cipher obfs obfs.Obfs protocol protocol.Protocol } type ShadowSocksROption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Password string `proxy:"password"` Cipher string `proxy:"cipher"` Obfs string `proxy:"obfs"` ObfsParam string `proxy:"obfs-param,omitempty"` Protocol string `proxy:"protocol"` ProtocolParam string `proxy:"protocol-param,omitempty"` UDP bool `proxy:"udp,omitempty"` } // StreamConnContext implements C.ProxyAdapter func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } c = ssr.obfs.StreamConn(c) c = ssr.cipher.StreamConn(c) var ( iv []byte ) switch conn := c.(type) { case *shadowstream.Conn: iv, err = conn.ObtainWriteIV() if err != nil { return nil, err } case *shadowaead.Conn: return nil, fmt.Errorf("invalid connection type") } c = ssr.protocol.StreamConn(c, iv) _, err = c.Write(serializesSocksAddr(metadata)) return c, err } // DialContext implements C.ProxyAdapter func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := ssr.dialer.DialContext(ctx, "tcp", ssr.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = ssr.StreamConnContext(ctx, c, metadata) return NewConn(c, ssr), err } // ListenPacketContext implements C.ProxyAdapter func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { if err := ssr.ResolveUDP(ctx, metadata); err != nil { return nil, err } addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer) if err != nil { return nil, err } pc, err := ssr.dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) if err != nil { return nil, err } epc := ssr.cipher.PacketConn(N.NewEnhancePacketConn(pc)) epc = ssr.protocol.PacketConn(epc) return newPacketConn(&ssrPacketConn{EnhancePacketConn: epc, rAddr: addr}, ssr), nil } // ProxyInfo implements C.ProxyAdapter func (ssr *ShadowSocksR) ProxyInfo() C.ProxyInfo { info := ssr.Base.ProxyInfo() info.DialerProxy = ssr.option.DialerProxy return info } func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { // SSR protocol compatibility // https://github.com/metacubex/mihomo/pull/2056 if option.Cipher == "none" { option.Cipher = "dummy" } addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) cipher := option.Cipher password := option.Password coreCiph, err := core.PickCipher(cipher, nil, password) if err != nil { return nil, fmt.Errorf("ssr %s cipher: %s initialize error: %w", addr, cipher, err) } var ( ivSize int key []byte ) if option.Cipher == "dummy" { ivSize = 0 key = core.Kdf(option.Password, 16) } else { ciph, ok := coreCiph.(*core.StreamCipher) if !ok { return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher) } ivSize = ciph.IVSize() key = ciph.Key } obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{ Host: option.Server, Port: option.Port, Key: key, IVSize: ivSize, Param: option.ObfsParam, }) if err != nil { return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err) } protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{ Key: key, Overhead: obfsOverhead, Param: option.ProtocolParam, }) if err != nil { return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err) } outbound := &ShadowSocksR{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.ShadowsocksR, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, cipher: coreCiph, obfs: obfs, protocol: protocol, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } type ssrPacketConn struct { N.EnhancePacketConn rAddr net.Addr } func (spc *ssrPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) if err != nil { return } return spc.EnhancePacketConn.WriteTo(packet[3:], spc.rAddr) } func (spc *ssrPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { n, _, e := spc.EnhancePacketConn.ReadFrom(b) if e != nil { return 0, nil, e } addr := socks5.SplitAddr(b[:n]) if addr == nil { return 0, nil, errors.New("parse addr error") } udpAddr := addr.UDPAddr() if udpAddr == nil { return 0, nil, errors.New("parse addr error") } copy(b, b[len(addr):]) return n - len(addr), udpAddr, e } func (spc *ssrPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { data, put, _, err = spc.EnhancePacketConn.WaitReadFrom() if err != nil { return nil, nil, nil, err } _addr := socks5.SplitAddr(data) if _addr == nil { if put != nil { put() } return nil, nil, nil, errors.New("parse addr error") } udpAddr := _addr.UDPAddr() if udpAddr == nil { if put != nil { put() } return nil, nil, nil, errors.New("parse addr error") } addr = udpAddr data = data[len(_addr):] return } ================================================ FILE: core/Clash.Meta/adapter/outbound/singmux.go ================================================ package outbound import ( "context" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" mux "github.com/metacubex/sing-mux" E "github.com/metacubex/sing/common/exceptions" M "github.com/metacubex/sing/common/metadata" ) type SingMux struct { ProxyAdapter client *mux.Client onlyTcp bool } type SingMuxOption struct { Enabled bool `proxy:"enabled,omitempty"` Protocol string `proxy:"protocol,omitempty"` MaxConnections int `proxy:"max-connections,omitempty"` MinStreams int `proxy:"min-streams,omitempty"` MaxStreams int `proxy:"max-streams,omitempty"` Padding bool `proxy:"padding,omitempty"` Statistic bool `proxy:"statistic,omitempty"` OnlyTcp bool `proxy:"only-tcp,omitempty"` BrutalOpts BrutalOption `proxy:"brutal-opts,omitempty"` } type BrutalOption struct { Enabled bool `proxy:"enabled,omitempty"` Up string `proxy:"up,omitempty"` Down string `proxy:"down,omitempty"` } func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) if err != nil { return nil, err } return NewConn(c, s), err } func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if s.onlyTcp { return s.ProxyAdapter.ListenPacketContext(ctx, metadata) } if err = s.ProxyAdapter.ResolveUDP(ctx, metadata); err != nil { return nil, err } pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr())) if err != nil { return nil, err } if pc == nil { return nil, E.New("packetConn is nil") } return newPacketConn(N.NewThreadSafePacketConn(pc), s), nil } func (s *SingMux) SupportUDP() bool { if s.onlyTcp { return s.ProxyAdapter.SupportUDP() } return true } func (s *SingMux) SupportUOT() bool { if s.onlyTcp { return s.ProxyAdapter.SupportUOT() } return true } func (s *SingMux) ProxyInfo() C.ProxyInfo { info := s.ProxyAdapter.ProxyInfo() info.SMUX = true return info } // Close implements C.ProxyAdapter func (s *SingMux) Close() error { if s.client != nil { _ = s.client.Close() } return s.ProxyAdapter.Close() } func NewSingMux(option SingMuxOption, proxy ProxyAdapter) (ProxyAdapter, error) { // TODO // "TCP Brutal is only supported on Linux-based systems" singDialer := proxydialer.NewSingDialer(proxydialer.New(proxy, option.Statistic)) client, err := mux.NewClient(mux.Options{ Dialer: singDialer, Logger: log.SingLogger, Protocol: option.Protocol, MaxConnections: option.MaxConnections, MinStreams: option.MinStreams, MaxStreams: option.MaxStreams, Padding: option.Padding, TCPTimeout: C.DefaultTCPTimeout, Brutal: mux.BrutalOptions{ Enabled: option.BrutalOpts.Enabled, SendBPS: StringToBps(option.BrutalOpts.Up), ReceiveBPS: StringToBps(option.BrutalOpts.Down), }, }) if err != nil { return nil, err } outbound := &SingMux{ ProxyAdapter: proxy, client: client, onlyTcp: option.OnlyTcp, } return outbound, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/snell.go ================================================ package outbound import ( "context" "fmt" "net" "strconv" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/structure" C "github.com/metacubex/mihomo/constant" obfs "github.com/metacubex/mihomo/transport/simple-obfs" "github.com/metacubex/mihomo/transport/snell" ) type Snell struct { *Base option *SnellOption psk []byte pool *snell.Pool obfsOption *simpleObfsOption version int } type SnellOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Psk string `proxy:"psk"` UDP bool `proxy:"udp,omitempty"` Version int `proxy:"version,omitempty"` ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"` } type streamOption struct { psk []byte version int addr string obfsOption *simpleObfsOption } func snellStreamConn(c net.Conn, option streamOption) *snell.Snell { switch option.obfsOption.Mode { case "tls": c = obfs.NewTLSObfs(c, option.obfsOption.Host) case "http": _, port, _ := net.SplitHostPort(option.addr) c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port) } return snell.StreamConn(c, option.psk, option.version) } // StreamConnContext implements C.ProxyAdapter func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { c = snellStreamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) err := s.writeHeaderContext(ctx, c, metadata) return c, err } func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } if metadata.NetWork == C.UDP { err = snell.WriteUDPHeader(c, s.version) return } err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version) return } // DialContext implements C.ProxyAdapter func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { if s.version == snell.Version2 { c, err := s.pool.Get() if err != nil { return nil, err } if err = s.writeHeaderContext(ctx, c, metadata); err != nil { _ = c.Close() return nil, err } return NewConn(c, s), err } c, err := s.dialer.DialContext(ctx, "tcp", s.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", s.addr, err) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = s.StreamConnContext(ctx, c, metadata) return NewConn(c, s), err } // ListenPacketContext implements C.ProxyAdapter func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = s.ResolveUDP(ctx, metadata); err != nil { return nil, err } c, err := s.dialer.DialContext(ctx, "tcp", s.addr) if err != nil { return nil, err } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = s.StreamConnContext(ctx, c, metadata) pc := snell.PacketConn(c) return newPacketConn(pc, s), nil } // SupportUOT implements C.ProxyAdapter func (s *Snell) SupportUOT() bool { return true } // ProxyInfo implements C.ProxyAdapter func (s *Snell) ProxyInfo() C.ProxyInfo { info := s.Base.ProxyInfo() info.DialerProxy = s.option.DialerProxy return info } func NewSnell(option SnellOption) (*Snell, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) psk := []byte(option.Psk) decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) obfsOption := &simpleObfsOption{Host: "bing.com"} if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil { return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err) } switch obfsOption.Mode { case "tls", "http", "": break default: return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode) } // backward compatible if option.Version == 0 { option.Version = snell.DefaultSnellVersion } switch option.Version { case snell.Version1, snell.Version2: if option.UDP { return nil, fmt.Errorf("snell version %d not support UDP", option.Version) } case snell.Version3: default: return nil, fmt.Errorf("snell version error: %d", option.Version) } s := &Snell{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Snell, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, psk: psk, obfsOption: obfsOption, version: option.Version, } s.dialer = option.NewDialer(s.DialOptions()) if option.Version == snell.Version2 { s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { c, err := s.dialer.DialContext(ctx, "tcp", addr) if err != nil { return nil, err } return snellStreamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil }) } return s, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/socks5.go ================================================ package outbound import ( "context" "errors" "fmt" "io" "net" "net/netip" "strconv" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/tls" ) type Socks5 struct { *Base option *Socks5Option user string pass string tls bool skipCertVerify bool tlsConfig *tls.Config } type Socks5Option struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` UserName string `proxy:"username,omitempty"` Password string `proxy:"password,omitempty"` TLS bool `proxy:"tls,omitempty"` UDP bool `proxy:"udp,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` } // StreamConnContext implements C.ProxyAdapter func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { if ss.tls { cc := tls.Client(c, ss.tlsConfig) err := cc.HandshakeContext(ctx) c = cc if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } } var user *socks5.User if ss.user != "" { user = &socks5.User{ Username: ss.user, Password: ss.pass, } } if _, err := ss.clientHandshakeContext(ctx, c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil { return nil, err } return c, nil } // DialContext implements C.ProxyAdapter func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := ss.dialer.DialContext(ctx, "tcp", ss.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = ss.StreamConnContext(ctx, c, metadata) if err != nil { return nil, err } return NewConn(c, ss), nil } // ListenPacketContext implements C.ProxyAdapter func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = ss.ResolveUDP(ctx, metadata); err != nil { return nil, err } c, err := ss.dialer.DialContext(ctx, "tcp", ss.addr) if err != nil { err = fmt.Errorf("%s connect error: %w", ss.addr, err) return } defer func(c net.Conn) { safeConnClose(c, err) }(c) if ss.tls { cc := tls.Client(c, ss.tlsConfig) err = cc.HandshakeContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } c = cc } var user *socks5.User if ss.user != "" { user = &socks5.User{ Username: ss.user, Password: ss.pass, } } udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) bindAddr, err := ss.clientHandshakeContext(ctx, c, udpAssocateAddr, socks5.CmdUDPAssociate, user) if err != nil { err = fmt.Errorf("client hanshake error: %w", err) return } // Support unspecified UDP bind address. bindUDPAddr := bindAddr.UDPAddr() if bindUDPAddr == nil { err = errors.New("invalid UDP bind address") return } else if bindUDPAddr.IP.IsUnspecified() { serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr(), C.IPv4Prefer) if err != nil { return nil, err } bindUDPAddr.IP = serverAddr.IP } pc, err := ss.dialer.ListenPacket(ctx, "udp", "", bindUDPAddr.AddrPort()) if err != nil { return } go func() { io.Copy(io.Discard, c) c.Close() // A UDP association terminates when the TCP connection that the UDP // ASSOCIATE request arrived on terminates. RFC1928 pc.Close() }() return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil } // ProxyInfo implements C.ProxyAdapter func (ss *Socks5) ProxyInfo() C.ProxyInfo { info := ss.Base.ProxyInfo() info.DialerProxy = ss.option.DialerProxy return info } func (ss *Socks5) clientHandshakeContext(ctx context.Context, c net.Conn, addr socks5.Addr, command socks5.Command, user *socks5.User) (_ socks5.Addr, err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } return socks5.ClientHandshake(c, addr, command, user) } func NewSocks5(option Socks5Option) (*Socks5, error) { var tlsConfig *tls.Config if option.TLS { var err error tlsConfig, err = ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ InsecureSkipVerify: option.SkipCertVerify, ServerName: option.Server, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } } outbound := &Socks5{ Base: NewBase(BaseOption{ Name: option.Name, Addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), Type: C.Socks5, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, user: option.UserName, pass: option.Password, tls: option.TLS, skipCertVerify: option.SkipCertVerify, tlsConfig: tlsConfig, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } type socksPacketConn struct { net.PacketConn rAddr net.Addr tcpConn net.Conn } func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) if err != nil { return } return uc.PacketConn.WriteTo(packet, uc.rAddr) } func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { n, _, e := uc.PacketConn.ReadFrom(b) if e != nil { return 0, nil, e } addr, payload, err := socks5.DecodeUDPPacket(b) if err != nil { return 0, nil, err } udpAddr := addr.UDPAddr() if udpAddr == nil { return 0, nil, errors.New("parse udp addr error") } // due to DecodeUDPPacket is mutable, record addr length copy(b, payload) return n - len(addr) - 3, udpAddr, nil } func (uc *socksPacketConn) Close() error { uc.tcpConn.Close() return uc.PacketConn.Close() } ================================================ FILE: core/Clash.Meta/adapter/outbound/ssh.go ================================================ package outbound import ( "bytes" "context" "encoding/base64" "fmt" "net" "os" "strconv" "strings" "sync" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/randv2" "github.com/metacubex/ssh" ) type Ssh struct { *Base option *SshOption config *ssh.ClientConfig client *ssh.Client cMutex sync.Mutex } type SshOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` UserName string `proxy:"username"` Password string `proxy:"password,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` PrivateKeyPassphrase string `proxy:"private-key-passphrase,omitempty"` HostKey []string `proxy:"host-key,omitempty"` HostKeyAlgorithms []string `proxy:"host-key-algorithms,omitempty"` } func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { client, err := s.connect(ctx, s.addr) if err != nil { return nil, err } c, err := client.DialContext(ctx, "tcp", metadata.RemoteAddress()) if err != nil { return nil, err } return NewConn(c, s), nil } func (s *Ssh) connect(ctx context.Context, addr string) (client *ssh.Client, err error) { s.cMutex.Lock() defer s.cMutex.Unlock() if s.client != nil { return s.client, nil } c, err := s.dialer.DialContext(ctx, "tcp", addr) if err != nil { return nil, err } defer func(c net.Conn) { safeConnClose(c, err) }(c) if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } clientConn, chans, reqs, err := ssh.NewClientConn(c, addr, s.config) if err != nil { return nil, err } client = ssh.NewClient(clientConn, chans, reqs) s.client = client go func() { _ = client.Wait() // wait shutdown _ = client.Close() s.cMutex.Lock() defer s.cMutex.Unlock() if s.client == client { s.client = nil } }() return client, nil } // ProxyInfo implements C.ProxyAdapter func (s *Ssh) ProxyInfo() C.ProxyInfo { info := s.Base.ProxyInfo() info.DialerProxy = s.option.DialerProxy return info } // Close implements C.ProxyAdapter func (s *Ssh) Close() error { s.cMutex.Lock() defer s.cMutex.Unlock() if s.client != nil { return s.client.Close() } return nil } func NewSsh(option SshOption) (*Ssh, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) config := ssh.ClientConfig{ User: option.UserName, HostKeyCallback: ssh.InsecureIgnoreHostKey(), HostKeyAlgorithms: option.HostKeyAlgorithms, } if option.PrivateKey != "" { var b []byte var err error if strings.Contains(option.PrivateKey, "PRIVATE KEY") { b = []byte(option.PrivateKey) } else { path := C.Path.Resolve(option.PrivateKey) if !C.Path.IsSafePath(path) { return nil, C.Path.ErrNotSafePath(path) } b, err = os.ReadFile(path) if err != nil { return nil, err } } var pKey ssh.Signer if option.PrivateKeyPassphrase != "" { pKey, err = ssh.ParsePrivateKeyWithPassphrase(b, []byte(option.PrivateKeyPassphrase)) } else { pKey, err = ssh.ParsePrivateKey(b) } if err != nil { return nil, err } config.Auth = append(config.Auth, ssh.PublicKeys(pKey)) } if option.Password != "" { config.Auth = append(config.Auth, ssh.Password(option.Password)) } if len(option.HostKey) != 0 { keys := make([]ssh.PublicKey, len(option.HostKey)) for i, hostKey := range option.HostKey { key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey)) if err != nil { return nil, fmt.Errorf("parse host key :%s", key) } keys[i] = key } config.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { serverKey := key.Marshal() for _, hostKey := range keys { if bytes.Equal(serverKey, hostKey.Marshal()) { return nil } } return fmt.Errorf("host key mismatch, server send :%s %s", key.Type(), base64.StdEncoding.EncodeToString(serverKey)) } } version := "SSH-2.0-OpenSSH_" if randv2.IntN(2) == 0 { version += "7." + strconv.Itoa(randv2.IntN(10)) } else { version += "8." + strconv.Itoa(randv2.IntN(9)) } config.ClientVersion = version outbound := &Ssh{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Ssh, ProviderName: option.ProviderName, UDP: false, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, config: &config, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/sudoku.go ================================================ package outbound import ( "context" "fmt" "net" "strconv" "strings" "sync" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/sudoku" "github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask" ) type Sudoku struct { *Base option *SudokuOption baseConf sudoku.ProtocolConfig muxMu sync.Mutex muxClient *sudoku.MultiplexClient httpMaskMu sync.Mutex httpMaskClient *httpmask.TunnelClient httpMaskKey string } type SudokuOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Key string `proxy:"key"` AEADMethod string `proxy:"aead-method,omitempty"` PaddingMin *int `proxy:"padding-min,omitempty"` PaddingMax *int `proxy:"padding-max,omitempty"` TableType string `proxy:"table-type,omitempty"` // "prefer_ascii", "prefer_entropy", or directional "up_ascii_down_entropy"/"up_entropy_down_ascii" EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"` HTTPMask *bool `proxy:"http-mask,omitempty"` HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto", "ws" HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port) PathRoot string `proxy:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto" (reuse h1/h2), "on" (single tunnel, multi-target) HTTPMaskOptions *SudokuHTTPMaskOptions `proxy:"httpmask,omitempty"` CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty } type SudokuHTTPMaskOptions struct { Disable bool `proxy:"disable,omitempty"` Mode string `proxy:"mode,omitempty"` TLS bool `proxy:"tls,omitempty"` Host string `proxy:"host,omitempty"` PathRoot string `proxy:"path-root,omitempty"` Multiplex string `proxy:"multiplex,omitempty"` } // DialContext implements C.ProxyAdapter func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { cfg, err := s.buildConfig(metadata) if err != nil { return nil, err } muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex) if muxMode == "on" && !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) { stream, muxErr := s.dialMultiplex(ctx, cfg.TargetAddress) if muxErr == nil { return NewConn(stream, s), nil } return nil, muxErr } c, err := s.dialAndHandshake(ctx, cfg) if err != nil { return nil, err } defer func() { safeConnClose(c, err) }() addrBuf, err := sudoku.EncodeAddress(cfg.TargetAddress) if err != nil { return nil, fmt.Errorf("encode target address failed: %w", err) } if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeOpenTCP, addrBuf); err != nil { return nil, fmt.Errorf("send target address failed: %w", err) } return NewConn(c, s), nil } // ListenPacketContext implements C.ProxyAdapter func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { if err := s.ResolveUDP(ctx, metadata); err != nil { return nil, err } cfg, err := s.buildConfig(metadata) if err != nil { return nil, err } c, err := s.dialAndHandshake(ctx, cfg) if err != nil { return nil, err } if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeStartUoT, nil); err != nil { _ = c.Close() return nil, fmt.Errorf("start uot failed: %w", err) } return newPacketConn(N.NewThreadSafePacketConn(sudoku.NewUoTPacketConn(c)), s), nil } // SupportUOT implements C.ProxyAdapter func (s *Sudoku) SupportUOT() bool { return true } // ProxyInfo implements C.ProxyAdapter func (s *Sudoku) ProxyInfo() C.ProxyInfo { info := s.Base.ProxyInfo() info.DialerProxy = s.option.DialerProxy return info } func (s *Sudoku) buildConfig(metadata *C.Metadata) (*sudoku.ProtocolConfig, error) { if metadata == nil || metadata.DstPort == 0 || !metadata.Valid() { return nil, fmt.Errorf("invalid metadata for sudoku outbound") } cfg := s.baseConf cfg.TargetAddress = metadata.RemoteAddress() if err := cfg.ValidateClient(); err != nil { return nil, err } return &cfg, nil } func NewSudoku(option SudokuOption) (*Sudoku, error) { if option.Server == "" { return nil, fmt.Errorf("server is required") } if option.Port <= 0 || option.Port > 65535 { return nil, fmt.Errorf("invalid port: %d", option.Port) } if option.Key == "" { return nil, fmt.Errorf("key is required") } defaultConf := sudoku.DefaultConfig() tableType, err := sudoku.NormalizeTableType(option.TableType) if err != nil { return nil, err } paddingMin, paddingMax := sudoku.ResolvePadding(option.PaddingMin, option.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax) enablePureDownlink := sudoku.DerefBool(option.EnablePureDownlink, defaultConf.EnablePureDownlink) disableHTTPMask := defaultConf.DisableHTTPMask if option.HTTPMask != nil { disableHTTPMask = !*option.HTTPMask } httpMaskMode := defaultConf.HTTPMaskMode if option.HTTPMaskMode != "" { httpMaskMode = option.HTTPMaskMode } httpMaskTLS := option.HTTPMaskTLS httpMaskHost := option.HTTPMaskHost pathRoot := strings.TrimSpace(option.PathRoot) httpMaskMultiplex := defaultConf.HTTPMaskMultiplex if option.HTTPMaskMultiplex != "" { httpMaskMultiplex = option.HTTPMaskMultiplex } if hm := option.HTTPMaskOptions; hm != nil { disableHTTPMask = hm.Disable if hm.Mode != "" { httpMaskMode = hm.Mode } httpMaskTLS = hm.TLS httpMaskHost = hm.Host if pr := strings.TrimSpace(hm.PathRoot); pr != "" { pathRoot = pr } if mux := strings.TrimSpace(hm.Multiplex); mux != "" { httpMaskMultiplex = mux } else { httpMaskMultiplex = defaultConf.HTTPMaskMultiplex } } baseConf := sudoku.ProtocolConfig{ ServerAddress: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), Key: option.Key, AEADMethod: defaultConf.AEADMethod, PaddingMin: paddingMin, PaddingMax: paddingMax, EnablePureDownlink: enablePureDownlink, HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds, DisableHTTPMask: disableHTTPMask, HTTPMaskMode: httpMaskMode, HTTPMaskTLSEnabled: httpMaskTLS, HTTPMaskHost: httpMaskHost, HTTPMaskPathRoot: pathRoot, HTTPMaskMultiplex: httpMaskMultiplex, } tables, err := sudoku.NewClientTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables) if err != nil { return nil, fmt.Errorf("build table(s) failed: %w", err) } if len(tables) == 1 { baseConf.Table = tables[0] } else { baseConf.Tables = tables } if option.AEADMethod != "" { baseConf.AEADMethod = option.AEADMethod } outbound := &Sudoku{ Base: NewBase(BaseOption{ Name: option.Name, Addr: baseConf.ServerAddress, Type: C.Sudoku, ProviderName: option.ProviderName, UDP: true, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, baseConf: baseConf, } outbound.dialer = option.NewDialer(outbound.DialOptions()) return outbound, nil } func (s *Sudoku) Close() error { s.resetMuxClient() s.resetHTTPMaskClient() return s.Base.Close() } func normalizeHTTPMaskMultiplex(mode string) string { switch strings.ToLower(strings.TrimSpace(mode)) { case "", "off": return "off" case "auto": return "auto" case "on": return "on" default: return "off" } } func httpTunnelModeEnabled(mode string) bool { switch strings.ToLower(strings.TrimSpace(mode)) { case "stream", "poll", "auto", "ws": return true default: return false } } func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfig) (_ net.Conn, err error) { if cfg == nil { return nil, fmt.Errorf("config is required") } handshakeCfg := *cfg if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) { handshakeCfg.DisableHTTPMask = true } upgrade := func(raw net.Conn) (net.Conn, error) { return sudoku.ClientHandshake(raw, &handshakeCfg) } var ( c net.Conn handshakeDone bool ) if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) { muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex) if muxMode == "auto" && strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) != "ws" { if client, cerr := s.getOrCreateHTTPMaskClient(cfg); cerr == nil && client != nil { c, err = client.DialTunnel(ctx, httpmask.TunnelDialOptions{ Mode: cfg.HTTPMaskMode, TLSEnabled: cfg.HTTPMaskTLSEnabled, HostOverride: cfg.HTTPMaskHost, PathRoot: cfg.HTTPMaskPathRoot, AuthKey: sudoku.ClientAEADSeed(cfg.Key), Upgrade: upgrade, Multiplex: cfg.HTTPMaskMultiplex, DialContext: s.dialer.DialContext, }) if err != nil { s.resetHTTPMaskClient() } } } if c == nil && err == nil { c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext, upgrade) } if err == nil && c != nil { handshakeDone = true } } if c == nil && err == nil { c, err = s.dialer.DialContext(ctx, "tcp", s.addr) } if err != nil { return nil, fmt.Errorf("%s connect error: %w", s.addr, err) } defer func() { safeConnClose(c, err) }() if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } if !handshakeDone { c, err = sudoku.ClientHandshake(c, &handshakeCfg) if err != nil { return nil, err } } return c, nil } func (s *Sudoku) dialMultiplex(ctx context.Context, targetAddress string) (net.Conn, error) { for attempt := 0; attempt < 2; attempt++ { client, err := s.getOrCreateMuxClient(ctx) if err != nil { return nil, err } stream, err := client.Dial(ctx, targetAddress) if err != nil { s.resetMuxClient() continue } return stream, nil } return nil, fmt.Errorf("multiplex open stream failed") } func (s *Sudoku) getOrCreateMuxClient(ctx context.Context) (*sudoku.MultiplexClient, error) { if s == nil { return nil, fmt.Errorf("nil adapter") } s.muxMu.Lock() if s.muxClient != nil && !s.muxClient.IsClosed() { client := s.muxClient s.muxMu.Unlock() return client, nil } s.muxMu.Unlock() s.muxMu.Lock() defer s.muxMu.Unlock() if s.muxClient != nil && !s.muxClient.IsClosed() { return s.muxClient, nil } baseCfg := s.baseConf baseConn, err := s.dialAndHandshake(ctx, &baseCfg) if err != nil { return nil, err } client, err := sudoku.StartMultiplexClient(baseConn) if err != nil { _ = baseConn.Close() return nil, err } s.muxClient = client return client, nil } func (s *Sudoku) resetMuxClient() { s.muxMu.Lock() defer s.muxMu.Unlock() if s.muxClient != nil { _ = s.muxClient.Close() s.muxClient = nil } } func (s *Sudoku) resetHTTPMaskClient() { s.httpMaskMu.Lock() defer s.httpMaskMu.Unlock() if s.httpMaskClient != nil { s.httpMaskClient.CloseIdleConnections() s.httpMaskClient = nil s.httpMaskKey = "" } } func (s *Sudoku) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*httpmask.TunnelClient, error) { if s == nil || cfg == nil { return nil, fmt.Errorf("nil adapter or config") } key := cfg.ServerAddress + "|" + strconv.FormatBool(cfg.HTTPMaskTLSEnabled) + "|" + strings.TrimSpace(cfg.HTTPMaskHost) s.httpMaskMu.Lock() if s.httpMaskClient != nil && s.httpMaskKey == key { client := s.httpMaskClient s.httpMaskMu.Unlock() return client, nil } s.httpMaskMu.Unlock() client, err := httpmask.NewTunnelClient(cfg.ServerAddress, httpmask.TunnelClientOptions{ TLSEnabled: cfg.HTTPMaskTLSEnabled, HostOverride: cfg.HTTPMaskHost, DialContext: s.dialer.DialContext, MaxIdleConns: 32, }) if err != nil { return nil, err } s.httpMaskMu.Lock() defer s.httpMaskMu.Unlock() if s.httpMaskClient != nil && s.httpMaskKey == key { client.CloseIdleConnections() return s.httpMaskClient, nil } if s.httpMaskClient != nil { s.httpMaskClient.CloseIdleConnections() } s.httpMaskClient = client s.httpMaskKey = key return client, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/trojan.go ================================================ package outbound import ( "context" "errors" "fmt" "net" "strconv" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/trojan" "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" "github.com/metacubex/tls" ) type Trojan struct { *Base option *TrojanOption hexPassword [trojan.KeyLength]byte // for gun mux gunClient *gun.Client realityConfig *tlsC.RealityConfig echConfig *ech.Config ssCipher core.Cipher } type TrojanOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Password string `proxy:"password"` ALPN []string `proxy:"alpn,omitempty"` SNI string `proxy:"sni,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"` SSOpts TrojanSSOption `proxy:"ss-opts,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } // TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 type TrojanSSOption struct { Enabled bool `proxy:"enabled,omitempty"` Method string `proxy:"method,omitempty"` Password string `proxy:"password,omitempty"` } func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { switch t.option.Network { case "ws": host, port, _ := net.SplitHostPort(t.addr) wsOpts := &vmess.WebsocketConfig{ Host: host, Port: port, Path: t.option.WSOpts.Path, MaxEarlyData: t.option.WSOpts.MaxEarlyData, EarlyDataHeaderName: t.option.WSOpts.EarlyDataHeaderName, V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen, ClientFingerprint: t.option.ClientFingerprint, ECHConfig: t.echConfig, Headers: http.Header{}, } if t.option.SNI != "" { wsOpts.Host = t.option.SNI } if len(t.option.WSOpts.Headers) != 0 { for key, value := range t.option.WSOpts.Headers { wsOpts.Headers.Add(key, value) } } alpn := trojan.DefaultWebsocketALPN if t.option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array alpn = t.option.ALPN } wsOpts.TLS = true wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ NextProtos: alpn, MinVersion: tls.VersionTLS12, InsecureSkipVerify: t.option.SkipCertVerify, ServerName: t.option.SNI, }, Fingerprint: t.option.Fingerprint, Certificate: t.option.Certificate, PrivateKey: t.option.PrivateKey, }) if err != nil { return nil, err } c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) case "grpc": break // already handle in dialContext default: // default tcp network // handle TLS alpn := trojan.DefaultALPN if t.option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array alpn = t.option.ALPN } c, err = vmess.StreamTLSConn(ctx, c, &vmess.TLSConfig{ Host: t.option.SNI, SkipCertVerify: t.option.SkipCertVerify, FingerPrint: t.option.Fingerprint, Certificate: t.option.Certificate, PrivateKey: t.option.PrivateKey, ClientFingerprint: t.option.ClientFingerprint, NextProtos: alpn, ECH: t.echConfig, Reality: t.realityConfig, }) } if err != nil { return nil, err } return t.streamConnContext(ctx, c, metadata) } func (t *Trojan) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { if t.ssCipher != nil { c = t.ssCipher.StreamConn(c) } if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } command := trojan.CommandTCP if metadata.NetWork == C.UDP { command = trojan.CommandUDP } err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata)) return c, err } func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } command := trojan.CommandTCP if metadata.NetWork == C.UDP { command = trojan.CommandUDP } err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata)) return err } func (t *Trojan) dialContext(ctx context.Context) (c net.Conn, err error) { switch t.option.Network { case "grpc": // gun transport return t.gunClient.Dial() default: } return t.dialer.DialContext(ctx, "tcp", t.addr) } // DialContext implements C.ProxyAdapter func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := t.dialContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = t.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } return NewConn(c, t), err } // ListenPacketContext implements C.ProxyAdapter func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = t.ResolveUDP(ctx, metadata); err != nil { return nil, err } c, err := t.dialContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = t.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } pc := trojan.NewPacketConn(c) return newPacketConn(pc, t), err } // SupportUOT implements C.ProxyAdapter func (t *Trojan) SupportUOT() bool { return true } // ProxyInfo implements C.ProxyAdapter func (t *Trojan) ProxyInfo() C.ProxyInfo { info := t.Base.ProxyInfo() info.DialerProxy = t.option.DialerProxy return info } // Close implements C.ProxyAdapter func (t *Trojan) Close() error { var errs []error if t.gunClient != nil { if err := t.gunClient.Close(); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } func NewTrojan(option TrojanOption) (*Trojan, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) if option.SNI == "" { option.SNI = option.Server } t := &Trojan{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Trojan, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, hexPassword: trojan.Key(option.Password), } t.dialer = option.NewDialer(t.DialOptions()) var err error t.realityConfig, err = option.RealityOpts.Parse() if err != nil { return nil, err } t.echConfig, err = option.ECHOpts.Parse() if err != nil { return nil, err } if option.SSOpts.Enabled { if option.SSOpts.Password == "" { return nil, errors.New("empty password") } if option.SSOpts.Method == "" { option.SSOpts.Method = "AES-128-GCM" } ciph, err := core.PickCipher(option.SSOpts.Method, nil, option.SSOpts.Password) if err != nil { return nil, err } t.ssCipher = ciph } if option.Network == "grpc" { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { c, err := t.dialer.DialContext(ctx, "tcp", t.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) } return c, nil } tlsConfig := &vmess.TLSConfig{ Host: option.SNI, SkipCertVerify: option.SkipCertVerify, FingerPrint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, ClientFingerprint: option.ClientFingerprint, NextProtos: []string{"h2"}, ECH: t.echConfig, Reality: t.realityConfig, } gunConfig := &gun.Config{ ServiceName: option.GrpcOpts.GrpcServiceName, UserAgent: option.GrpcOpts.GrpcUserAgent, Host: option.SNI, PingInterval: option.GrpcOpts.PingInterval, } t.gunClient = gun.NewClient( func() *gun.Transport { return gun.NewTransport(dialFn, tlsConfig, gunConfig) }, option.GrpcOpts.MaxConnections, option.GrpcOpts.MinStreams, option.GrpcOpts.MaxStreams, ) } return t, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/trusttunnel.go ================================================ package outbound import ( "context" "net" "strconv" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/trusttunnel" "github.com/metacubex/mihomo/transport/vmess" ) type TrustTunnel struct { *Base client *trusttunnel.PoolClient option *TrustTunnelOption } type TrustTunnelOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` UserName string `proxy:"username,omitempty"` Password string `proxy:"password,omitempty"` ALPN []string `proxy:"alpn,omitempty"` SNI string `proxy:"sni,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` UDP bool `proxy:"udp,omitempty"` HealthCheck bool `proxy:"health-check,omitempty"` // quic options Quic bool `proxy:"quic,omitempty"` CongestionController string `proxy:"congestion-controller,omitempty"` CWND int `proxy:"cwnd,omitempty"` BBRProfile string `proxy:"bbr-profile,omitempty"` // reuse options MaxConnections int `proxy:"max-connections,omitempty"` MinStreams int `proxy:"min-streams,omitempty"` MaxStreams int `proxy:"max-streams,omitempty"` } func (t *TrustTunnel) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := t.client.Dial(ctx, metadata.RemoteAddress()) if err != nil { return nil, err } return NewConn(c, t), nil } func (t *TrustTunnel) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = t.ResolveUDP(ctx, metadata); err != nil { return nil, err } pc, err := t.client.ListenPacket(ctx) if err != nil { return nil, err } return newPacketConn(N.NewThreadSafePacketConn(pc), t), nil } // SupportUOT implements C.ProxyAdapter func (t *TrustTunnel) SupportUOT() bool { return true } // ProxyInfo implements C.ProxyAdapter func (t *TrustTunnel) ProxyInfo() C.ProxyInfo { info := t.Base.ProxyInfo() info.DialerProxy = t.option.DialerProxy return info } // Close implements C.ProxyAdapter func (t *TrustTunnel) Close() error { return t.client.Close() } func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) outbound := &TrustTunnel{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.TrustTunnel, ProviderName: option.ProviderName, UDP: option.UDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, } outbound.dialer = option.NewDialer(outbound.DialOptions()) tOption := trusttunnel.ClientOptions{ Dialer: outbound.dialer, DialOptions: outbound.DialOptions, Server: addr, Username: option.UserName, Password: option.Password, QUIC: option.Quic, QUICCongestionControl: option.CongestionController, QUICCwnd: option.CWND, QUICBBRProfile: option.BBRProfile, HealthCheck: option.HealthCheck, MaxConnections: option.MaxConnections, MinStreams: option.MinStreams, MaxStreams: option.MaxStreams, } echConfig, err := option.ECHOpts.Parse() if err != nil { return nil, err } tlsConfig := &vmess.TLSConfig{ Host: option.SNI, SkipCertVerify: option.SkipCertVerify, NextProtos: option.ALPN, FingerPrint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, ClientFingerprint: option.ClientFingerprint, ECH: echConfig, } if tlsConfig.Host == "" { tlsConfig.Host = option.Server } tOption.TLSConfig = tlsConfig client, err := trusttunnel.NewPoolClient(context.TODO(), tOption) if err != nil { return nil, err } outbound.client = client return outbound, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/tuic.go ================================================ package outbound import ( "context" "fmt" "math" "net" "strconv" "time" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/tuic" "github.com/metacubex/mihomo/transport/tuic/common" "github.com/gofrs/uuid/v5" "github.com/metacubex/quic-go" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/uot" "github.com/metacubex/tls" ) type Tuic struct { *Base option *TuicOption client *tuic.PoolClient quicConfig *quic.Config tlsConfig *tls.Config echConfig *ech.Config } type TuicOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Token string `proxy:"token,omitempty"` UUID string `proxy:"uuid,omitempty"` Password string `proxy:"password,omitempty"` Ip string `proxy:"ip,omitempty"` HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"` ALPN []string `proxy:"alpn,omitempty"` ReduceRtt bool `proxy:"reduce-rtt,omitempty"` RequestTimeout int `proxy:"request-timeout,omitempty"` UdpRelayMode string `proxy:"udp-relay-mode,omitempty"` CongestionController string `proxy:"congestion-controller,omitempty"` DisableSni bool `proxy:"disable-sni,omitempty"` MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"` FastOpen bool `proxy:"fast-open,omitempty"` MaxOpenStreams int `proxy:"max-open-streams,omitempty"` CWND int `proxy:"cwnd,omitempty"` BBRProfile string `proxy:"bbr-profile,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` SNI string `proxy:"sni,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` UDPOverStream bool `proxy:"udp-over-stream,omitempty"` UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"` } // DialContext implements C.ProxyAdapter func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { conn, err := t.client.DialContext(ctx, metadata) if err != nil { return nil, err } return NewConn(conn, t), err } // ListenPacketContext implements C.ProxyAdapter func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = t.ResolveUDP(ctx, metadata); err != nil { return nil, err } if t.option.UDPOverStream { uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion)) uotMetadata := *metadata uotMetadata.Host = uotDestination.Fqdn uotMetadata.DstPort = uotDestination.Port c, err := t.DialContext(ctx, &uotMetadata) if err != nil { return nil, err } // tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr destination := M.SocksaddrFromNet(metadata.UDPAddr()) if t.option.UDPOverStreamVersion == uot.LegacyVersion { return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil } else { return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil } } pc, err := t.client.ListenPacket(ctx, metadata) if err != nil { return nil, err } return newPacketConn(pc, t), nil } func (t *Tuic) dial(ctx context.Context) (quicConn *quic.Conn, err error) { _, quicConn, err = common.DialQuic(ctx, t.addr, t.DialOptions(), t.dialer, t.tlsConfig, t.quicConfig, t.option.ReduceRtt) if err != nil { return nil, err } common.SetCongestionController(quicConn, t.option.CongestionController, t.option.CWND, t.option.BBRProfile) return quicConn, nil } // ProxyInfo implements C.ProxyAdapter func (t *Tuic) ProxyInfo() C.ProxyInfo { info := t.Base.ProxyInfo() info.DialerProxy = t.option.DialerProxy return info } func NewTuic(option TuicOption) (*Tuic, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) serverName := option.Server if option.SNI != "" { serverName = option.SNI } tlsConfig, err := ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: serverName, InsecureSkipVerify: option.SkipCertVerify, MinVersion: tls.VersionTLS13, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array tlsConfig.NextProtos = option.ALPN } else { tlsConfig.NextProtos = []string{"h3"} } if option.RequestTimeout == 0 { option.RequestTimeout = 8000 } if option.HeartbeatInterval <= 0 { option.HeartbeatInterval = 10000 } udpRelayMode := tuic.QUIC if option.UdpRelayMode != "quic" { udpRelayMode = tuic.NATIVE } if option.MaxUdpRelayPacketSize == 0 { option.MaxUdpRelayPacketSize = 1252 } if option.MaxOpenStreams == 0 { option.MaxOpenStreams = 100 } if option.CWND == 0 { option.CWND = 32 } packetOverHead := tuic.PacketOverHeadV4 if len(option.Token) == 0 { packetOverHead = tuic.PacketOverHeadV5 } if option.MaxDatagramFrameSize == 0 { option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + packetOverHead } if option.MaxDatagramFrameSize > 1400 { option.MaxDatagramFrameSize = 1400 } option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - packetOverHead // ensure server's incoming stream can handle correctly, increase to 1.1x quicMaxOpenStreams := int64(option.MaxOpenStreams) quicMaxOpenStreams = quicMaxOpenStreams + int64(math.Ceil(float64(quicMaxOpenStreams)/10.0)) quicConfig := &quic.Config{ InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), InitialConnectionReceiveWindow: uint64(option.ReceiveWindow), MaxConnectionReceiveWindow: uint64(option.ReceiveWindow), MaxIncomingStreams: quicMaxOpenStreams, MaxIncomingUniStreams: quicMaxOpenStreams, KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond, DisablePathMTUDiscovery: option.DisableMTUDiscovery, MaxDatagramFrameSize: int64(option.MaxDatagramFrameSize), EnableDatagrams: true, } if option.ReceiveWindowConn == 0 { quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10 quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow } if option.ReceiveWindow == 0 { quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow } if len(option.Ip) > 0 { addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port)) } if option.DisableSni { tlsConfig.ServerName = "" tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config } echConfig, err := option.ECHOpts.Parse() if err != nil { return nil, err } switch option.UDPOverStreamVersion { case uot.Version, uot.LegacyVersion: case 0: option.UDPOverStreamVersion = uot.LegacyVersion default: return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion) } t := &Tuic{ Base: NewBase(BaseOption{ Name: option.Name, Addr: addr, Type: C.Tuic, ProviderName: option.ProviderName, UDP: true, TFO: option.FastOpen, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), option: &option, quicConfig: quicConfig, tlsConfig: tlsConfig, echConfig: echConfig, } t.dialer = option.NewDialer(t.DialOptions()) clientMaxOpenStreams := int64(option.MaxOpenStreams) // to avoid tuic's "too many open streams", decrease to 0.9x if clientMaxOpenStreams == 100 { clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0)) } if clientMaxOpenStreams < 1 { clientMaxOpenStreams = 1 } if len(option.Token) > 0 { tkn := tuic.GenTKN(option.Token) clientOption := &tuic.ClientOptionV4{ Token: tkn, UdpRelayMode: udpRelayMode, RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond, MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, FastOpen: option.FastOpen, MaxOpenStreams: clientMaxOpenStreams, } t.client = tuic.NewPoolClientV4(clientOption, t.dial) } else { maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 { maxUdpRelayPacketSize = tuic.MaxFragSizeV5 } clientOption := &tuic.ClientOptionV5{ Uuid: uuid.FromStringOrNil(option.UUID), Password: option.Password, UdpRelayMode: udpRelayMode, MaxUdpRelayPacketSize: maxUdpRelayPacketSize, MaxOpenStreams: clientMaxOpenStreams, } t.client = tuic.NewPoolClientV5(clientOption, t.dial) } return t, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/util.go ================================================ package outbound import ( "bytes" "context" "fmt" "net" "net/netip" "regexp" "strconv" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) func serializesSocksAddr(metadata *C.Metadata) []byte { var buf [][]byte addrType := metadata.AddrType() p := uint(metadata.DstPort) port := []byte{uint8(p >> 8), uint8(p & 0xff)} switch addrType { case C.AtypDomainName: lenM := uint8(len(metadata.Host)) host := []byte(metadata.Host) buf = [][]byte{{socks5.AtypDomainName, lenM}, host, port} case C.AtypIPv4: host := metadata.DstIP.AsSlice() buf = [][]byte{{socks5.AtypIPv4}, host, port} case C.AtypIPv6: host := metadata.DstIP.AsSlice() buf = [][]byte{{socks5.AtypIPv6}, host, port} } return bytes.Join(buf, nil) } func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) { host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } var ip netip.Addr switch prefer { case C.IPv4Only: ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver) case C.IPv6Only: ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver) case C.IPv6Prefer: ip, err = resolver.ResolveIPPrefer6WithResolver(ctx, host, resolver.ProxyServerHostResolver) default: ip, err = resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver) } if err != nil { return nil, err } ip, port = resolver.LookupIP4P(ip, port) var uint16Port uint16 if port, err := strconv.ParseUint(port, 10, 16); err == nil { uint16Port = uint16(port) } else { return nil, err } // our resolver always unmap before return, so unneeded unmap at here // which is different with net.ResolveUDPAddr maybe return 4in6 address // 4in6 addresses can cause some strange effects on sing-based code return net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16Port)), nil } func safeConnClose(c net.Conn, err error) { if err != nil && c != nil { _ = c.Close() } } var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) func StringToBps(s string) uint64 { if s == "" { return 0 } // when have not unit, use Mbps if v, err := strconv.Atoi(s); err == nil { return StringToBps(fmt.Sprintf("%d Mbps", v)) } m := rateStringRegexp.FindStringSubmatch(s) if m == nil { return 0 } var n uint64 = 1 switch m[2] { case "T": n *= 1000 fallthrough case "G": n *= 1000 fallthrough case "M": n *= 1000 fallthrough case "K": n *= 1000 } v, _ := strconv.ParseUint(m[1], 10, 64) n *= v if m[3] == "b" { // Bits, need to convert to bytes n /= 8 } return n } ================================================ FILE: core/Clash.Meta/adapter/outbound/vless.go ================================================ package outbound import ( "context" "errors" "fmt" "net" "strconv" "time" "github.com/metacubex/mihomo/common/convert" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/mihomo/transport/vless" "github.com/metacubex/mihomo/transport/vless/encryption" "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/xhttp" "github.com/metacubex/http" "github.com/metacubex/quic-go" vmessSing "github.com/metacubex/sing-vmess" "github.com/metacubex/sing-vmess/packetaddr" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/tls" "github.com/samber/lo" ) type Vless struct { *Base client *vless.Client option *VlessOption encryption *encryption.ClientInstance // for gun mux gunClient *gun.Client // for xhttp xhttpClient *xhttp.Client realityConfig *tlsC.RealityConfig echConfig *ech.Config } type VlessOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` UUID string `proxy:"uuid"` Flow string `proxy:"flow,omitempty"` TLS bool `proxy:"tls,omitempty"` ALPN []string `proxy:"alpn,omitempty"` UDP bool `proxy:"udp,omitempty"` PacketAddr bool `proxy:"packet-addr,omitempty"` XUDP bool `proxy:"xudp,omitempty"` PacketEncoding string `proxy:"packet-encoding,omitempty"` Encryption string `proxy:"encryption,omitempty"` Network string `proxy:"network,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"` XHTTPOpts XHTTPOptions `proxy:"xhttp-opts,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` ServerName string `proxy:"servername,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } type XHTTPOptions struct { Path string `proxy:"path,omitempty"` Host string `proxy:"host,omitempty"` Mode string `proxy:"mode,omitempty"` Headers map[string]string `proxy:"headers,omitempty"` NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"` XPaddingBytes string `proxy:"x-padding-bytes,omitempty"` XPaddingObfsMode bool `proxy:"x-padding-obfs-mode,omitempty"` XPaddingKey string `proxy:"x-padding-key,omitempty"` XPaddingHeader string `proxy:"x-padding-header,omitempty"` XPaddingPlacement string `proxy:"x-padding-placement,omitempty"` XPaddingMethod string `proxy:"x-padding-method,omitempty"` UplinkHTTPMethod string `proxy:"uplink-http-method,omitempty"` SessionPlacement string `proxy:"session-placement,omitempty"` SessionKey string `proxy:"session-key,omitempty"` SeqPlacement string `proxy:"seq-placement,omitempty"` SeqKey string `proxy:"seq-key,omitempty"` UplinkDataPlacement string `proxy:"uplink-data-placement,omitempty"` UplinkDataKey string `proxy:"uplink-data-key,omitempty"` UplinkChunkSize string `proxy:"uplink-chunk-size,omitempty"` ScMaxEachPostBytes string `proxy:"sc-max-each-post-bytes,omitempty"` ScMinPostsIntervalMs string `proxy:"sc-min-posts-interval-ms,omitempty"` ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX DownloadSettings *XHTTPDownloadSettings `proxy:"download-settings,omitempty"` } type XHTTPReuseSettings struct { MaxConcurrency string `proxy:"max-concurrency,omitempty"` MaxConnections string `proxy:"max-connections,omitempty"` CMaxReuseTimes string `proxy:"c-max-reuse-times,omitempty"` HMaxRequestTimes string `proxy:"h-max-request-times,omitempty"` HMaxReusableSecs string `proxy:"h-max-reusable-secs,omitempty"` HKeepAlivePeriod int `proxy:"h-keep-alive-period,omitempty"` } type XHTTPDownloadSettings struct { // xhttp part Path *string `proxy:"path,omitempty"` Host *string `proxy:"host,omitempty"` Headers *map[string]string `proxy:"headers,omitempty"` ReuseSettings *XHTTPReuseSettings `proxy:"reuse-settings,omitempty"` // aka XMUX // proxy part Server *string `proxy:"server,omitempty"` Port *int `proxy:"port,omitempty"` TLS *bool `proxy:"tls,omitempty"` ALPN *[]string `proxy:"alpn,omitempty"` ECHOpts *ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts *RealityOptions `proxy:"reality-opts,omitempty"` SkipCertVerify *bool `proxy:"skip-cert-verify,omitempty"` Fingerprint *string `proxy:"fingerprint,omitempty"` Certificate *string `proxy:"certificate,omitempty"` PrivateKey *string `proxy:"private-key,omitempty"` ServerName *string `proxy:"servername,omitempty"` ClientFingerprint *string `proxy:"client-fingerprint,omitempty"` } func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { switch v.option.Network { case "ws": host, port, _ := net.SplitHostPort(v.addr) wsOpts := &vmess.WebsocketConfig{ Host: host, Port: port, Path: v.option.WSOpts.Path, MaxEarlyData: v.option.WSOpts.MaxEarlyData, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, ClientFingerprint: v.option.ClientFingerprint, ECHConfig: v.echConfig, Headers: http.Header{}, } if len(v.option.WSOpts.Headers) != 0 { for key, value := range v.option.WSOpts.Headers { wsOpts.Headers.Add(key, value) } } if v.option.TLS { wsOpts.TLS = true wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: host, InsecureSkipVerify: v.option.SkipCertVerify, NextProtos: []string{"http/1.1"}, }, Fingerprint: v.option.Fingerprint, Certificate: v.option.Certificate, PrivateKey: v.option.PrivateKey, }) if err != nil { return nil, err } if v.option.ServerName != "" { wsOpts.TLSConfig.ServerName = v.option.ServerName } else if host := wsOpts.Headers.Get("Host"); host != "" { wsOpts.TLSConfig.ServerName = host } } else { if host := wsOpts.Headers.Get("Host"); host == "" { wsOpts.Headers.Set("Host", convert.RandHost()) convert.SetUserAgent(wsOpts.Headers) } } c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) case "http": // readability first, so just copy default TLS logic c, err = v.streamTLSConn(ctx, c, false) if err != nil { return nil, err } host, _, _ := net.SplitHostPort(v.addr) httpOpts := &vmess.HTTPConfig{ Host: host, Method: v.option.HTTPOpts.Method, Path: v.option.HTTPOpts.Path, Headers: v.option.HTTPOpts.Headers, } c = vmess.StreamHTTPConn(c, httpOpts) case "h2": c, err = v.streamTLSConn(ctx, c, true) if err != nil { return nil, err } h2Opts := &vmess.H2Config{ Hosts: v.option.HTTP2Opts.Host, Path: v.option.HTTP2Opts.Path, } c, err = vmess.StreamH2Conn(ctx, c, h2Opts) case "grpc": break // already handle in dialContext case "xhttp": break // already handle in dialContext default: // default tcp network // handle TLS c, err = v.streamTLSConn(ctx, c, false) } if err != nil { return nil, err } return v.streamConnContext(ctx, c, metadata) } func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } if v.encryption != nil { c, err = v.encryption.Handshake(c) if err != nil { return } } if metadata.NetWork == C.UDP { if v.option.PacketAddr { metadata = &C.Metadata{ NetWork: C.UDP, Host: packetaddr.SeqPacketMagicAddress, DstPort: 443, } } else { metadata = &C.Metadata{ // a clear metadata only contains ip NetWork: C.UDP, DstIP: metadata.DstIP, DstPort: metadata.DstPort, } } conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP)) } else { conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, false)) } if err != nil { conn = nil } return } func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) { if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr) tlsOpts := vmess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, Certificate: v.option.Certificate, PrivateKey: v.option.PrivateKey, ClientFingerprint: v.option.ClientFingerprint, ECH: v.echConfig, Reality: v.realityConfig, NextProtos: v.option.ALPN, } if isH2 { tlsOpts.NextProtos = []string{"h2"} } if v.option.ServerName != "" { tlsOpts.Host = v.option.ServerName } return vmess.StreamTLSConn(ctx, conn, &tlsOpts) } return conn, nil } func (v *Vless) dialContext(ctx context.Context) (c net.Conn, err error) { switch v.option.Network { case "grpc": // gun transport return v.gunClient.Dial() case "xhttp": return v.xhttpClient.Dial(ctx) default: } return v.dialer.DialContext(ctx, "tcp", v.addr) } // DialContext implements C.ProxyAdapter func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := v.dialContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = v.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } return NewConn(c, v), err } // ListenPacketContext implements C.ProxyAdapter func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = v.ResolveUDP(ctx, metadata); err != nil { return nil, err } c, err := v.dialContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = v.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } if v.option.XUDP { var globalID [8]byte if metadata.SourceValid() { globalID = utils.GlobalID(metadata.SourceAddress()) } return newPacketConn(N.NewThreadSafePacketConn( vmessSing.NewXUDPConn(c, globalID, M.SocksaddrFromNet(metadata.UDPAddr())), ), v), nil } else if v.option.PacketAddr { return newPacketConn(N.NewThreadSafePacketConn( packetaddr.NewConn(v.client.PacketConn(c, metadata.UDPAddr()), M.SocksaddrFromNet(metadata.UDPAddr())), ), v), nil } return newPacketConn(N.NewThreadSafePacketConn(v.client.PacketConn(c, metadata.UDPAddr())), v), nil } // SupportUOT implements C.ProxyAdapter func (v *Vless) SupportUOT() bool { return true } // ProxyInfo implements C.ProxyAdapter func (v *Vless) ProxyInfo() C.ProxyInfo { info := v.Base.ProxyInfo() info.DialerProxy = v.option.DialerProxy return info } // Close implements C.ProxyAdapter func (v *Vless) Close() error { var errs []error if v.gunClient != nil { if err := v.gunClient.Close(); err != nil { errs = append(errs, err) } } if v.xhttpClient != nil { if err := v.xhttpClient.Close(); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr { var addrType byte var addr []byte switch metadata.AddrType() { case C.AtypIPv4: addrType = vless.AtypIPv4 addr = make([]byte, net.IPv4len) copy(addr[:], metadata.DstIP.AsSlice()) case C.AtypIPv6: addrType = vless.AtypIPv6 addr = make([]byte, net.IPv6len) copy(addr[:], metadata.DstIP.AsSlice()) case C.AtypDomainName: addrType = vless.AtypDomainName addr = make([]byte, len(metadata.Host)+1) addr[0] = byte(len(metadata.Host)) copy(addr[1:], metadata.Host) } return &vless.DstAddr{ UDP: metadata.NetWork == C.UDP, AddrType: addrType, Addr: addr, Port: metadata.DstPort, Mux: metadata.NetWork == C.UDP && xudp, } } func NewVless(option VlessOption) (*Vless, error) { var addons *vless.Addons if len(option.Flow) >= 16 { option.Flow = option.Flow[:16] if option.Flow != vless.XRV { return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) } addons = &vless.Addons{ Flow: option.Flow, } } switch option.PacketEncoding { case "packetaddr", "packet": option.PacketAddr = true option.XUDP = false default: // https://github.com/XTLS/Xray-core/pull/1567#issuecomment-1407305458 if !option.PacketAddr { option.XUDP = true } } if option.XUDP { option.PacketAddr = false } client, err := vless.NewClient(option.UUID, addons) if err != nil { return nil, err } v := &Vless{ Base: NewBase(BaseOption{ Name: option.Name, Addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), Type: C.Vless, ProviderName: option.ProviderName, UDP: option.UDP, XUDP: option.XUDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), client: client, option: &option, } v.dialer = option.NewDialer(v.DialOptions()) v.encryption, err = encryption.NewClient(option.Encryption) if err != nil { return nil, err } v.realityConfig, err = v.option.RealityOpts.Parse() if err != nil { return nil, err } v.echConfig, err = v.option.ECHOpts.Parse() if err != nil { return nil, err } switch option.Network { case "h2": if len(option.HTTP2Opts.Host) == 0 { option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") } case "grpc": dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { c, err := v.dialer.DialContext(ctx, "tcp", v.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } return c, nil } gunConfig := &gun.Config{ ServiceName: option.GrpcOpts.GrpcServiceName, UserAgent: option.GrpcOpts.GrpcUserAgent, Host: option.ServerName, PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr } var tlsConfig *vmess.TLSConfig if option.TLS { tlsConfig = &vmess.TLSConfig{ Host: option.ServerName, SkipCertVerify: option.SkipCertVerify, FingerPrint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, ClientFingerprint: option.ClientFingerprint, NextProtos: []string{"h2"}, ECH: v.echConfig, Reality: v.realityConfig, } if option.ServerName == "" { host, _, _ := net.SplitHostPort(v.addr) tlsConfig.Host = host } } v.gunClient = gun.NewClient( func() *gun.Transport { return gun.NewTransport(dialFn, tlsConfig, gunConfig) }, option.GrpcOpts.MaxConnections, option.GrpcOpts.MinStreams, option.GrpcOpts.MaxStreams, ) case "xhttp": requestHost := v.option.XHTTPOpts.Host if requestHost == "" { if v.option.ServerName != "" { requestHost = v.option.ServerName } else { requestHost = v.option.Server } } var hKeepAlivePeriod time.Duration var reuseCfg *xhttp.ReuseConfig if option.XHTTPOpts.ReuseSettings != nil { reuseCfg = &xhttp.ReuseConfig{ MaxConcurrency: option.XHTTPOpts.ReuseSettings.MaxConcurrency, MaxConnections: option.XHTTPOpts.ReuseSettings.MaxConnections, CMaxReuseTimes: option.XHTTPOpts.ReuseSettings.CMaxReuseTimes, HMaxRequestTimes: option.XHTTPOpts.ReuseSettings.HMaxRequestTimes, HMaxReusableSecs: option.XHTTPOpts.ReuseSettings.HMaxReusableSecs, } hKeepAlivePeriod = time.Duration(option.XHTTPOpts.ReuseSettings.HKeepAlivePeriod) * time.Second } cfg := &xhttp.Config{ Host: requestHost, Path: v.option.XHTTPOpts.Path, Mode: v.option.XHTTPOpts.Mode, Headers: v.option.XHTTPOpts.Headers, NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader, XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes, XPaddingObfsMode: v.option.XHTTPOpts.XPaddingObfsMode, XPaddingKey: v.option.XHTTPOpts.XPaddingKey, XPaddingHeader: v.option.XHTTPOpts.XPaddingHeader, XPaddingPlacement: v.option.XHTTPOpts.XPaddingPlacement, XPaddingMethod: v.option.XHTTPOpts.XPaddingMethod, UplinkHTTPMethod: v.option.XHTTPOpts.UplinkHTTPMethod, SessionPlacement: v.option.XHTTPOpts.SessionPlacement, SessionKey: v.option.XHTTPOpts.SessionKey, SeqPlacement: v.option.XHTTPOpts.SeqPlacement, SeqKey: v.option.XHTTPOpts.SeqKey, UplinkDataPlacement: v.option.XHTTPOpts.UplinkDataPlacement, UplinkDataKey: v.option.XHTTPOpts.UplinkDataKey, UplinkChunkSize: v.option.XHTTPOpts.UplinkChunkSize, ScMaxEachPostBytes: v.option.XHTTPOpts.ScMaxEachPostBytes, ScMinPostsIntervalMs: v.option.XHTTPOpts.ScMinPostsIntervalMs, ReuseConfig: reuseCfg, } makeTransport := func() http.RoundTripper { return xhttp.NewTransport( func(ctx context.Context) (net.Conn, error) { return v.dialer.DialContext(ctx, "tcp", v.addr) }, func(ctx context.Context, raw net.Conn, isH2 bool) (net.Conn, error) { return v.streamTLSConn(ctx, raw, isH2) }, func(ctx context.Context, cfg *quic.Config) (*quic.Conn, error) { host, _, _ := net.SplitHostPort(v.addr) tlsOpts := &vmess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, Certificate: v.option.Certificate, PrivateKey: v.option.PrivateKey, ClientFingerprint: v.option.ClientFingerprint, ECH: v.echConfig, Reality: v.realityConfig, NextProtos: []string{"h3"}, } if v.option.ServerName != "" { tlsOpts.Host = v.option.ServerName } if !v.option.TLS { return nil, errors.New("xhttp HTTP/3 requires TLS") } if v.realityConfig != nil { return nil, errors.New("xhttp HTTP/3 does not support reality") } tlsConfig, err := tlsOpts.ToStdConfig() if err != nil { return nil, err } err = v.echConfig.ClientHandle(ctx, tlsConfig) if err != nil { return nil, err } _, quicConn, err := common.DialQuic(ctx, v.addr, v.DialOptions(), v.dialer, tlsConfig, cfg, true) if err != nil { return nil, err } return quicConn, nil }, v.option.ALPN, hKeepAlivePeriod, ) } var makeDownloadTransport func() http.RoundTripper if ds := v.option.XHTTPOpts.DownloadSettings; ds != nil { if cfg.Mode == "stream-one" { return nil, fmt.Errorf(`xhttp mode "stream-one" cannot be used with download-settings`) } downloadServer := lo.FromPtrOr(ds.Server, v.option.Server) downloadPort := lo.FromPtrOr(ds.Port, v.option.Port) downloadTLS := lo.FromPtrOr(ds.TLS, v.option.TLS) downloadALPN := lo.FromPtrOr(ds.ALPN, v.option.ALPN) downloadEchConfig := v.echConfig if ds.ECHOpts != nil { downloadEchConfig, err = ds.ECHOpts.Parse() if err != nil { return nil, err } } downloadRealityCfg := v.realityConfig if ds.RealityOpts != nil { downloadRealityCfg, err = ds.RealityOpts.Parse() if err != nil { return nil, err } } downloadSkipCertVerify := lo.FromPtrOr(ds.SkipCertVerify, v.option.SkipCertVerify) downloadFingerprint := lo.FromPtrOr(ds.Fingerprint, v.option.Fingerprint) downloadCertificate := lo.FromPtrOr(ds.Certificate, v.option.Certificate) downloadPrivateKey := lo.FromPtrOr(ds.PrivateKey, v.option.PrivateKey) downloadServerName := lo.FromPtrOr(ds.ServerName, v.option.ServerName) downloadClientFingerprint := lo.FromPtrOr(ds.ClientFingerprint, v.option.ClientFingerprint) downloadAddr := net.JoinHostPort(downloadServer, strconv.Itoa(downloadPort)) downloadHost := lo.FromPtrOr(ds.Host, v.option.XHTTPOpts.Host) if downloadHost == "" { if downloadServerName != "" { downloadHost = downloadServerName } else { downloadHost = downloadServer } } downloadHKeepAlivePeriod := hKeepAlivePeriod downloadCfg := *cfg // make a copy downloadCfg.Host = downloadHost downloadCfg.Path = lo.FromPtrOr(ds.Path, v.option.XHTTPOpts.Path) downloadCfg.Headers = lo.FromPtrOr(ds.Headers, v.option.XHTTPOpts.Headers) if ds.ReuseSettings != nil { downloadCfg.ReuseConfig = &xhttp.ReuseConfig{ MaxConcurrency: ds.ReuseSettings.MaxConcurrency, MaxConnections: ds.ReuseSettings.MaxConnections, CMaxReuseTimes: ds.ReuseSettings.CMaxReuseTimes, HMaxRequestTimes: ds.ReuseSettings.HMaxRequestTimes, HMaxReusableSecs: ds.ReuseSettings.HMaxReusableSecs, } downloadHKeepAlivePeriod = time.Duration(ds.ReuseSettings.HKeepAlivePeriod) * time.Second } cfg.DownloadConfig = &downloadCfg makeDownloadTransport = func() http.RoundTripper { return xhttp.NewTransport( func(ctx context.Context) (net.Conn, error) { return v.dialer.DialContext(ctx, "tcp", downloadAddr) }, func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) { if downloadTLS { host, _, _ := net.SplitHostPort(downloadAddr) tlsOpts := vmess.TLSConfig{ Host: host, SkipCertVerify: downloadSkipCertVerify, FingerPrint: downloadFingerprint, Certificate: downloadCertificate, PrivateKey: downloadPrivateKey, ClientFingerprint: downloadClientFingerprint, ECH: downloadEchConfig, Reality: downloadRealityCfg, NextProtos: downloadALPN, } if isH2 { tlsOpts.NextProtos = []string{"h2"} } if downloadServerName != "" { tlsOpts.Host = downloadServerName } return vmess.StreamTLSConn(ctx, conn, &tlsOpts) } return conn, nil }, func(ctx context.Context, cfg *quic.Config) (*quic.Conn, error) { host, _, _ := net.SplitHostPort(downloadAddr) tlsOpts := &vmess.TLSConfig{ Host: host, SkipCertVerify: downloadSkipCertVerify, FingerPrint: downloadFingerprint, Certificate: downloadCertificate, PrivateKey: downloadPrivateKey, ClientFingerprint: downloadClientFingerprint, ECH: downloadEchConfig, Reality: downloadRealityCfg, NextProtos: []string{"h3"}, } if downloadServerName != "" { tlsOpts.Host = downloadServerName } if !downloadTLS { return nil, errors.New("xhttp HTTP/3 requires TLS") } if downloadRealityCfg != nil { return nil, errors.New("xhttp HTTP/3 does not support reality") } tlsConfig, err := tlsOpts.ToStdConfig() if err != nil { return nil, err } err = downloadEchConfig.ClientHandle(ctx, tlsConfig) if err != nil { return nil, err } _, quicConn, err := common.DialQuic(ctx, downloadAddr, v.DialOptions(), v.dialer, tlsConfig, cfg, true) if err != nil { return nil, err } return quicConn, nil }, downloadALPN, downloadHKeepAlivePeriod, ) } } v.xhttpClient, err = xhttp.NewClient(cfg, makeTransport, makeDownloadTransport, v.realityConfig != nil) if err != nil { return nil, err } } return v, nil } ================================================ FILE: core/Clash.Meta/adapter/outbound/vmess.go ================================================ package outbound import ( "context" "errors" "fmt" "net" "strconv" "strings" "sync" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/gun" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" vmess "github.com/metacubex/sing-vmess" "github.com/metacubex/sing-vmess/packetaddr" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/tls" ) var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address") type Vmess struct { *Base client *vmess.Client option *VmessOption // for gun mux gunClient *gun.Client realityConfig *tlsC.RealityConfig echConfig *ech.Config } type VmessOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` UUID string `proxy:"uuid"` AlterID int `proxy:"alterId"` Cipher string `proxy:"cipher"` UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` TLS bool `proxy:"tls,omitempty"` ALPN []string `proxy:"alpn,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` Certificate string `proxy:"certificate,omitempty"` PrivateKey string `proxy:"private-key,omitempty"` ServerName string `proxy:"servername,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"` PacketAddr bool `proxy:"packet-addr,omitempty"` XUDP bool `proxy:"xudp,omitempty"` PacketEncoding string `proxy:"packet-encoding,omitempty"` GlobalPadding bool `proxy:"global-padding,omitempty"` AuthenticatedLength bool `proxy:"authenticated-length,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } type HTTPOptions struct { Method string `proxy:"method,omitempty"` Path []string `proxy:"path,omitempty"` Headers map[string][]string `proxy:"headers,omitempty"` } type HTTP2Options struct { Host []string `proxy:"host,omitempty"` Path string `proxy:"path,omitempty"` } type GrpcOptions struct { GrpcServiceName string `proxy:"grpc-service-name,omitempty"` GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"` PingInterval int `proxy:"ping-interval,omitempty"` MaxConnections int `proxy:"max-connections,omitempty"` MinStreams int `proxy:"min-streams,omitempty"` MaxStreams int `proxy:"max-streams,omitempty"` } type WSOptions struct { Path string `proxy:"path,omitempty"` Headers map[string]string `proxy:"headers,omitempty"` MaxEarlyData int `proxy:"max-early-data,omitempty"` EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"` V2rayHttpUpgrade bool `proxy:"v2ray-http-upgrade,omitempty"` V2rayHttpUpgradeFastOpen bool `proxy:"v2ray-http-upgrade-fast-open,omitempty"` } func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { switch v.option.Network { case "ws": host, port, _ := net.SplitHostPort(v.addr) wsOpts := &mihomoVMess.WebsocketConfig{ Host: host, Port: port, Path: v.option.WSOpts.Path, MaxEarlyData: v.option.WSOpts.MaxEarlyData, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, ClientFingerprint: v.option.ClientFingerprint, ECHConfig: v.echConfig, Headers: http.Header{}, } if len(v.option.WSOpts.Headers) != 0 { for key, value := range v.option.WSOpts.Headers { wsOpts.Headers.Add(key, value) } } if v.option.TLS { wsOpts.TLS = true wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: host, InsecureSkipVerify: v.option.SkipCertVerify, NextProtos: []string{"http/1.1"}, }, Fingerprint: v.option.Fingerprint, Certificate: v.option.Certificate, PrivateKey: v.option.PrivateKey, }) if err != nil { return nil, err } if v.option.ServerName != "" { wsOpts.TLSConfig.ServerName = v.option.ServerName } else if host := wsOpts.Headers.Get("Host"); host != "" { wsOpts.TLSConfig.ServerName = host } } c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts) case "http": // readability first, so just copy default TLS logic c, err = v.streamTLSConn(ctx, c, false) if err != nil { return nil, err } host, _, _ := net.SplitHostPort(v.addr) httpOpts := &mihomoVMess.HTTPConfig{ Host: host, Method: v.option.HTTPOpts.Method, Path: v.option.HTTPOpts.Path, Headers: v.option.HTTPOpts.Headers, } c = mihomoVMess.StreamHTTPConn(c, httpOpts) case "h2": c, err = v.streamTLSConn(ctx, c, true) if err != nil { return nil, err } h2Opts := &mihomoVMess.H2Config{ Hosts: v.option.HTTP2Opts.Host, Path: v.option.HTTP2Opts.Path, } c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts) case "grpc": break // already handle in dialContext default: // default tcp network // handle TLS c, err = v.streamTLSConn(ctx, c, false) } if err != nil { return nil, err } return v.streamConnContext(ctx, c, metadata) } func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { useEarly := N.NeedHandshake(c) if !useEarly { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) defer done(&err) } } if metadata.NetWork == C.UDP { if v.option.XUDP { var globalID [8]byte if metadata.SourceValid() { globalID = utils.GlobalID(metadata.SourceAddress()) } if useEarly { conn = v.client.DialEarlyXUDPPacketConn(c, globalID, M.SocksaddrFromNet(metadata.UDPAddr())) } else { conn, err = v.client.DialXUDPPacketConn(c, globalID, M.SocksaddrFromNet(metadata.UDPAddr())) } } else if v.option.PacketAddr { if useEarly { conn = v.client.DialEarlyPacketConn(c, M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443)) } else { conn, err = v.client.DialPacketConn(c, M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443)) } conn = packetaddr.NewBindConn(conn) } else { if useEarly { conn = v.client.DialEarlyPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr())) } else { conn, err = v.client.DialPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr())) } } } else { if useEarly { conn = v.client.DialEarlyConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) } else { conn, err = v.client.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) } } if err != nil { conn = nil } return } func (v *Vmess) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) { if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr) tlsOpts := mihomoVMess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, Certificate: v.option.Certificate, PrivateKey: v.option.PrivateKey, ClientFingerprint: v.option.ClientFingerprint, ECH: v.echConfig, Reality: v.realityConfig, NextProtos: v.option.ALPN, } if isH2 { tlsOpts.NextProtos = []string{"h2"} } if v.option.ServerName != "" { tlsOpts.Host = v.option.ServerName } return mihomoVMess.StreamTLSConn(ctx, conn, &tlsOpts) } return conn, nil } func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) { switch v.option.Network { case "grpc": // gun transport return v.gunClient.Dial() default: } return v.dialer.DialContext(ctx, "tcp", v.addr) } // DialContext implements C.ProxyAdapter func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { c, err := v.dialContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = v.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } return NewConn(c, v), err } // ListenPacketContext implements C.ProxyAdapter func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { if err = v.ResolveUDP(ctx, metadata); err != nil { return nil, err } c, err := v.dialContext(ctx) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } defer func(c net.Conn) { safeConnClose(c, err) }(c) c, err = v.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } if pc, ok := c.(net.PacketConn); ok { return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil } return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil } // ProxyInfo implements C.ProxyAdapter func (v *Vmess) ProxyInfo() C.ProxyInfo { info := v.Base.ProxyInfo() info.DialerProxy = v.option.DialerProxy return info } // Close implements C.ProxyAdapter func (v *Vmess) Close() error { var errs []error if v.gunClient != nil { if err := v.gunClient.Close(); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } // SupportUOT implements C.ProxyAdapter func (v *Vmess) SupportUOT() bool { return true } func NewVmess(option VmessOption) (*Vmess, error) { security := strings.ToLower(option.Cipher) var options []vmess.ClientOption if option.GlobalPadding { options = append(options, vmess.ClientWithGlobalPadding()) } if option.AuthenticatedLength { options = append(options, vmess.ClientWithAuthenticatedLength()) } options = append(options, vmess.ClientWithTimeFunc(ntp.Now)) client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...) if err != nil { return nil, err } switch option.PacketEncoding { case "packetaddr", "packet": option.PacketAddr = true case "xudp": option.XUDP = true } if option.XUDP { option.PacketAddr = false } v := &Vmess{ Base: NewBase(BaseOption{ Name: option.Name, Addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), Type: C.Vmess, ProviderName: option.ProviderName, UDP: option.UDP, XUDP: option.XUDP, TFO: option.TFO, MPTCP: option.MPTCP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), client: client, option: &option, } v.dialer = option.NewDialer(v.DialOptions()) v.realityConfig, err = v.option.RealityOpts.Parse() if err != nil { return nil, err } v.echConfig, err = v.option.ECHOpts.Parse() if err != nil { return nil, err } switch option.Network { case "h2": if len(option.HTTP2Opts.Host) == 0 { option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") } case "grpc": dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { c, err := v.dialer.DialContext(ctx, "tcp", v.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } return c, nil } gunConfig := &gun.Config{ ServiceName: option.GrpcOpts.GrpcServiceName, UserAgent: option.GrpcOpts.GrpcUserAgent, Host: option.ServerName, PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr } var tlsConfig *mihomoVMess.TLSConfig if option.TLS { tlsConfig = &mihomoVMess.TLSConfig{ Host: option.ServerName, SkipCertVerify: option.SkipCertVerify, FingerPrint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, ClientFingerprint: option.ClientFingerprint, NextProtos: []string{"h2"}, ECH: v.echConfig, Reality: v.realityConfig, } if option.ServerName == "" { host, _, _ := net.SplitHostPort(v.addr) tlsConfig.Host = host } } v.gunClient = gun.NewClient( func() *gun.Transport { return gun.NewTransport(dialFn, tlsConfig, gunConfig) }, option.GrpcOpts.MaxConnections, option.GrpcOpts.MinStreams, option.GrpcOpts.MaxStreams, ) } return v, nil } type vmessPacketConn struct { net.Conn rAddr net.Addr access sync.Mutex } // WriteTo implments C.PacketConn.WriteTo // Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not. func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { allowedAddr := uc.rAddr destAddr := addr if allowedAddr.String() != destAddr.String() { return 0, ErrUDPRemoteAddrMismatch } uc.access.Lock() defer uc.access.Unlock() return uc.Conn.Write(b) } func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { n, err := uc.Conn.Read(b) return n, uc.rAddr, err } ================================================ FILE: core/Clash.Meta/adapter/outbound/wireguard.go ================================================ package outbound import ( "context" "encoding/base64" "encoding/hex" "fmt" "net" "net/netip" "strconv" "strings" "sync" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/slowdown" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/log" amnezia "github.com/metacubex/amneziawg-go/device" wireguard "github.com/metacubex/sing-wireguard" "github.com/metacubex/wireguard-go/device" "github.com/metacubex/sing/common/debug" E "github.com/metacubex/sing/common/exceptions" M "github.com/metacubex/sing/common/metadata" ) type wireguardGoDevice interface { Close() IpcSet(uapiConf string) error } type WireGuard struct { *Base bind *wireguard.ClientBind device wireguardGoDevice tunDevice wireguard.Device resolver resolver.Resolver initOk atomic.Bool initMutex sync.Mutex initErr error option WireGuardOption connectAddr M.Socksaddr localPrefixes []netip.Prefix serverAddrMap map[M.Socksaddr]netip.AddrPort serverAddrTime atomic.TypedValue[time.Time] serverAddrMutex sync.Mutex } type WireGuardOption struct { BasicOption WireGuardPeerOption Name string `proxy:"name"` Ip string `proxy:"ip,omitempty"` Ipv6 string `proxy:"ipv6,omitempty"` PrivateKey string `proxy:"private-key"` Workers int `proxy:"workers,omitempty"` MTU int `proxy:"mtu,omitempty"` UDP bool `proxy:"udp,omitempty"` PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"` AmneziaWGOption *AmneziaWGOption `proxy:"amnezia-wg-option,omitempty"` Peers []WireGuardPeerOption `proxy:"peers,omitempty"` RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"` Dns []string `proxy:"dns,omitempty"` RefreshServerIPInterval int `proxy:"refresh-server-ip-interval,omitempty"` } type WireGuardPeerOption struct { Server string `proxy:"server,omitempty"` Port int `proxy:"port,omitempty"` PublicKey string `proxy:"public-key,omitempty"` PreSharedKey string `proxy:"pre-shared-key,omitempty"` Reserved []uint8 `proxy:"reserved,omitempty"` AllowedIPs []string `proxy:"allowed-ips,omitempty"` } type AmneziaWGOption struct { JC int `proxy:"jc,omitempty"` JMin int `proxy:"jmin,omitempty"` JMax int `proxy:"jmax,omitempty"` S1 int `proxy:"s1,omitempty"` S2 int `proxy:"s2,omitempty"` S3 int `proxy:"s3,omitempty"` // AmneziaWG v1.5 and v2 S4 int `proxy:"s4,omitempty"` // AmneziaWG v1.5 and v2 H1 string `proxy:"h1,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation H2 string `proxy:"h2,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation H3 string `proxy:"h3,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation H4 string `proxy:"h4,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation I1 string `proxy:"i1,omitempty"` // AmneziaWG v1.5 and v2 I2 string `proxy:"i2,omitempty"` // AmneziaWG v1.5 and v2 I3 string `proxy:"i3,omitempty"` // AmneziaWG v1.5 and v2 I4 string `proxy:"i4,omitempty"` // AmneziaWG v1.5 and v2 I5 string `proxy:"i5,omitempty"` // AmneziaWG v1.5 and v2 J1 string `proxy:"j1,omitempty"` // AmneziaWG v1.5 only (removed in v2) J2 string `proxy:"j2,omitempty"` // AmneziaWG v1.5 only (removed in v2) J3 string `proxy:"j3,omitempty"` // AmneziaWG v1.5 only (removed in v2) Itime int64 `proxy:"itime,omitempty"` // AmneziaWG v1.5 only (removed in v2) } type wgSingErrorHandler struct { name string } var _ E.Handler = (*wgSingErrorHandler)(nil) func (w wgSingErrorHandler) NewError(ctx context.Context, err error) { if E.IsClosedOrCanceled(err) { log.SingLogger.Debug(fmt.Sprintf("[WG](%s) connection closed: %s", w.name, err)) return } log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", w.name, err)) } type wgNetDialer struct { tunDevice wireguard.Device } var _ dialer.NetDialer = (*wgNetDialer)(nil) func (d wgNetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap()) } func (option WireGuardPeerOption) Addr() M.Socksaddr { return M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)) } func (option WireGuardOption) Prefixes() ([]netip.Prefix, error) { localPrefixes := make([]netip.Prefix, 0, 2) if len(option.Ip) > 0 { if !strings.Contains(option.Ip, "/") { option.Ip = option.Ip + "/32" } if prefix, err := netip.ParsePrefix(option.Ip); err == nil { localPrefixes = append(localPrefixes, prefix) } else { return nil, E.Cause(err, "ip address parse error") } } if len(option.Ipv6) > 0 { if !strings.Contains(option.Ipv6, "/") { option.Ipv6 = option.Ipv6 + "/128" } if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil { localPrefixes = append(localPrefixes, prefix) } else { return nil, E.Cause(err, "ipv6 address parse error") } } if len(localPrefixes) == 0 { return nil, E.New("missing local address") } return localPrefixes, nil } func NewWireGuard(option WireGuardOption) (*WireGuard, error) { outbound := &WireGuard{ Base: NewBase(BaseOption{ Name: option.Name, Addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), Type: C.WireGuard, ProviderName: option.ProviderName, UDP: option.UDP, Interface: option.Interface, RoutingMark: option.RoutingMark, Prefer: option.IPVersion, }), } outbound.dialer = option.NewDialer(outbound.DialOptions()) singDialer := proxydialer.NewSingDialer(proxydialer.NewSlowDownDialer(outbound.dialer, slowdown.New())) var reserved [3]uint8 if len(option.Reserved) > 0 { if len(option.Reserved) != 3 { return nil, E.New("invalid reserved value, required 3 bytes, got ", len(option.Reserved)) } copy(reserved[:], option.Reserved) } var isConnect bool if len(option.Peers) < 2 { isConnect = true if len(option.Peers) == 1 { outbound.connectAddr = option.Peers[0].Addr() } else { outbound.connectAddr = option.Addr() } } outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, singDialer, isConnect, outbound.connectAddr.AddrPort(), reserved) var err error outbound.localPrefixes, err = option.Prefixes() if err != nil { return nil, err } { bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey) if err != nil { return nil, E.Cause(err, "decode private key") } option.PrivateKey = hex.EncodeToString(bytes) } if len(option.Peers) > 0 { for i := range option.Peers { peer := &option.Peers[i] // we need modify option here bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) if err != nil { return nil, E.Cause(err, "decode public key for peer ", i) } peer.PublicKey = hex.EncodeToString(bytes) if peer.PreSharedKey != "" { bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey) if err != nil { return nil, E.Cause(err, "decode pre shared key for peer ", i) } peer.PreSharedKey = hex.EncodeToString(bytes) } if len(peer.AllowedIPs) == 0 { return nil, E.New("missing allowed_ips for peer ", i) } if len(peer.Reserved) > 0 { if len(peer.Reserved) != 3 { return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved)) } } } } else { { bytes, err := base64.StdEncoding.DecodeString(option.PublicKey) if err != nil { return nil, E.Cause(err, "decode peer public key") } option.PublicKey = hex.EncodeToString(bytes) } if option.PreSharedKey != "" { bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey) if err != nil { return nil, E.Cause(err, "decode pre shared key") } option.PreSharedKey = hex.EncodeToString(bytes) } } outbound.option = option mtu := option.MTU if mtu == 0 { mtu = 1408 } if len(outbound.localPrefixes) == 0 { return nil, E.New("missing local address") } outbound.tunDevice, err = wireguard.NewStackDevice(outbound.localPrefixes, uint32(mtu)) if err != nil { return nil, E.Cause(err, "create WireGuard device") } logger := &device.Logger{ Verbosef: func(format string, args ...interface{}) { log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) }, Errorf: func(format string, args ...interface{}) { log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) }, } if option.AmneziaWGOption != nil { outbound.bind.SetParseReserved(false) // AmneziaWG don't need parse reserved outbound.device = amnezia.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers) } else { outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers) } var has6 bool for _, address := range outbound.localPrefixes { if !address.Addr().Unmap().Is4() { has6 = true break } } if option.RemoteDnsResolve && len(option.Dns) > 0 { nss, err := dns.ParseNameServer(option.Dns) if err != nil { return nil, err } for i := range nss { nss[i].ProxyAdapter = outbound } outbound.resolver = dns.NewResolver(dns.Config{ Main: nss, IPv6: has6, }) } return outbound, nil } func (w *WireGuard) resolve(ctx context.Context, address M.Socksaddr) (netip.AddrPort, error) { if address.Addr.IsValid() { return address.AddrPort(), nil } udpAddr, err := resolveUDPAddr(ctx, "udp", address.String(), w.prefer) if err != nil { return netip.AddrPort{}, err } // net.ResolveUDPAddr maybe return 4in6 address, so unmap at here addrPort := udpAddr.AddrPort() return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()), nil } func (w *WireGuard) init(ctx context.Context) error { err := w.init0(ctx) if err != nil { return err } w.updateServerAddr(ctx) return nil } func (w *WireGuard) init0(ctx context.Context) error { if w.initOk.Load() { return nil } w.initMutex.Lock() defer w.initMutex.Unlock() // double check like sync.Once if w.initOk.Load() { return nil } if w.initErr != nil { return w.initErr } w.bind.ResetReservedForEndpoint() w.serverAddrMap = make(map[M.Socksaddr]netip.AddrPort) ipcConf, err := w.genIpcConf(ctx, false) if err != nil { // !!! do not set initErr here !!! // let us can retry domain resolve in next time return err } if debug.Enabled { log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", w.option.Name, ipcConf)) } err = w.device.IpcSet(ipcConf) if err != nil { w.initErr = E.Cause(err, "setup wireguard") return w.initErr } w.serverAddrTime.Store(time.Now()) err = w.tunDevice.Start() if err != nil { w.initErr = err return w.initErr } w.initOk.Store(true) return nil } func (w *WireGuard) updateServerAddr(ctx context.Context) { if w.option.RefreshServerIPInterval != 0 && time.Since(w.serverAddrTime.Load()) > time.Second*time.Duration(w.option.RefreshServerIPInterval) { if w.serverAddrMutex.TryLock() { defer w.serverAddrMutex.Unlock() ipcConf, err := w.genIpcConf(ctx, true) if err != nil { log.Warnln("[WG](%s)UpdateServerAddr failed to generate wireguard ipc conf: %s", w.option.Name, err) return } err = w.device.IpcSet(ipcConf) if err != nil { log.Warnln("[WG](%s)UpdateServerAddr failed to update wireguard ipc conf: %s", w.option.Name, err) return } w.serverAddrTime.Store(time.Now()) } } } func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, error) { ipcConf := "" if !updateOnly { ipcConf += "private_key=" + w.option.PrivateKey + "\n" if w.option.AmneziaWGOption != nil { if w.option.AmneziaWGOption.JC != 0 { ipcConf += "jc=" + strconv.Itoa(w.option.AmneziaWGOption.JC) + "\n" } if w.option.AmneziaWGOption.JMin != 0 { ipcConf += "jmin=" + strconv.Itoa(w.option.AmneziaWGOption.JMin) + "\n" } if w.option.AmneziaWGOption.JMax != 0 { ipcConf += "jmax=" + strconv.Itoa(w.option.AmneziaWGOption.JMax) + "\n" } if w.option.AmneziaWGOption.S1 != 0 { ipcConf += "s1=" + strconv.Itoa(w.option.AmneziaWGOption.S1) + "\n" } if w.option.AmneziaWGOption.S2 != 0 { ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n" } if w.option.AmneziaWGOption.S3 != 0 { ipcConf += "s3=" + strconv.Itoa(w.option.AmneziaWGOption.S3) + "\n" } if w.option.AmneziaWGOption.S4 != 0 { ipcConf += "s4=" + strconv.Itoa(w.option.AmneziaWGOption.S4) + "\n" } if w.option.AmneziaWGOption.H1 != "" { ipcConf += "h1=" + w.option.AmneziaWGOption.H1 + "\n" } if w.option.AmneziaWGOption.H2 != "" { ipcConf += "h2=" + w.option.AmneziaWGOption.H2 + "\n" } if w.option.AmneziaWGOption.H3 != "" { ipcConf += "h3=" + w.option.AmneziaWGOption.H3 + "\n" } if w.option.AmneziaWGOption.H4 != "" { ipcConf += "h4=" + w.option.AmneziaWGOption.H4 + "\n" } if w.option.AmneziaWGOption.I1 != "" { ipcConf += "i1=" + w.option.AmneziaWGOption.I1 + "\n" } if w.option.AmneziaWGOption.I2 != "" { ipcConf += "i2=" + w.option.AmneziaWGOption.I2 + "\n" } if w.option.AmneziaWGOption.I3 != "" { ipcConf += "i3=" + w.option.AmneziaWGOption.I3 + "\n" } if w.option.AmneziaWGOption.I4 != "" { ipcConf += "i4=" + w.option.AmneziaWGOption.I4 + "\n" } if w.option.AmneziaWGOption.I5 != "" { ipcConf += "i5=" + w.option.AmneziaWGOption.I5 + "\n" } if w.option.AmneziaWGOption.J1 != "" { ipcConf += "j1=" + w.option.AmneziaWGOption.J1 + "\n" } if w.option.AmneziaWGOption.J2 != "" { ipcConf += "j2=" + w.option.AmneziaWGOption.J2 + "\n" } if w.option.AmneziaWGOption.J3 != "" { ipcConf += "j3=" + w.option.AmneziaWGOption.J3 + "\n" } if w.option.AmneziaWGOption.Itime != 0 { ipcConf += "itime=" + strconv.FormatInt(int64(w.option.AmneziaWGOption.Itime), 10) + "\n" } } } if len(w.option.Peers) > 0 { for i, peer := range w.option.Peers { peerAddr := peer.Addr() destination, err := w.resolve(ctx, peerAddr) if err != nil { return "", E.Cause(err, "resolve endpoint domain for peer ", i) } if w.serverAddrMap[peerAddr] != destination { w.serverAddrMap[peerAddr] = destination } else if updateOnly { continue } if len(w.option.Peers) == 1 { // must call SetConnectAddr if isConnect == true w.bind.SetConnectAddr(destination) } ipcConf += "public_key=" + peer.PublicKey + "\n" if updateOnly { ipcConf += "update_only=true\n" } ipcConf += "endpoint=" + destination.String() + "\n" if len(peer.Reserved) > 0 { var reserved [3]uint8 copy(reserved[:], w.option.Reserved) w.bind.SetReservedForEndpoint(destination, reserved) } if updateOnly { continue } if peer.PreSharedKey != "" { ipcConf += "preshared_key=" + peer.PreSharedKey + "\n" } for _, allowedIP := range peer.AllowedIPs { ipcConf += "allowed_ip=" + allowedIP + "\n" } if w.option.PersistentKeepalive != 0 { ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive) } } } else { destination, err := w.resolve(ctx, w.connectAddr) if err != nil { return "", E.Cause(err, "resolve endpoint domain") } if w.serverAddrMap[w.connectAddr] != destination { w.serverAddrMap[w.connectAddr] = destination } else if updateOnly { return "", nil } w.bind.SetConnectAddr(destination) // must call SetConnectAddr if isConnect == true ipcConf += "public_key=" + w.option.PublicKey + "\n" if updateOnly { ipcConf += "update_only=true\n" } ipcConf += "endpoint=" + destination.String() + "\n" if updateOnly { return ipcConf, nil } if w.option.PreSharedKey != "" { ipcConf += "preshared_key=" + w.option.PreSharedKey + "\n" } var has4, has6 bool for _, address := range w.localPrefixes { if address.Addr().Is4() { has4 = true } else { has6 = true } } if has4 { ipcConf += "allowed_ip=0.0.0.0/0\n" } if has6 { ipcConf += "allowed_ip=::/0\n" } if w.option.PersistentKeepalive != 0 { ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive) } } return ipcConf, nil } // Close implements C.ProxyAdapter func (w *WireGuard) Close() error { if w.device != nil { w.device.Close() } return nil } func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { var conn net.Conn if err = w.init(ctx); err != nil { return nil, err } if !metadata.Resolved() || w.resolver != nil { r := resolver.DefaultResolver if w.resolver != nil { r = w.resolver } options := w.DialOptions() options = append(options, dialer.WithResolver(r)) options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice})) conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress()) } else { conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) } if err != nil { return nil, err } if conn == nil { return nil, E.New("conn is nil") } return NewConn(conn, w), nil } func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { var pc net.PacketConn if err = w.init(ctx); err != nil { return nil, err } if err = w.ResolveUDP(ctx, metadata); err != nil { return nil, err } pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) if err != nil { return nil, err } if pc == nil { return nil, E.New("packetConn is nil") } return newPacketConn(pc, w), nil } func (w *WireGuard) ResolveUDP(ctx context.Context, metadata *C.Metadata) error { if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { r := resolver.DefaultResolver if w.resolver != nil { r = w.resolver } ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) if err != nil { return fmt.Errorf("can't resolve ip: %w", err) } metadata.DstIP = ip } return nil } // ProxyInfo implements C.ProxyAdapter func (w *WireGuard) ProxyInfo() C.ProxyInfo { info := w.Base.ProxyInfo() info.DialerProxy = w.option.DialerProxy return info } // IsL3Protocol implements C.ProxyAdapter func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool { return true } ================================================ FILE: core/Clash.Meta/adapter/outboundgroup/fallback.go ================================================ package outboundgroup import ( "context" "encoding/json" "errors" "time" "github.com/metacubex/mihomo/common/callback" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" ) type Fallback struct { *GroupBase disableUDP bool testUrl string selected string expectedStatus string } func (f *Fallback) Now() string { proxy := f.findAliveProxy(false) return proxy.Name() } // DialContext implements C.ProxyAdapter func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { proxy := f.findAliveProxy(true) c, err := proxy.DialContext(ctx, metadata) if err == nil { c.AppendToChains(f) } else { f.onDialFailed(proxy.Type(), err, f.healthCheck) } if N.NeedHandshake(c) { c = callback.NewFirstWriteCallBackConn(c, func(err error) { if err == nil { f.onDialSuccess() } else { f.onDialFailed(proxy.Type(), err, f.healthCheck) } }) } return c, err } // ListenPacketContext implements C.ProxyAdapter func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { proxy := f.findAliveProxy(true) pc, err := proxy.ListenPacketContext(ctx, metadata) if err == nil { pc.AppendToChains(f) } return pc, err } // SupportUDP implements C.ProxyAdapter func (f *Fallback) SupportUDP() bool { if f.disableUDP { return false } proxy := f.findAliveProxy(false) return proxy.SupportUDP() } // IsL3Protocol implements C.ProxyAdapter func (f *Fallback) IsL3Protocol(metadata *C.Metadata) bool { return f.findAliveProxy(false).IsL3Protocol(metadata) } // MarshalJSON implements C.ProxyAdapter func (f *Fallback) MarshalJSON() ([]byte, error) { all := []string{} for _, proxy := range f.GetProxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ "type": f.Type().String(), "now": f.Now(), "all": all, "testUrl": f.testUrl, "expectedStatus": f.expectedStatus, "fixed": f.selected, "hidden": f.Hidden(), "icon": f.Icon(), }) } // Unwrap implements C.ProxyAdapter func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { proxy := f.findAliveProxy(touch) return proxy } func (f *Fallback) findAliveProxy(touch bool) C.Proxy { proxies := f.GetProxies(touch) for _, proxy := range proxies { if len(f.selected) == 0 { if proxy.AliveForTestUrl(f.testUrl) { return proxy } } else { if proxy.Name() == f.selected { if proxy.AliveForTestUrl(f.testUrl) { return proxy } else { f.selected = "" } } } } return proxies[0] } func (f *Fallback) Set(name string) error { var p C.Proxy for _, proxy := range f.GetProxies(false) { if proxy.Name() == name { p = proxy break } } if p == nil { return errors.New("proxy not exist") } f.selected = name if !p.AliveForTestUrl(f.testUrl) { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000)) defer cancel() expectedStatus, _ := utils.NewUnsignedRanges[uint16](f.expectedStatus) _, _ = p.URLTest(ctx, f.testUrl, expectedStatus) } return nil } func (f *Fallback) ForceSet(name string) { f.selected = name } func (f *Fallback) Providers() []P.ProxyProvider { return f.providers } func (f *Fallback) Proxies() []C.Proxy { return f.GetProxies(false) } func NewFallback(option *GroupCommonOption, providers []P.ProxyProvider) *Fallback { return &Fallback{ GroupBase: NewGroupBase(GroupBaseOption{ Name: option.Name, Type: C.Fallback, Hidden: option.Hidden, Icon: option.Icon, Filter: option.Filter, ExcludeFilter: option.ExcludeFilter, ExcludeType: option.ExcludeType, TestTimeout: option.TestTimeout, MaxFailedTimes: option.MaxFailedTimes, Providers: providers, }), disableUDP: option.DisableUDP, testUrl: option.URL, expectedStatus: option.ExpectedStatus, } } ================================================ FILE: core/Clash.Meta/adapter/outboundgroup/groupbase.go ================================================ package outboundgroup import ( "context" "errors" "fmt" "strings" "sync" "time" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel" "github.com/dlclark/regexp2" "golang.org/x/exp/slices" ) type GroupBase struct { *outbound.Base hidden bool icon string filterRegs []*regexp2.Regexp excludeFilterRegs []*regexp2.Regexp excludeTypeArray []string providers []P.ProxyProvider failedTestMux sync.Mutex failedTimes int failedTime time.Time failedTesting atomic.Bool testTimeout int maxFailedTimes int // for GetProxies getProxiesMutex sync.Mutex providerVersions []uint32 providerProxies []C.Proxy } type GroupBaseOption struct { Name string Type C.AdapterType Hidden bool Icon string Filter string ExcludeFilter string ExcludeType string TestTimeout int MaxFailedTimes int Providers []P.ProxyProvider } func NewGroupBase(opt GroupBaseOption) *GroupBase { var excludeTypeArray []string if opt.ExcludeType != "" { excludeTypeArray = strings.Split(opt.ExcludeType, "|") } var excludeFilterRegs []*regexp2.Regexp if opt.ExcludeFilter != "" { for _, excludeFilter := range strings.Split(opt.ExcludeFilter, "`") { excludeFilterReg := regexp2.MustCompile(excludeFilter, regexp2.None) excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg) } } var filterRegs []*regexp2.Regexp if opt.Filter != "" { for _, filter := range strings.Split(opt.Filter, "`") { filterReg := regexp2.MustCompile(filter, regexp2.None) filterRegs = append(filterRegs, filterReg) } } gb := &GroupBase{ Base: outbound.NewBase(outbound.BaseOption{Name: opt.Name, Type: opt.Type}), hidden: opt.Hidden, icon: opt.Icon, filterRegs: filterRegs, excludeFilterRegs: excludeFilterRegs, excludeTypeArray: excludeTypeArray, providers: opt.Providers, failedTesting: atomic.NewBool(false), testTimeout: opt.TestTimeout, maxFailedTimes: opt.MaxFailedTimes, } if gb.testTimeout == 0 { gb.testTimeout = 5000 } if gb.maxFailedTimes == 0 { gb.maxFailedTimes = 5 } return gb } func (gb *GroupBase) Hidden() bool { return gb.hidden } func (gb *GroupBase) Icon() string { return gb.icon } func (gb *GroupBase) Touch() { for _, pd := range gb.providers { pd.Touch() } } func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { providerVersions := make([]uint32, len(gb.providers)) for i, pd := range gb.providers { if touch { // touch first pd.Touch() } providerVersions[i] = pd.Version() } // thread safe gb.getProxiesMutex.Lock() defer gb.getProxiesMutex.Unlock() // return the cached proxies if version not changed if slices.Equal(providerVersions, gb.providerVersions) { return gb.providerProxies } var proxies []C.Proxy if len(gb.filterRegs) == 0 { for _, pd := range gb.providers { proxies = append(proxies, pd.Proxies()...) } } else { for _, pd := range gb.providers { if pd.VehicleType() == P.Compatible { // compatible provider unneeded filter proxies = append(proxies, pd.Proxies()...) continue } var newProxies []C.Proxy proxiesSet := map[string]struct{}{} for _, filterReg := range gb.filterRegs { for _, p := range pd.Proxies() { name := p.Name() if mat, _ := filterReg.MatchString(name); mat { if _, ok := proxiesSet[name]; !ok { proxiesSet[name] = struct{}{} newProxies = append(newProxies, p) } } } } proxies = append(proxies, newProxies...) } } // Multiple filers means that proxies are sorted in the order in which the filers appear. // Although the filter has been performed once in the previous process, // when there are multiple providers, the array needs to be reordered as a whole. if len(gb.providers) > 1 && len(gb.filterRegs) > 1 { var newProxies []C.Proxy proxiesSet := map[string]struct{}{} for _, filterReg := range gb.filterRegs { for _, p := range proxies { name := p.Name() if mat, _ := filterReg.MatchString(name); mat { if _, ok := proxiesSet[name]; !ok { proxiesSet[name] = struct{}{} newProxies = append(newProxies, p) } } } } for _, p := range proxies { // add not matched proxies at the end name := p.Name() if _, ok := proxiesSet[name]; !ok { proxiesSet[name] = struct{}{} newProxies = append(newProxies, p) } } proxies = newProxies } if len(gb.excludeFilterRegs) > 0 { var newProxies []C.Proxy LOOP1: for _, p := range proxies { name := p.Name() for _, excludeFilterReg := range gb.excludeFilterRegs { if mat, _ := excludeFilterReg.MatchString(name); mat { continue LOOP1 } } newProxies = append(newProxies, p) } proxies = newProxies } if gb.excludeTypeArray != nil { var newProxies []C.Proxy LOOP2: for _, p := range proxies { mType := p.Type().String() for _, excludeType := range gb.excludeTypeArray { if strings.EqualFold(mType, excludeType) { continue LOOP2 } } newProxies = append(newProxies, p) } proxies = newProxies } if len(proxies) == 0 { return []C.Proxy{tunnel.Proxies()["COMPATIBLE"]} } // only cache when proxies not empty gb.providerVersions = providerVersions gb.providerProxies = proxies return proxies } func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) { var wg sync.WaitGroup var lock sync.Mutex mp := map[string]uint16{} proxies := gb.GetProxies(false) for _, proxy := range proxies { proxy := proxy wg.Add(1) go func() { delay, err := proxy.URLTest(ctx, url, expectedStatus) if err == nil { lock.Lock() mp[proxy.Name()] = delay lock.Unlock() } wg.Done() }() } wg.Wait() if len(mp) == 0 { return mp, fmt.Errorf("get delay: all proxies timeout") } else { return mp, nil } } func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error, fn func()) { if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass || adapterType == C.RejectDrop { return } if errors.Is(err, C.ErrNotSupport) { return } go func() { if strings.Contains(err.Error(), "connection refused") { fn() return } gb.failedTestMux.Lock() defer gb.failedTestMux.Unlock() gb.failedTimes++ if gb.failedTimes == 1 { log.Debugln("ProxyGroup: %s first failed", gb.Name()) gb.failedTime = time.Now() } else { if time.Since(gb.failedTime) > time.Duration(gb.testTimeout)*time.Millisecond { gb.failedTimes = 0 return } log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes) if gb.failedTimes >= gb.maxFailedTimes { log.Warnln("because %s failed multiple times, activate health check", gb.Name()) fn() } } }() } func (gb *GroupBase) healthCheck() { if gb.failedTesting.Load() { return } gb.failedTesting.Store(true) wg := sync.WaitGroup{} for _, proxyProvider := range gb.providers { wg.Add(1) proxyProvider := proxyProvider go func() { defer wg.Done() proxyProvider.HealthCheck() }() } wg.Wait() gb.failedTesting.Store(false) gb.failedTimes = 0 } func (gb *GroupBase) onDialSuccess() { if !gb.failedTesting.Load() { gb.failedTimes = 0 } } ================================================ FILE: core/Clash.Meta/adapter/outboundgroup/loadbalance.go ================================================ package outboundgroup import ( "context" "encoding/json" "errors" "fmt" "net" "sync" "time" "github.com/metacubex/mihomo/common/callback" "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "golang.org/x/net/publicsuffix" ) type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy type LoadBalance struct { *GroupBase disableUDP bool strategyFn strategyFn testUrl string expectedStatus string } var errStrategy = errors.New("unsupported strategy") func parseStrategy(config map[string]any) string { if strategy, ok := config["strategy"].(string); ok { return strategy } return "consistent-hashing" } func getKey(metadata *C.Metadata) string { if metadata == nil { return "" } if metadata.Host != "" { // ip host if ip := net.ParseIP(metadata.Host); ip != nil { return metadata.Host } if etld, err := publicsuffix.EffectiveTLDPlusOne(metadata.Host); err == nil { return etld } } if !metadata.DstIP.IsValid() { return "" } return metadata.DstIP.String() } func getKeyWithSrcAndDst(metadata *C.Metadata) string { dst := getKey(metadata) src := "" if metadata != nil { src = metadata.SrcIP.String() } return fmt.Sprintf("%s%s", src, dst) } func jumpHash(key uint64, buckets int32) int32 { var b, j int64 for j < int64(buckets) { b = j key = key*2862933555777941757 + 1 j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1))) } return int32(b) } // DialContext implements C.ProxyAdapter func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { proxy := lb.Unwrap(metadata, true) c, err = proxy.DialContext(ctx, metadata) if err == nil { c.AppendToChains(lb) } else { lb.onDialFailed(proxy.Type(), err, lb.healthCheck) } if N.NeedHandshake(c) { c = callback.NewFirstWriteCallBackConn(c, func(err error) { if err == nil { lb.onDialSuccess() } else { lb.onDialFailed(proxy.Type(), err, lb.healthCheck) } }) } return } // ListenPacketContext implements C.ProxyAdapter func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (pc C.PacketConn, err error) { defer func() { if err == nil { pc.AppendToChains(lb) } }() proxy := lb.Unwrap(metadata, true) return proxy.ListenPacketContext(ctx, metadata) } // SupportUDP implements C.ProxyAdapter func (lb *LoadBalance) SupportUDP() bool { return !lb.disableUDP } // IsL3Protocol implements C.ProxyAdapter func (lb *LoadBalance) IsL3Protocol(metadata *C.Metadata) bool { return lb.Unwrap(metadata, false).IsL3Protocol(metadata) } func strategyRoundRobin(url string) strategyFn { idx := 0 idxMutex := sync.Mutex{} return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { idxMutex.Lock() defer idxMutex.Unlock() i := 0 length := len(proxies) if touch { defer func() { idx = (idx + i) % length }() } for ; i < length; i++ { id := (idx + i) % length proxy := proxies[id] if proxy.AliveForTestUrl(url) { i++ return proxy } } return proxies[0] } } func strategyConsistentHashing(url string) strategyFn { maxRetry := 5 return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { key := utils.MapHash(getKey(metadata)) buckets := int32(len(proxies)) for i := 0; i < maxRetry; i, key = i+1, key+1 { idx := jumpHash(key, buckets) proxy := proxies[idx] if proxy.AliveForTestUrl(url) { return proxy } } // when availability is poor, traverse the entire list to get the available nodes for _, proxy := range proxies { if proxy.AliveForTestUrl(url) { return proxy } } return proxies[0] } } func strategyStickySessions(url string) strategyFn { ttl := time.Minute * 10 maxRetry := 5 lruCache := lru.New[uint64, int]( lru.WithAge[uint64, int](int64(ttl.Seconds())), lru.WithSize[uint64, int](1000)) return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { key := utils.MapHash(getKeyWithSrcAndDst(metadata)) length := len(proxies) idx, has := lruCache.Get(key) if !has || idx >= length { idx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length))) } nowIdx := idx for i := 1; i < maxRetry; i++ { proxy := proxies[nowIdx] if proxy.AliveForTestUrl(url) { if !has || nowIdx != idx { lruCache.Set(key, nowIdx) } return proxy } else { nowIdx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length))) } } lruCache.Set(key, 0) return proxies[0] } } // Unwrap implements C.ProxyAdapter func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { proxies := lb.GetProxies(touch) return lb.strategyFn(proxies, metadata, touch) } // MarshalJSON implements C.ProxyAdapter func (lb *LoadBalance) MarshalJSON() ([]byte, error) { var all []string for _, proxy := range lb.GetProxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ "type": lb.Type().String(), "all": all, "testUrl": lb.testUrl, "expectedStatus": lb.expectedStatus, "hidden": lb.Hidden(), "icon": lb.Icon(), }) } func (lb *LoadBalance) Providers() []P.ProxyProvider { return lb.providers } func (lb *LoadBalance) Proxies() []C.Proxy { return lb.GetProxies(false) } func (lb *LoadBalance) Now() string { return "" } func NewLoadBalance(option *GroupCommonOption, providers []P.ProxyProvider, strategy string) (lb *LoadBalance, err error) { var strategyFn strategyFn switch strategy { case "consistent-hashing": strategyFn = strategyConsistentHashing(option.URL) case "round-robin": strategyFn = strategyRoundRobin(option.URL) case "sticky-sessions": strategyFn = strategyStickySessions(option.URL) default: return nil, fmt.Errorf("%w: %s", errStrategy, strategy) } return &LoadBalance{ GroupBase: NewGroupBase(GroupBaseOption{ Name: option.Name, Type: C.LoadBalance, Hidden: option.Hidden, Icon: option.Icon, Filter: option.Filter, ExcludeFilter: option.ExcludeFilter, ExcludeType: option.ExcludeType, TestTimeout: option.TestTimeout, MaxFailedTimes: option.MaxFailedTimes, Providers: providers, }), strategyFn: strategyFn, disableUDP: option.DisableUDP, testUrl: option.URL, expectedStatus: option.ExpectedStatus, }, nil } ================================================ FILE: core/Clash.Meta/adapter/outboundgroup/parser.go ================================================ package outboundgroup import ( "errors" "fmt" "strings" "github.com/dlclark/regexp2" "github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" ) var ( errFormat = errors.New("format error") errType = errors.New("unsupported type") errMissProxy = errors.New("`use` or `proxies` missing") errDuplicateProvider = errors.New("duplicate provider name") ) type GroupCommonOption struct { Name string `group:"name"` Type string `group:"type"` Proxies []string `group:"proxies,omitempty"` Use []string `group:"use,omitempty"` URL string `group:"url,omitempty"` Interval int `group:"interval,omitempty"` TestTimeout int `group:"timeout,omitempty"` MaxFailedTimes int `group:"max-failed-times,omitempty"` Lazy bool `group:"lazy,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"` Filter string `group:"filter,omitempty"` ExcludeFilter string `group:"exclude-filter,omitempty"` ExcludeType string `group:"exclude-type,omitempty"` ExpectedStatus string `group:"expected-status,omitempty"` IncludeAll bool `group:"include-all,omitempty"` IncludeAllProxies bool `group:"include-all-proxies,omitempty"` IncludeAllProviders bool `group:"include-all-providers,omitempty"` Hidden bool `group:"hidden,omitempty"` Icon string `group:"icon,omitempty"` } func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]P.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) { decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) groupOption := &GroupCommonOption{ Lazy: true, } if err := decoder.Decode(config, groupOption); err != nil { return nil, errFormat } if groupOption.Type == "" || groupOption.Name == "" { return nil, errFormat } if _, ok := config["routing-mark"]; ok { log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name) } if _, ok := config["interface-name"]; ok { log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name) } if _, ok := config["dialer-proxy"]; ok { log.Errorln("The group [%s] with dialer-proxy configuration is not allowed, please set it directly on the proxy instead", groupOption.Name) } groupName := groupOption.Name providers := []P.ProxyProvider{} if groupOption.IncludeAll { groupOption.IncludeAllProviders = true groupOption.IncludeAllProxies = true } if groupOption.IncludeAllProviders { groupOption.Use = AllProviders } if groupOption.IncludeAllProxies { if groupOption.Filter != "" { var filterRegs []*regexp2.Regexp for _, filter := range strings.Split(groupOption.Filter, "`") { filterReg := regexp2.MustCompile(filter, regexp2.None) filterRegs = append(filterRegs, filterReg) } for _, p := range AllProxies { for _, filterReg := range filterRegs { if mat, _ := filterReg.MatchString(p); mat { groupOption.Proxies = append(groupOption.Proxies, p) } } } } else { groupOption.Proxies = append(groupOption.Proxies, AllProxies...) } if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { groupOption.Proxies = []string{"COMPATIBLE"} } } if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { return nil, fmt.Errorf("%s: %w", groupName, errMissProxy) } expectedStatus, err := utils.NewUnsignedRanges[uint16](groupOption.ExpectedStatus) if err != nil { return nil, fmt.Errorf("%s: %w", groupName, err) } status := strings.TrimSpace(groupOption.ExpectedStatus) if status == "" { status = "*" } groupOption.ExpectedStatus = status if len(groupOption.Use) != 0 { PDs, err := getProviders(providersMap, groupOption.Use) if err != nil { return nil, fmt.Errorf("%s: %w", groupName, err) } // if test URL is empty, use the first health check URL of providers if groupOption.URL == "" { for _, pd := range PDs { if pd.HealthCheckURL() != "" { groupOption.URL = pd.HealthCheckURL() break } } if groupOption.URL == "" { groupOption.URL = C.DefaultTestURL } } else { addTestUrlToProviders(PDs, groupOption.URL, expectedStatus, groupOption.Filter, uint(groupOption.Interval)) } providers = append(providers, PDs...) } if len(groupOption.Proxies) != 0 { ps, err := getProxies(proxyMap, groupOption.Proxies) if err != nil { return nil, fmt.Errorf("%s: %w", groupName, err) } if _, ok := providersMap[groupName]; ok { return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider) } if groupOption.URL == "" { groupOption.URL = C.DefaultTestURL } // select don't need auto health check if groupOption.Type != "select" && groupOption.Type != "relay" { if groupOption.Interval == 0 { groupOption.Interval = 300 } } hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus) pd, err := provider.NewCompatibleProvider(groupName, ps, hc) if err != nil { return nil, fmt.Errorf("%s: %w", groupName, err) } providers = append([]P.ProxyProvider{pd}, providers...) providersMap[groupName] = pd } var group C.ProxyAdapter switch groupOption.Type { case "url-test": opts := parseURLTestOption(config) group = NewURLTest(groupOption, providers, opts...) case "select": group = NewSelector(groupOption, providers) case "fallback": group = NewFallback(groupOption, providers) case "load-balance": strategy := parseStrategy(config) return NewLoadBalance(groupOption, providers, strategy) case "relay": return nil, fmt.Errorf("%w: The group [%s] with relay type was removed, please using dialer-proxy instead", errType, groupName) default: return nil, fmt.Errorf("%w: %s", errType, groupOption.Type) } return group, nil } func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) { var ps []C.Proxy for _, name := range list { p, ok := mapping[name] if !ok { return nil, fmt.Errorf("'%s' not found", name) } ps = append(ps, p) } return ps, nil } func getProviders(mapping map[string]P.ProxyProvider, list []string) ([]P.ProxyProvider, error) { var ps []P.ProxyProvider for _, name := range list { p, ok := mapping[name] if !ok { return nil, fmt.Errorf("'%s' not found", name) } if p.VehicleType() == P.Compatible { return nil, fmt.Errorf("proxy group %s can't contains in `use`", name) } ps = append(ps, p) } return ps, nil } func addTestUrlToProviders(providers []P.ProxyProvider, url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { if len(providers) == 0 || len(url) == 0 { return } for _, pd := range providers { pd.RegisterHealthCheckTask(url, expectedStatus, filter, interval) } } ================================================ FILE: core/Clash.Meta/adapter/outboundgroup/selector.go ================================================ package outboundgroup import ( "context" "encoding/json" "errors" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" ) type Selector struct { *GroupBase disableUDP bool selected string testUrl string } // DialContext implements C.ProxyAdapter func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { c, err := s.selectedProxy(true).DialContext(ctx, metadata) if err == nil { c.AppendToChains(s) } return c, err } // ListenPacketContext implements C.ProxyAdapter func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata) if err == nil { pc.AppendToChains(s) } return pc, err } // SupportUDP implements C.ProxyAdapter func (s *Selector) SupportUDP() bool { if s.disableUDP { return false } return s.selectedProxy(false).SupportUDP() } // IsL3Protocol implements C.ProxyAdapter func (s *Selector) IsL3Protocol(metadata *C.Metadata) bool { return s.selectedProxy(false).IsL3Protocol(metadata) } // MarshalJSON implements C.ProxyAdapter func (s *Selector) MarshalJSON() ([]byte, error) { all := []string{} for _, proxy := range s.GetProxies(false) { all = append(all, proxy.Name()) } // When testurl is the default value // do not append a value to ensure that the web dashboard follows the settings of the dashboard var url string if s.testUrl != C.DefaultTestURL { url = s.testUrl } return json.Marshal(map[string]any{ "type": s.Type().String(), "now": s.Now(), "all": all, "testUrl": url, "hidden": s.Hidden(), "icon": s.Icon(), }) } func (s *Selector) Now() string { return s.selectedProxy(false).Name() } func (s *Selector) Set(name string) error { for _, proxy := range s.GetProxies(false) { if proxy.Name() == name { s.selected = name return nil } } return errors.New("proxy not exist") } func (s *Selector) ForceSet(name string) { s.selected = name } // Unwrap implements C.ProxyAdapter func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { return s.selectedProxy(touch) } func (s *Selector) selectedProxy(touch bool) C.Proxy { proxies := s.GetProxies(touch) for _, proxy := range proxies { if proxy.Name() == s.selected { return proxy } } return proxies[0] } func (s *Selector) Providers() []P.ProxyProvider { return s.providers } func (s *Selector) Proxies() []C.Proxy { return s.GetProxies(false) } func NewSelector(option *GroupCommonOption, providers []P.ProxyProvider) *Selector { return &Selector{ GroupBase: NewGroupBase(GroupBaseOption{ Name: option.Name, Type: C.Selector, Hidden: option.Hidden, Icon: option.Icon, Filter: option.Filter, ExcludeFilter: option.ExcludeFilter, ExcludeType: option.ExcludeType, TestTimeout: option.TestTimeout, MaxFailedTimes: option.MaxFailedTimes, Providers: providers, }), selected: "COMPATIBLE", disableUDP: option.DisableUDP, testUrl: option.URL, } } ================================================ FILE: core/Clash.Meta/adapter/outboundgroup/urltest.go ================================================ package outboundgroup import ( "context" "encoding/json" "errors" "time" "github.com/metacubex/mihomo/common/callback" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/singledo" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" ) type urlTestOption func(*URLTest) func urlTestWithTolerance(tolerance uint16) urlTestOption { return func(u *URLTest) { u.tolerance = tolerance } } type URLTest struct { *GroupBase selected string testUrl string expectedStatus string tolerance uint16 disableUDP bool fastNode C.Proxy fastSingle *singledo.Single[C.Proxy] } func (u *URLTest) Now() string { return u.fast(false).Name() } func (u *URLTest) Set(name string) error { var p C.Proxy for _, proxy := range u.GetProxies(false) { if proxy.Name() == name { p = proxy break } } if p == nil { return errors.New("proxy not exist") } u.ForceSet(name) return nil } func (u *URLTest) ForceSet(name string) { u.selected = name u.fastSingle.Reset() } // DialContext implements C.ProxyAdapter func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { proxy := u.fast(true) c, err = proxy.DialContext(ctx, metadata) if err == nil { c.AppendToChains(u) } else { u.onDialFailed(proxy.Type(), err, u.healthCheck) } if N.NeedHandshake(c) { c = callback.NewFirstWriteCallBackConn(c, func(err error) { if err == nil { u.onDialSuccess() } else { u.onDialFailed(proxy.Type(), err, u.healthCheck) } }) } return c, err } // ListenPacketContext implements C.ProxyAdapter func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { proxy := u.fast(true) pc, err := proxy.ListenPacketContext(ctx, metadata) if err == nil { pc.AppendToChains(u) } else { u.onDialFailed(proxy.Type(), err, u.healthCheck) } return pc, err } // Unwrap implements C.ProxyAdapter func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { return u.fast(touch) } func (u *URLTest) healthCheck() { u.fastSingle.Reset() u.GroupBase.healthCheck() u.fastSingle.Reset() } func (u *URLTest) fast(touch bool) C.Proxy { elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) { proxies := u.GetProxies(touch) if u.selected != "" { for _, proxy := range proxies { if !proxy.AliveForTestUrl(u.testUrl) { continue } if proxy.Name() == u.selected { u.fastNode = proxy return proxy, nil } } } fast := proxies[0] minDelay := fast.LastDelayForTestUrl(u.testUrl) fastNotExist := true for _, proxy := range proxies[1:] { if u.fastNode != nil && proxy.Name() == u.fastNode.Name() { fastNotExist = false } if !proxy.AliveForTestUrl(u.testUrl) { continue } delay := proxy.LastDelayForTestUrl(u.testUrl) if delay < minDelay { fast = proxy minDelay = delay } } // tolerance if u.fastNode == nil || fastNotExist || !u.fastNode.AliveForTestUrl(u.testUrl) || u.fastNode.LastDelayForTestUrl(u.testUrl) > fast.LastDelayForTestUrl(u.testUrl)+u.tolerance { u.fastNode = fast } return u.fastNode, nil }) if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again u.Touch() } return elm } // SupportUDP implements C.ProxyAdapter func (u *URLTest) SupportUDP() bool { if u.disableUDP { return false } return u.fast(false).SupportUDP() } // IsL3Protocol implements C.ProxyAdapter func (u *URLTest) IsL3Protocol(metadata *C.Metadata) bool { return u.fast(false).IsL3Protocol(metadata) } // MarshalJSON implements C.ProxyAdapter func (u *URLTest) MarshalJSON() ([]byte, error) { all := []string{} for _, proxy := range u.GetProxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ "type": u.Type().String(), "now": u.Now(), "all": all, "testUrl": u.testUrl, "expectedStatus": u.expectedStatus, "fixed": u.selected, "hidden": u.Hidden(), "icon": u.Icon(), }) } func (u *URLTest) Providers() []P.ProxyProvider { return u.providers } func (u *URLTest) Proxies() []C.Proxy { return u.GetProxies(false) } func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) { return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus) } func parseURLTestOption(config map[string]any) []urlTestOption { opts := []urlTestOption{} // tolerance if elm, ok := config["tolerance"]; ok { if tolerance, ok := elm.(int); ok { opts = append(opts, urlTestWithTolerance(uint16(tolerance))) } } return opts } func NewURLTest(option *GroupCommonOption, providers []P.ProxyProvider, options ...urlTestOption) *URLTest { urlTest := &URLTest{ GroupBase: NewGroupBase(GroupBaseOption{ Name: option.Name, Type: C.URLTest, Hidden: option.Hidden, Icon: option.Icon, Filter: option.Filter, ExcludeFilter: option.ExcludeFilter, ExcludeType: option.ExcludeType, TestTimeout: option.TestTimeout, MaxFailedTimes: option.MaxFailedTimes, Providers: providers, }), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), disableUDP: option.DisableUDP, testUrl: option.URL, expectedStatus: option.ExpectedStatus, } for _, option := range options { option(urlTest) } return urlTest } ================================================ FILE: core/Clash.Meta/adapter/outboundgroup/util.go ================================================ package outboundgroup import ( "context" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" ) type ProxyGroup interface { C.ProxyAdapter Providers() []P.ProxyProvider Proxies() []C.Proxy Now() string Touch() Hidden() bool Icon() string URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error) } var _ ProxyGroup = (*Fallback)(nil) var _ ProxyGroup = (*LoadBalance)(nil) var _ ProxyGroup = (*URLTest)(nil) var _ ProxyGroup = (*Selector)(nil) type SelectAble interface { Set(string) error ForceSet(name string) } var _ SelectAble = (*Fallback)(nil) var _ SelectAble = (*URLTest)(nil) var _ SelectAble = (*Selector)(nil) ================================================ FILE: core/Clash.Meta/adapter/parser.go ================================================ package adapter import ( "fmt" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/common/structure" C "github.com/metacubex/mihomo/constant" ) func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error) { decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) proxyType, existType := mapping["type"].(string) if !existType { return nil, fmt.Errorf("missing type") } opt := applyProxyOptions(options...) basicOption := outbound.BasicOption{ DialerForAPI: opt.DialerForAPI, ProviderName: opt.ProviderName, } var ( proxy outbound.ProxyAdapter err error ) switch proxyType { case "ss": ssOption := &outbound.ShadowSocksOption{BasicOption: basicOption} err = decoder.Decode(mapping, ssOption) if err != nil { break } proxy, err = outbound.NewShadowSocks(*ssOption) case "ssr": ssrOption := &outbound.ShadowSocksROption{BasicOption: basicOption} err = decoder.Decode(mapping, ssrOption) if err != nil { break } proxy, err = outbound.NewShadowSocksR(*ssrOption) case "socks5": socksOption := &outbound.Socks5Option{BasicOption: basicOption} err = decoder.Decode(mapping, socksOption) if err != nil { break } proxy, err = outbound.NewSocks5(*socksOption) case "http": httpOption := &outbound.HttpOption{BasicOption: basicOption} err = decoder.Decode(mapping, httpOption) if err != nil { break } proxy, err = outbound.NewHttp(*httpOption) case "vmess": vmessOption := &outbound.VmessOption{BasicOption: basicOption} err = decoder.Decode(mapping, vmessOption) if err != nil { break } proxy, err = outbound.NewVmess(*vmessOption) case "vless": vlessOption := &outbound.VlessOption{BasicOption: basicOption} err = decoder.Decode(mapping, vlessOption) if err != nil { break } proxy, err = outbound.NewVless(*vlessOption) case "snell": snellOption := &outbound.SnellOption{BasicOption: basicOption} err = decoder.Decode(mapping, snellOption) if err != nil { break } proxy, err = outbound.NewSnell(*snellOption) case "trojan": trojanOption := &outbound.TrojanOption{BasicOption: basicOption} err = decoder.Decode(mapping, trojanOption) if err != nil { break } proxy, err = outbound.NewTrojan(*trojanOption) case "hysteria": hyOption := &outbound.HysteriaOption{BasicOption: basicOption} err = decoder.Decode(mapping, hyOption) if err != nil { break } proxy, err = outbound.NewHysteria(*hyOption) case "hysteria2": hyOption := &outbound.Hysteria2Option{BasicOption: basicOption} err = decoder.Decode(mapping, hyOption) if err != nil { break } proxy, err = outbound.NewHysteria2(*hyOption) case "wireguard": wgOption := &outbound.WireGuardOption{BasicOption: basicOption} err = decoder.Decode(mapping, wgOption) if err != nil { break } proxy, err = outbound.NewWireGuard(*wgOption) case "tuic": tuicOption := &outbound.TuicOption{BasicOption: basicOption} err = decoder.Decode(mapping, tuicOption) if err != nil { break } proxy, err = outbound.NewTuic(*tuicOption) case "direct": directOption := &outbound.DirectOption{BasicOption: basicOption} err = decoder.Decode(mapping, directOption) if err != nil { break } proxy = outbound.NewDirectWithOption(*directOption) case "dns": dnsOptions := &outbound.DnsOption{BasicOption: basicOption} err = decoder.Decode(mapping, dnsOptions) if err != nil { break } proxy = outbound.NewDnsWithOption(*dnsOptions) case "reject": rejectOption := &outbound.RejectOption{BasicOption: basicOption} err = decoder.Decode(mapping, rejectOption) if err != nil { break } proxy = outbound.NewRejectWithOption(*rejectOption) case "ssh": sshOption := &outbound.SshOption{BasicOption: basicOption} err = decoder.Decode(mapping, sshOption) if err != nil { break } proxy, err = outbound.NewSsh(*sshOption) case "mieru": mieruOption := &outbound.MieruOption{BasicOption: basicOption} err = decoder.Decode(mapping, mieruOption) if err != nil { break } proxy, err = outbound.NewMieru(*mieruOption) case "anytls": anytlsOption := &outbound.AnyTLSOption{BasicOption: basicOption} err = decoder.Decode(mapping, anytlsOption) if err != nil { break } proxy, err = outbound.NewAnyTLS(*anytlsOption) case "sudoku": sudokuOption := &outbound.SudokuOption{BasicOption: basicOption} err = decoder.Decode(mapping, sudokuOption) if err != nil { break } proxy, err = outbound.NewSudoku(*sudokuOption) case "masque": masqueOption := &outbound.MasqueOption{BasicOption: basicOption} err = decoder.Decode(mapping, masqueOption) if err != nil { break } proxy, err = outbound.NewMasque(*masqueOption) case "trusttunnel": trustTunnelOption := &outbound.TrustTunnelOption{BasicOption: basicOption} err = decoder.Decode(mapping, trustTunnelOption) if err != nil { break } proxy, err = outbound.NewTrustTunnel(*trustTunnelOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } if err != nil { return nil, err } if muxMapping, muxExist := mapping["smux"].(map[string]any); muxExist { muxOption := &outbound.SingMuxOption{} err = decoder.Decode(muxMapping, muxOption) if err != nil { return nil, err } if muxOption.Enabled { proxy, err = outbound.NewSingMux(*muxOption, proxy) if err != nil { return nil, err } } } proxy = outbound.NewAutoCloseProxyAdapter(proxy) return NewProxy(proxy), nil } type proxyOption struct { DialerForAPI C.Dialer ProviderName string } func applyProxyOptions(options ...ProxyOption) proxyOption { opt := proxyOption{} for _, o := range options { o(&opt) } return opt } type ProxyOption func(opt *proxyOption) func WithDialerForAPI(dialer C.Dialer) ProxyOption { return func(opt *proxyOption) { opt.DialerForAPI = dialer } } func WithProviderName(name string) ProxyOption { return func(opt *proxyOption) { opt.ProviderName = name } } ================================================ FILE: core/Clash.Meta/adapter/patch.go ================================================ package adapter type UrlTestCheck func(url string, name string, delay uint16) var UrlTestHook UrlTestCheck ================================================ FILE: core/Clash.Meta/adapter/provider/healthcheck.go ================================================ package provider import ( "context" "strings" "sync" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/singledo" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/dlclark/regexp2" "golang.org/x/sync/errgroup" ) type HealthCheckOption struct { URL string Interval uint } type extraOption struct { expectedStatus utils.IntRanges[uint16] filters map[string]struct{} } type HealthCheck struct { ctx context.Context ctxCancel context.CancelFunc url string extra map[string]*extraOption mu sync.Mutex proxies []C.Proxy interval time.Duration lazy bool expectedStatus utils.IntRanges[uint16] lastTouch atomic.TypedValue[time.Time] singleDo *singledo.Single[struct{}] timeout time.Duration } func (hc *HealthCheck) process() { ticker := time.NewTicker(hc.interval) go hc.check() for { select { case <-ticker.C: lastTouch := hc.lastTouch.Load() since := time.Since(lastTouch) if !hc.lazy || since < hc.interval { hc.check() } else { log.Debugln("Skip once health check because we are lazy") } case <-hc.ctx.Done(): ticker.Stop() return } } } func (hc *HealthCheck) setProxies(proxies []C.Proxy) { hc.proxies = proxies } func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { url = strings.TrimSpace(url) if len(url) == 0 || url == hc.url { log.Debugln("ignore invalid health check url: %s", url) return } hc.mu.Lock() defer hc.mu.Unlock() // if the provider has not set up health checks, then modify it to be the same as the group's interval if hc.interval == 0 { hc.interval = time.Duration(interval) * time.Second } if hc.extra == nil { hc.extra = make(map[string]*extraOption) } // prioritize the use of previously registered configurations, especially those from provider if _, ok := hc.extra[url]; ok { // provider default health check does not set filter if url != hc.url && len(filter) != 0 { splitAndAddFiltersToExtra(filter, hc.extra[url]) } log.Debugln("health check url: %s exists", url) return } option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus} splitAndAddFiltersToExtra(filter, option) hc.extra[url] = option } func splitAndAddFiltersToExtra(filter string, option *extraOption) { filter = strings.TrimSpace(filter) if len(filter) != 0 { for _, regex := range strings.Split(filter, "`") { regex = strings.TrimSpace(regex) if len(regex) != 0 { option.filters[regex] = struct{}{} } } } } func (hc *HealthCheck) auto() bool { return hc.interval != 0 } func (hc *HealthCheck) touch() { hc.lastTouch.Store(time.Now()) } func (hc *HealthCheck) check() { if len(hc.proxies) == 0 { return } _, _, _ = hc.singleDo.Do(func() (struct{}, error) { id := utils.NewUUIDV4().String() log.Debugln("Start New Health Checking {%s}", id) b := new(errgroup.Group) b.SetLimit(10) // execute default health check option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus} hc.execute(b, hc.url, id, option) // execute extra health check if len(hc.extra) != 0 { for url, option := range hc.extra { hc.execute(b, url, id, option) } } _ = b.Wait() log.Debugln("Finish A Health Checking {%s}", id) return struct{}{}, nil }) } func (hc *HealthCheck) execute(b *errgroup.Group, url, uid string, option *extraOption) { url = strings.TrimSpace(url) if len(url) == 0 { log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid) return } var filterReg *regexp2.Regexp var expectedStatus utils.IntRanges[uint16] if option != nil { expectedStatus = option.expectedStatus if len(option.filters) != 0 { filters := make([]string, 0, len(option.filters)) for filter := range option.filters { filters = append(filters, filter) } filterReg = regexp2.MustCompile(strings.Join(filters, "|"), regexp2.None) } } for _, proxy := range hc.proxies { // skip proxies that do not require health check if filterReg != nil { if match, _ := filterReg.MatchString(proxy.Name()); !match { continue } } p := proxy b.Go(func() error { ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout) defer cancel() log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid) _, _ = p.URLTest(ctx, url, expectedStatus) log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid) return nil }) } } func (hc *HealthCheck) close() { hc.ctxCancel() } func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck { if url == "" { expectedStatus = nil interval = 0 } if timeout == 0 { timeout = 5000 } ctx, cancel := context.WithCancel(context.Background()) return &HealthCheck{ ctx: ctx, ctxCancel: cancel, proxies: proxies, url: url, timeout: time.Duration(timeout) * time.Millisecond, extra: map[string]*extraOption{}, interval: time.Duration(interval) * time.Second, lazy: lazy, expectedStatus: expectedStatus, singleDo: singledo.NewSingle[struct{}](time.Second), } } ================================================ FILE: core/Clash.Meta/adapter/provider/override.go ================================================ package provider import ( "encoding" "fmt" "github.com/dlclark/regexp2" ) type overrideSchema struct { TFO *bool `provider:"tfo,omitempty"` MPTcp *bool `provider:"mptcp,omitempty"` UDP *bool `provider:"udp,omitempty"` UDPOverTCP *bool `provider:"udp-over-tcp,omitempty"` Up *string `provider:"up,omitempty"` Down *string `provider:"down,omitempty"` DialerProxy *string `provider:"dialer-proxy,omitempty"` SkipCertVerify *bool `provider:"skip-cert-verify,omitempty"` Interface *string `provider:"interface-name,omitempty"` RoutingMark *int `provider:"routing-mark,omitempty"` IPVersion *string `provider:"ip-version,omitempty"` AdditionalPrefix *string `provider:"additional-prefix,omitempty"` AdditionalSuffix *string `provider:"additional-suffix,omitempty"` ProxyName []overrideProxyNameSchema `provider:"proxy-name,omitempty"` } type overrideProxyNameSchema struct { // matching expression for regex replacement Pattern *regexp2.Regexp `provider:"pattern"` // the new content after regex matching Target string `provider:"target"` } var _ encoding.TextUnmarshaler = (*regexp2.Regexp)(nil) // ensure *regexp2.Regexp can decode direct by structure package func (o *overrideSchema) Apply(mapping map[string]any) error { if o.TFO != nil { mapping["tfo"] = *o.TFO } if o.MPTcp != nil { mapping["mptcp"] = *o.MPTcp } if o.UDP != nil { mapping["udp"] = *o.UDP } if o.UDPOverTCP != nil { mapping["udp-over-tcp"] = *o.UDPOverTCP } if o.Up != nil { mapping["up"] = *o.Up } if o.Down != nil { mapping["down"] = *o.Down } if o.DialerProxy != nil { mapping["dialer-proxy"] = *o.DialerProxy } if o.SkipCertVerify != nil { mapping["skip-cert-verify"] = *o.SkipCertVerify } if o.Interface != nil { mapping["interface-name"] = *o.Interface } if o.RoutingMark != nil { mapping["routing-mark"] = *o.RoutingMark } if o.IPVersion != nil { mapping["ip-version"] = *o.IPVersion } for _, expr := range o.ProxyName { name := mapping["name"].(string) newName, err := expr.Pattern.Replace(name, expr.Target, 0, -1) if err != nil { return fmt.Errorf("proxy name replace error: %w", err) } mapping["name"] = newName } if o.AdditionalPrefix != nil { mapping["name"] = fmt.Sprintf("%s%s", *o.AdditionalPrefix, mapping["name"]) } if o.AdditionalSuffix != nil { mapping["name"] = fmt.Sprintf("%s%s", mapping["name"], *o.AdditionalSuffix) } return nil } ================================================ FILE: core/Clash.Meta/adapter/provider/parser.go ================================================ package provider import ( "errors" "fmt" "time" "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" ) var ( errVehicleType = errors.New("unsupport vehicle type") ) type healthCheckSchema struct { Enable bool `provider:"enable"` URL string `provider:"url,omitempty"` Interval int `provider:"interval,omitempty"` TestTimeout int `provider:"timeout,omitempty"` Lazy bool `provider:"lazy,omitempty"` ExpectedStatus string `provider:"expected-status,omitempty"` } type proxyProviderSchema struct { Type string `provider:"type"` Path string `provider:"path,omitempty"` URL string `provider:"url,omitempty"` Proxy string `provider:"proxy,omitempty"` Interval int `provider:"interval,omitempty"` Filter string `provider:"filter,omitempty"` ExcludeFilter string `provider:"exclude-filter,omitempty"` ExcludeType string `provider:"exclude-type,omitempty"` DialerProxy string `provider:"dialer-proxy,omitempty"` SizeLimit int64 `provider:"size-limit,omitempty"` Payload []map[string]any `provider:"payload,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"` Override overrideSchema `provider:"override,omitempty"` Header map[string][]string `provider:"header,omitempty"` } func ParseProxyProvider(name string, mapping map[string]any) (P.ProxyProvider, error) { decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) schema := &proxyProviderSchema{ HealthCheck: healthCheckSchema{ Lazy: true, }, } if err := decoder.Decode(mapping, schema); err != nil { return nil, err } expectedStatus, err := utils.NewUnsignedRanges[uint16](schema.HealthCheck.ExpectedStatus) if err != nil { return nil, err } var hcInterval uint if schema.HealthCheck.Enable { if schema.HealthCheck.Interval == 0 { schema.HealthCheck.Interval = 300 } hcInterval = uint(schema.HealthCheck.Interval) } hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus) parser, err := NewProxiesParser(name, schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override) if err != nil { return nil, err } var vehicle P.Vehicle switch schema.Type { case "file": path := C.Path.Resolve(schema.Path) if !C.Path.IsSafePath(path) { return nil, C.Path.ErrNotSafePath(path) } vehicle = resource.NewFileVehicle(path) case "http": path := C.Path.GetPathByHash("proxies", schema.URL) if schema.Path != "" { path = C.Path.Resolve(schema.Path) if !C.Path.IsSafePath(path) { return nil, C.Path.ErrNotSafePath(path) } } vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit) case "inline": return NewInlineProvider(name, schema.Payload, parser, hc) default: return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) } interval := time.Duration(uint(schema.Interval)) * time.Second return NewProxySetProvider(name, interval, schema.Payload, parser, vehicle, hc) } ================================================ FILE: core/Clash.Meta/adapter/provider/patch.go ================================================ package provider func (pp *proxySetProvider) GetSubscriptionInfo() *SubscriptionInfo { return pp.subscriptionInfo } ================================================ FILE: core/Clash.Meta/adapter/provider/provider.go ================================================ package provider import ( "encoding/json" "errors" "fmt" "runtime" "strings" "sync" "time" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/common/convert" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/yaml" "github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/tunnel/statistic" "github.com/dlclark/regexp2" "github.com/metacubex/http" ) const ( ReservedName = "default" ) type ProxySchema struct { Proxies []map[string]any `yaml:"proxies"` } type providerForApi struct { Name string `json:"name"` Type string `json:"type"` VehicleType string `json:"vehicleType"` Proxies []C.Proxy `json:"proxies"` TestUrl string `json:"testUrl"` ExpectedStatus string `json:"expectedStatus"` UpdatedAt time.Time `json:"updatedAt,omitempty"` SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"` } type baseProvider struct { mutex sync.RWMutex name string proxies []C.Proxy healthCheck *HealthCheck version uint32 } func (bp *baseProvider) Name() string { return bp.name } func (bp *baseProvider) Version() uint32 { bp.mutex.RLock() defer bp.mutex.RUnlock() return bp.version } func (bp *baseProvider) Initial() error { if bp.healthCheck.auto() { go bp.healthCheck.process() } return nil } func (bp *baseProvider) HealthCheck() { bp.healthCheck.check() } func (bp *baseProvider) Type() P.ProviderType { return P.Proxy } func (bp *baseProvider) Proxies() []C.Proxy { bp.mutex.RLock() defer bp.mutex.RUnlock() return bp.proxies } func (bp *baseProvider) Count() int { bp.mutex.RLock() defer bp.mutex.RUnlock() return len(bp.proxies) } func (bp *baseProvider) Touch() { bp.healthCheck.touch() } func (bp *baseProvider) HealthCheckURL() string { return bp.healthCheck.url } func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { bp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval) } func (bp *baseProvider) setProxies(proxies []C.Proxy) { bp.mutex.Lock() defer bp.mutex.Unlock() bp.proxies = proxies bp.version += 1 bp.healthCheck.setProxies(proxies) if bp.healthCheck.auto() { go bp.healthCheck.check() } } func (bp *baseProvider) Close() error { bp.healthCheck.close() return nil } // ProxySetProvider for auto gc type ProxySetProvider struct { *proxySetProvider } type proxySetProvider struct { baseProvider *resource.Fetcher[[]C.Proxy] subscriptionInfo *SubscriptionInfo } func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { return json.Marshal(providerForApi{ Name: pp.Name(), Type: pp.Type().String(), VehicleType: pp.VehicleType().String(), Proxies: pp.Proxies(), TestUrl: pp.healthCheck.url, ExpectedStatus: pp.healthCheck.expectedStatus.String(), UpdatedAt: pp.UpdatedAt(), SubscriptionInfo: pp.subscriptionInfo, }) } func (pp *proxySetProvider) Name() string { return pp.Fetcher.Name() } func (pp *proxySetProvider) Update() error { _, _, err := pp.Fetcher.Update() return err } func (pp *proxySetProvider) Initial() error { if err := pp.baseProvider.Initial(); err != nil { return err } _, err := pp.Fetcher.Initial() if err != nil { return err } if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" { pp.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) } pp.closeAllConnections() return nil } func (pp *proxySetProvider) closeAllConnections() { statistic.DefaultManager.Range(func(c statistic.Tracker) bool { for _, chain := range c.ProviderChains() { if chain == pp.Name() { _ = c.Close() break } } return true }) } func (pp *proxySetProvider) Close() error { _ = pp.baseProvider.Close() return pp.Fetcher.Close() } func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle P.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { pd := &proxySetProvider{ baseProvider: baseProvider{ name: name, proxies: []C.Proxy{}, healthCheck: hc, }, } if len(payload) > 0 { // using as fallback proxies ps := ProxySchema{Proxies: payload} buf, err := yaml.Marshal(ps) if err != nil { return nil, err } proxies, err := parser(buf) if err != nil { return nil, err } pd.proxies = proxies // direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial() hc.setProxies(proxies) } fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies) pd.Fetcher = fetcher if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok { httpVehicle.SetInRead(func(resp *http.Response) { if subscriptionInfo := resp.Header.Get("subscription-userinfo"); subscriptionInfo != "" { cachefile.Cache().SetSubscriptionInfo(name, subscriptionInfo) pd.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) } }) } wrapper := &ProxySetProvider{pd} runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close) return wrapper, nil } func (pp *ProxySetProvider) Close() error { runtime.SetFinalizer(pp, nil) return pp.proxySetProvider.Close() } // InlineProvider for auto gc type InlineProvider struct { *inlineProvider } type inlineProvider struct { baseProvider updateAt time.Time } func (ip *inlineProvider) MarshalJSON() ([]byte, error) { return json.Marshal(providerForApi{ Name: ip.Name(), Type: ip.Type().String(), VehicleType: ip.VehicleType().String(), Proxies: ip.Proxies(), TestUrl: ip.healthCheck.url, ExpectedStatus: ip.healthCheck.expectedStatus.String(), UpdatedAt: ip.updateAt, }) } func (ip *inlineProvider) VehicleType() P.VehicleType { return P.Inline } func (ip *inlineProvider) Update() error { // make api update happy ip.updateAt = time.Now() return nil } func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) { ps := ProxySchema{Proxies: payload} buf, err := yaml.Marshal(ps) if err != nil { return nil, err } proxies, err := parser(buf) if err != nil { return nil, err } // direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial() hc.setProxies(proxies) ip := &inlineProvider{ baseProvider: baseProvider{ name: name, proxies: proxies, healthCheck: hc, }, updateAt: time.Now(), } wrapper := &InlineProvider{ip} runtime.SetFinalizer(wrapper, (*InlineProvider).Close) return wrapper, nil } func (ip *InlineProvider) Close() error { runtime.SetFinalizer(ip, nil) return ip.baseProvider.Close() } // CompatibleProvider for auto gc type CompatibleProvider struct { *compatibleProvider } type compatibleProvider struct { baseProvider } func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { return json.Marshal(providerForApi{ Name: cp.Name(), Type: cp.Type().String(), VehicleType: cp.VehicleType().String(), Proxies: cp.Proxies(), TestUrl: cp.healthCheck.url, ExpectedStatus: cp.healthCheck.expectedStatus.String(), }) } func (cp *compatibleProvider) Update() error { return nil } func (cp *compatibleProvider) VehicleType() P.VehicleType { return P.Compatible } func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) { if len(proxies) == 0 { return nil, errors.New("provider need one proxy at least") } pd := &compatibleProvider{ baseProvider: baseProvider{ name: name, proxies: proxies, healthCheck: hc, }, } wrapper := &CompatibleProvider{pd} runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close) return wrapper, nil } func (cp *CompatibleProvider) Close() error { runtime.SetFinalizer(cp, nil) return cp.compatibleProvider.Close() } func NewProxiesParser(pdName string, filter string, excludeFilter string, excludeType string, dialerProxy string, override overrideSchema) (resource.Parser[[]C.Proxy], error) { var excludeTypeArray []string if excludeType != "" { excludeTypeArray = strings.Split(excludeType, "|") } var excludeFilterRegs []*regexp2.Regexp if excludeFilter != "" { for _, excludeFilter := range strings.Split(excludeFilter, "`") { excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) if err != nil { return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) } excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg) } } var filterRegs []*regexp2.Regexp for _, filter := range strings.Split(filter, "`") { filterReg, err := regexp2.Compile(filter, regexp2.None) if err != nil { return nil, fmt.Errorf("invalid filter regex: %w", err) } filterRegs = append(filterRegs, filterReg) } return func(buf []byte) ([]C.Proxy, error) { schema := &ProxySchema{} if err := yaml.Unmarshal(buf, schema); err != nil { proxies, err1 := convert.ConvertsV2Ray(buf) if err1 != nil { return nil, fmt.Errorf("%w, %w", err, err1) } schema.Proxies = proxies } if schema.Proxies == nil { return nil, errors.New("file must have a `proxies` field") } proxies := []C.Proxy{} proxiesSet := map[string]struct{}{} for _, filterReg := range filterRegs { LOOP1: for idx, mapping := range schema.Proxies { if len(excludeTypeArray) > 0 { mType, ok := mapping["type"] if !ok { continue } pType, ok := mType.(string) if !ok { continue } for _, excludeType := range excludeTypeArray { if strings.EqualFold(pType, excludeType) { continue LOOP1 } } } mName, ok := mapping["name"] if !ok { continue } name, ok := mName.(string) if !ok { continue } if len(excludeFilterRegs) > 0 { for _, excludeFilterReg := range excludeFilterRegs { if mat, _ := excludeFilterReg.MatchString(name); mat { continue LOOP1 } } } if len(filter) > 0 { if mat, _ := filterReg.MatchString(name); !mat { continue } } if _, ok := proxiesSet[name]; ok { continue } if len(dialerProxy) > 0 { mapping["dialer-proxy"] = dialerProxy } err := override.Apply(mapping) if err != nil { return nil, fmt.Errorf("proxy %d override error: %w", idx, err) } proxy, err := adapter.ParseProxy(mapping, adapter.WithProviderName(pdName)) if err != nil { return nil, fmt.Errorf("proxy %d error: %w", idx, err) } proxiesSet[name] = struct{}{} proxies = append(proxies, proxy) } } if len(proxies) == 0 { if len(filter) > 0 { return nil, errors.New("doesn't match any proxy, please check your filter") } return nil, errors.New("file doesn't have any proxy") } return proxies, nil }, nil } ================================================ FILE: core/Clash.Meta/adapter/provider/subscription_info.go ================================================ package provider import ( "fmt" "strconv" "strings" "github.com/metacubex/mihomo/log" ) type SubscriptionInfo struct { Upload int64 Download int64 Total int64 Expire int64 } func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo) { userinfo = strings.ReplaceAll(strings.ToLower(userinfo), " ", "") si = new(SubscriptionInfo) for _, field := range strings.Split(userinfo, ";") { name, value, ok := strings.Cut(field, "=") if !ok { continue } intValue, err := parseValue(value) if err != nil { log.Warnln("[Provider] get subscription-userinfo: %e", err) continue } switch name { case "upload": si.Upload = intValue case "download": si.Download = intValue case "total": si.Total = intValue case "expire": si.Expire = intValue } } return si } func parseValue(value string) (int64, error) { if intValue, err := strconv.ParseInt(value, 10, 64); err == nil { return intValue, nil } if floatValue, err := strconv.ParseFloat(value, 64); err == nil { return int64(floatValue), nil } return 0, fmt.Errorf("failed to parse value '%s'", value) } ================================================ FILE: core/Clash.Meta/android_tz.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 android && cgo // +build android,cgo // kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89 package main // #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: core/Clash.Meta/check_amd64.sh ================================================ #!/bin/sh flags=$(grep '^flags\b' = a.b2.Len() { d = 1 } else { d = a.b2.Len() / a.b1.Len() } a.p = min(a.p+d, a.c) a.replace(ent) ent.setMRU(a.t2) case ent.ll == a.b2: // Case III // Cache Miss in t1 and t2 // Adaptation var d int if a.b2.Len() >= a.b1.Len() { d = 1 } else { d = a.b1.Len() / a.b2.Len() } a.p = max(a.p-d, 0) a.replace(ent) ent.setMRU(a.t2) case ent.ll == nil && a.t1.Len()+a.b1.Len() == a.c: // Case IV A if a.t1.Len() < a.c { a.delLRU(a.b1) a.replace(ent) } else { a.delLRU(a.t1) } ent.setMRU(a.t1) case ent.ll == nil && a.t1.Len()+a.b1.Len() < a.c: // Case IV B if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() >= a.c { if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() == 2*a.c { a.delLRU(a.b2) } a.replace(ent) } ent.setMRU(a.t1) case ent.ll == nil: // Case IV, not A nor B ent.setMRU(a.t1) } } func (a *ARC[K, V]) delLRU(list *list.List[*entry[K, V]]) { lru := list.Back() list.Remove(lru) a.len-- delete(a.cache, lru.Value.key) } func (a *ARC[K, V]) replace(ent *entry[K, V]) { if a.t1.Len() > 0 && ((a.t1.Len() > a.p) || (ent.ll == a.b2 && a.t1.Len() == a.p)) { lru := a.t1.Back().Value lru.value = lo.Empty[V]() lru.ghost = true a.len-- lru.setMRU(a.b1) } else { lru := a.t2.Back().Value lru.value = lo.Empty[V]() lru.ghost = true a.len-- lru.setMRU(a.b2) } } func min(a, b int) int { if a < b { return a } return b } func max(a int, b int) int { if a < b { return b } return a } ================================================ FILE: core/Clash.Meta/common/arc/arc_test.go ================================================ package arc import ( "testing" ) func TestInsertion(t *testing.T) { cache := New[string, string](WithSize[string, string](3)) if got, want := cache.Len(), 0; got != want { t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) } const ( k1 = "Hello" k2 = "Hallo" k3 = "Ciao" k4 = "Salut" v1 = "World" v2 = "Worlds" v3 = "Welt" ) // Insert the first value cache.Set(k1, v1) if got, want := cache.Len(), 1; got != want { t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) } if got, ok := cache.Get(k1); !ok || got != v1 { t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v1) } // Replace existing value for a given key cache.Set(k1, v2) if got, want := cache.Len(), 1; got != want { t.Errorf("re-insertion: cache.Len(): got %d want %d", cache.Len(), want) } if got, ok := cache.Get(k1); !ok || got != v2 { t.Errorf("re-insertion: cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) } // Add a second different key cache.Set(k2, v3) if got, want := cache.Len(), 2; got != want { t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) } if got, ok := cache.Get(k1); !ok || got != v2 { t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) } if got, ok := cache.Get(k2); !ok || got != v3 { t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k2, got, ok, v3) } // Fill cache cache.Set(k3, v1) if got, want := cache.Len(), 3; got != want { t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) } // Exceed size, this should not exceed size: cache.Set(k4, v1) if got, want := cache.Len(), 3; got != want { t.Errorf("insertion of key out of size: cache.Len(): got %d want %d", cache.Len(), want) } } func TestEviction(t *testing.T) { size := 3 cache := New[string, string](WithSize[string, string](size)) if got, want := cache.Len(), 0; got != want { t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) } tests := []struct { k, v string }{ {"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}, {"k4", "v4"}, } for i, tt := range tests[:size] { cache.Set(tt.k, tt.v) if got, want := cache.Len(), i+1; got != want { t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) } } // Exceed size and check we don't outgrow it: cache.Set(tests[size].k, tests[size].v) if got := cache.Len(); got != size { t.Errorf("insertion of overflow key #%d: cache.Len(): got %d want %d", 4, cache.Len(), size) } // Check that LRU got evicted: if got, ok := cache.Get(tests[0].k); ok || got != "" { t.Errorf("cache.Get(%v): got (%v,%t) want (,true)", tests[0].k, got, ok) } for _, tt := range tests[1:] { if got, ok := cache.Get(tt.k); !ok || got != tt.v { t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", tt.k, got, ok, tt.v) } } } ================================================ FILE: core/Clash.Meta/common/arc/entry.go ================================================ package arc import ( list "github.com/bahlo/generic-list-go" ) type entry[K comparable, V any] struct { key K value V ll *list.List[*entry[K, V]] el *list.Element[*entry[K, V]] ghost bool expires int64 } func (e *entry[K, V]) setLRU(list *list.List[*entry[K, V]]) { e.detach() e.ll = list e.el = e.ll.PushBack(e) } func (e *entry[K, V]) setMRU(list *list.List[*entry[K, V]]) { e.detach() e.ll = list e.el = e.ll.PushFront(e) } func (e *entry[K, V]) detach() { if e.ll != nil { e.ll.Remove(e.el) } } ================================================ FILE: core/Clash.Meta/common/atomic/enum.go ================================================ package atomic import ( "encoding/json" "fmt" "sync/atomic" ) type Int32Enum[T ~int32] struct { value atomic.Int32 } func (i *Int32Enum[T]) MarshalJSON() ([]byte, error) { return json.Marshal(i.Load()) } func (i *Int32Enum[T]) UnmarshalJSON(b []byte) error { var v T if err := json.Unmarshal(b, &v); err != nil { return err } i.Store(v) return nil } func (i *Int32Enum[T]) MarshalYAML() (any, error) { return i.Load(), nil } func (i *Int32Enum[T]) UnmarshalYAML(unmarshal func(any) error) error { var v T if err := unmarshal(&v); err != nil { return err } i.Store(v) return nil } func (i *Int32Enum[T]) String() string { return fmt.Sprint(i.Load()) } func (i *Int32Enum[T]) Store(v T) { i.value.Store(int32(v)) } func (i *Int32Enum[T]) Load() T { return T(i.value.Load()) } func (i *Int32Enum[T]) Swap(new T) T { return T(i.value.Swap(int32(new))) } func (i *Int32Enum[T]) CompareAndSwap(old, new T) bool { return i.value.CompareAndSwap(int32(old), int32(new)) } func NewInt32Enum[T ~int32](v T) *Int32Enum[T] { a := &Int32Enum[T]{} a.Store(v) return a } ================================================ FILE: core/Clash.Meta/common/atomic/type.go ================================================ package atomic import ( "encoding/json" "fmt" "strconv" "sync/atomic" ) type Bool struct { atomic.Bool } func NewBool(val bool) (i Bool) { i.Store(val) return } func (i *Bool) MarshalJSON() ([]byte, error) { return json.Marshal(i.Load()) } func (i *Bool) UnmarshalJSON(b []byte) error { var v bool if err := json.Unmarshal(b, &v); err != nil { return err } i.Store(v) return nil } func (i *Bool) MarshalYAML() (any, error) { return i.Load(), nil } func (i *Bool) UnmarshalYAML(unmarshal func(any) error) error { var v bool if err := unmarshal(&v); err != nil { return err } i.Store(v) return nil } func (i *Bool) String() string { v := i.Load() return strconv.FormatBool(v) } type Pointer[T any] struct { atomic.Pointer[T] } func NewPointer[T any](v *T) (p Pointer[T]) { if v != nil { p.Store(v) } return } func (p *Pointer[T]) MarshalJSON() ([]byte, error) { return json.Marshal(p.Load()) } func (p *Pointer[T]) UnmarshalJSON(b []byte) error { var v *T if err := json.Unmarshal(b, &v); err != nil { return err } p.Store(v) return nil } func (p *Pointer[T]) MarshalYAML() (any, error) { return p.Load(), nil } func (p *Pointer[T]) UnmarshalYAML(unmarshal func(any) error) error { var v *T if err := unmarshal(&v); err != nil { return err } p.Store(v) return nil } func (p *Pointer[T]) String() string { return fmt.Sprint(p.Load()) } type Int32 struct { atomic.Int32 } func NewInt32(val int32) (i Int32) { i.Store(val) return } func (i *Int32) MarshalJSON() ([]byte, error) { return json.Marshal(i.Load()) } func (i *Int32) UnmarshalJSON(b []byte) error { var v int32 if err := json.Unmarshal(b, &v); err != nil { return err } i.Store(v) return nil } func (i *Int32) MarshalYAML() (any, error) { return i.Load(), nil } func (i *Int32) UnmarshalYAML(unmarshal func(any) error) error { var v int32 if err := unmarshal(&v); err != nil { return err } i.Store(v) return nil } func (i *Int32) String() string { v := i.Load() return strconv.FormatInt(int64(v), 10) } type Int64 struct { atomic.Int64 } func NewInt64(val int64) (i Int64) { i.Store(val) return } func (i *Int64) MarshalJSON() ([]byte, error) { return json.Marshal(i.Load()) } func (i *Int64) UnmarshalJSON(b []byte) error { var v int64 if err := json.Unmarshal(b, &v); err != nil { return err } i.Store(v) return nil } func (i *Int64) MarshalYAML() (any, error) { return i.Load(), nil } func (i *Int64) UnmarshalYAML(unmarshal func(any) error) error { var v int64 if err := unmarshal(&v); err != nil { return err } i.Store(v) return nil } func (i *Int64) String() string { v := i.Load() return strconv.FormatInt(int64(v), 10) } type Uint32 struct { atomic.Uint32 } func NewUint32(val uint32) (i Uint32) { i.Store(val) return } func (i *Uint32) MarshalJSON() ([]byte, error) { return json.Marshal(i.Load()) } func (i *Uint32) UnmarshalJSON(b []byte) error { var v uint32 if err := json.Unmarshal(b, &v); err != nil { return err } i.Store(v) return nil } func (i *Uint32) MarshalYAML() (any, error) { return i.Load(), nil } func (i *Uint32) UnmarshalYAML(unmarshal func(any) error) error { var v uint32 if err := unmarshal(&v); err != nil { return err } i.Store(v) return nil } func (i *Uint32) String() string { v := i.Load() return strconv.FormatUint(uint64(v), 10) } type Uint64 struct { atomic.Uint64 } func NewUint64(val uint64) (i Uint64) { i.Store(val) return } func (i *Uint64) MarshalJSON() ([]byte, error) { return json.Marshal(i.Load()) } func (i *Uint64) UnmarshalJSON(b []byte) error { var v uint64 if err := json.Unmarshal(b, &v); err != nil { return err } i.Store(v) return nil } func (i *Uint64) MarshalYAML() (any, error) { return i.Load(), nil } func (i *Uint64) UnmarshalYAML(unmarshal func(any) error) error { var v uint64 if err := unmarshal(&v); err != nil { return err } i.Store(v) return nil } func (i *Uint64) String() string { v := i.Load() return strconv.FormatUint(uint64(v), 10) } type Uintptr struct { atomic.Uintptr } func NewUintptr(val uintptr) (i Uintptr) { i.Store(val) return } func (i *Uintptr) MarshalJSON() ([]byte, error) { return json.Marshal(i.Load()) } func (i *Uintptr) UnmarshalJSON(b []byte) error { var v uintptr if err := json.Unmarshal(b, &v); err != nil { return err } i.Store(v) return nil } func (i *Uintptr) MarshalYAML() (any, error) { return i.Load(), nil } func (i *Uintptr) UnmarshalYAML(unmarshal func(any) error) error { var v uintptr if err := unmarshal(&v); err != nil { return err } i.Store(v) return nil } func (i *Uintptr) String() string { v := i.Load() return strconv.FormatUint(uint64(v), 10) } ================================================ FILE: core/Clash.Meta/common/atomic/value.go ================================================ package atomic import ( "encoding/json" "sync/atomic" ) type TypedValue[T any] struct { value atomic.Pointer[T] } func (t *TypedValue[T]) Load() (v T) { v, _ = t.LoadOk() return } func (t *TypedValue[T]) LoadOk() (v T, ok bool) { value := t.value.Load() if value == nil { return } return *value, true } func (t *TypedValue[T]) Store(value T) { t.value.Store(&value) } func (t *TypedValue[T]) Swap(new T) (v T) { old := t.value.Swap(&new) if old == nil { return } return *old } func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { for { currentP := t.value.Load() var currentValue T if currentP != nil { currentValue = *currentP } // Compare old and current via runtime equality check. if any(currentValue) != any(old) { return false } if t.value.CompareAndSwap(currentP, &new) { return true } } } func (t *TypedValue[T]) MarshalJSON() ([]byte, error) { return json.Marshal(t.Load()) } func (t *TypedValue[T]) UnmarshalJSON(b []byte) error { var v T if err := json.Unmarshal(b, &v); err != nil { return err } t.Store(v) return nil } func (t *TypedValue[T]) MarshalYAML() (any, error) { return t.Load(), nil } func (t *TypedValue[T]) UnmarshalYAML(unmarshal func(any) error) error { var v T if err := unmarshal(&v); err != nil { return err } t.Store(v) return nil } func NewTypedValue[T any](t T) (v TypedValue[T]) { v.Store(t) return } ================================================ FILE: core/Clash.Meta/common/atomic/value_test.go ================================================ package atomic import ( "io" "os" "testing" ) func TestTypedValue(t *testing.T) { { var v TypedValue[int] got, gotOk := v.LoadOk() if got != 0 || gotOk { t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk) } v.Store(1) got, gotOk = v.LoadOk() if got != 1 || !gotOk { t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk) } } { var v TypedValue[error] got, gotOk := v.LoadOk() if got != nil || gotOk { t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk) } v.Store(io.EOF) got, gotOk = v.LoadOk() if got != io.EOF || !gotOk { t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk) } err := &os.PathError{} v.Store(err) got, gotOk = v.LoadOk() if got != err || !gotOk { t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err) } v.Store(nil) got, gotOk = v.LoadOk() if got != nil || !gotOk { t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk) } } { e1, e2, e3 := io.EOF, &os.PathError{}, &os.PathError{} var v TypedValue[error] if v.CompareAndSwap(e1, e2) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != nil { t.Fatalf("Load = (%v), want (%v)", value, nil) } if v.CompareAndSwap(nil, e1) != true { t.Fatalf("CompareAndSwap = false, want true") } if value := v.Load(); value != e1 { t.Fatalf("Load = (%v), want (%v)", value, e1) } if v.CompareAndSwap(e2, e3) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != e1 { t.Fatalf("Load = (%v), want (%v)", value, e1) } if v.CompareAndSwap(e1, e2) != true { t.Fatalf("CompareAndSwap = false, want true") } if value := v.Load(); value != e2 { t.Fatalf("Load = (%v), want (%v)", value, e2) } if v.CompareAndSwap(e3, e2) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != e2 { t.Fatalf("Load = (%v), want (%v)", value, e2) } if v.CompareAndSwap(nil, e3) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != e2 { t.Fatalf("Load = (%v), want (%v)", value, e2) } } { c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{}) var v TypedValue[chan struct{}] if v.CompareAndSwap(c1, c2) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != nil { t.Fatalf("Load = (%v), want (%v)", value, nil) } if v.CompareAndSwap(nil, c1) != true { t.Fatalf("CompareAndSwap = false, want true") } if value := v.Load(); value != c1 { t.Fatalf("Load = (%v), want (%v)", value, c1) } if v.CompareAndSwap(c2, c3) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != c1 { t.Fatalf("Load = (%v), want (%v)", value, c1) } if v.CompareAndSwap(c1, c2) != true { t.Fatalf("CompareAndSwap = false, want true") } if value := v.Load(); value != c2 { t.Fatalf("Load = (%v), want (%v)", value, c2) } if v.CompareAndSwap(c3, c2) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != c2 { t.Fatalf("Load = (%v), want (%v)", value, c2) } if v.CompareAndSwap(nil, c3) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != c2 { t.Fatalf("Load = (%v), want (%v)", value, c2) } } { c1, c2, c3 := &io.LimitedReader{}, &io.SectionReader{}, &io.SectionReader{} var v TypedValue[io.Reader] if v.CompareAndSwap(c1, c2) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != nil { t.Fatalf("Load = (%v), want (%v)", value, nil) } if v.CompareAndSwap(nil, c1) != true { t.Fatalf("CompareAndSwap = false, want true") } if value := v.Load(); value != c1 { t.Fatalf("Load = (%v), want (%v)", value, c1) } if v.CompareAndSwap(c2, c3) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != c1 { t.Fatalf("Load = (%v), want (%v)", value, c1) } if v.CompareAndSwap(c1, c2) != true { t.Fatalf("CompareAndSwap = false, want true") } if value := v.Load(); value != c2 { t.Fatalf("Load = (%v), want (%v)", value, c2) } if v.CompareAndSwap(c3, c2) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != c2 { t.Fatalf("Load = (%v), want (%v)", value, c2) } if v.CompareAndSwap(nil, c3) != false { t.Fatalf("CompareAndSwap = true, want false") } if value := v.Load(); value != c2 { t.Fatalf("Load = (%v), want (%v)", value, c2) } } } ================================================ FILE: core/Clash.Meta/common/batch/batch.go ================================================ package batch import ( "context" "sync" ) type Option[T any] func(b *Batch[T]) type Result[T any] struct { Value T Err error } type Error struct { Key string Err error } func WithConcurrencyNum[T any](n int) Option[T] { return func(b *Batch[T]) { q := make(chan struct{}, n) for i := 0; i < n; i++ { q <- struct{}{} } b.queue = q } } // Batch similar to errgroup, but can control the maximum number of concurrent type Batch[T any] struct { result map[string]Result[T] queue chan struct{} wg sync.WaitGroup mux sync.Mutex err *Error once sync.Once cancel func() } func (b *Batch[T]) Go(key string, fn func() (T, error)) { b.wg.Add(1) go func() { defer b.wg.Done() if b.queue != nil { <-b.queue defer func() { b.queue <- struct{}{} }() } value, err := fn() if err != nil { b.once.Do(func() { b.err = &Error{key, err} if b.cancel != nil { b.cancel() } }) } ret := Result[T]{value, err} b.mux.Lock() defer b.mux.Unlock() b.result[key] = ret }() } func (b *Batch[T]) Wait() *Error { b.wg.Wait() if b.cancel != nil { b.cancel() } return b.err } func (b *Batch[T]) WaitAndGetResult() (map[string]Result[T], *Error) { err := b.Wait() return b.Result(), err } func (b *Batch[T]) Result() map[string]Result[T] { b.mux.Lock() defer b.mux.Unlock() copyM := map[string]Result[T]{} for k, v := range b.result { copyM[k] = v } return copyM } func New[T any](ctx context.Context, opts ...Option[T]) (*Batch[T], context.Context) { ctx, cancel := context.WithCancel(ctx) b := &Batch[T]{ result: map[string]Result[T]{}, } for _, o := range opts { o(b) } b.cancel = cancel return b, ctx } ================================================ FILE: core/Clash.Meta/common/batch/batch_test.go ================================================ package batch import ( "context" "errors" "strconv" "testing" "time" "github.com/stretchr/testify/assert" ) func TestBatch(t *testing.T) { b, _ := New[string](context.Background()) now := time.Now() b.Go("foo", func() (string, error) { time.Sleep(time.Millisecond * 100) return "foo", nil }) b.Go("bar", func() (string, error) { time.Sleep(time.Millisecond * 150) return "bar", nil }) result, err := b.WaitAndGetResult() assert.Nil(t, err) duration := time.Since(now) assert.Less(t, duration, time.Millisecond*200) assert.Equal(t, 2, len(result)) for k, v := range result { assert.NoError(t, v.Err) assert.Equal(t, k, v.Value) } } func TestBatchWithConcurrencyNum(t *testing.T) { b, _ := New[string]( context.Background(), WithConcurrencyNum[string](3), ) now := time.Now() for i := 0; i < 7; i++ { idx := i b.Go(strconv.Itoa(idx), func() (string, error) { time.Sleep(time.Millisecond * 100) return strconv.Itoa(idx), nil }) } result, _ := b.WaitAndGetResult() duration := time.Since(now) assert.Greater(t, duration, time.Millisecond*260) assert.Equal(t, 7, len(result)) for k, v := range result { assert.NoError(t, v.Err) assert.Equal(t, k, v.Value) } } func TestBatchContext(t *testing.T) { b, ctx := New[string](context.Background()) b.Go("error", func() (string, error) { time.Sleep(time.Millisecond * 100) return "", errors.New("test error") }) b.Go("ctx", func() (string, error) { <-ctx.Done() return "", ctx.Err() }) result, err := b.WaitAndGetResult() assert.NotNil(t, err) assert.Equal(t, "error", err.Key) assert.Equal(t, ctx.Err(), result["ctx"].Err) } ================================================ FILE: core/Clash.Meta/common/buf/sing.go ================================================ package buf import ( "github.com/metacubex/sing/common/buf" ) const BufferSize = buf.BufferSize type Buffer = buf.Buffer func New() *Buffer { return buf.New() } func NewPacket() *Buffer { return buf.NewPacket() } func NewSize(size int) *Buffer { return buf.NewSize(size) } func With(data []byte) *Buffer { return buf.With(data) } func As(data []byte) *Buffer { return buf.As(data) } func ReleaseMulti(buffers []*Buffer) { buf.ReleaseMulti(buffers) } func Error(_ any, err error) error { return err } func Must(errs ...error) { for _, err := range errs { if err != nil { panic(err) } } } func Must1[T any](result T, err error) T { if err != nil { panic(err) } return result } func Must2[T any, T2 any](result T, result2 T2, err error) (T, T2) { if err != nil { panic(err) } return result, result2 } ================================================ FILE: core/Clash.Meta/common/callback/callback.go ================================================ package callback import ( "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" ) type firstWriteCallBackConn struct { C.Conn callback func(error) written bool } func (c *firstWriteCallBackConn) Write(b []byte) (n int, err error) { defer func() { if !c.written { c.written = true c.callback(err) } }() return c.Conn.Write(b) } func (c *firstWriteCallBackConn) WriteBuffer(buffer *buf.Buffer) (err error) { defer func() { if !c.written { c.written = true c.callback(err) } }() return c.Conn.WriteBuffer(buffer) } func (c *firstWriteCallBackConn) Upstream() any { return c.Conn } func (c *firstWriteCallBackConn) WriterReplaceable() bool { return c.written } func (c *firstWriteCallBackConn) ReaderReplaceable() bool { return true } var _ N.ExtendedConn = (*firstWriteCallBackConn)(nil) func NewFirstWriteCallBackConn(c C.Conn, callback func(error)) C.Conn { return &firstWriteCallBackConn{ Conn: c, callback: callback, written: false, } } ================================================ FILE: core/Clash.Meta/common/callback/close_callback.go ================================================ package callback import ( "sync" C "github.com/metacubex/mihomo/constant" ) type closeCallbackConn struct { C.Conn closeFunc func() closeOnce sync.Once } func (w *closeCallbackConn) Close() error { w.closeOnce.Do(w.closeFunc) return w.Conn.Close() } func (w *closeCallbackConn) ReaderReplaceable() bool { return true } func (w *closeCallbackConn) WriterReplaceable() bool { return true } func (w *closeCallbackConn) Upstream() any { return w.Conn } func NewCloseCallbackConn(conn C.Conn, callback func()) C.Conn { return &closeCallbackConn{Conn: conn, closeFunc: callback} } type closeCallbackPacketConn struct { C.PacketConn closeFunc func() closeOnce sync.Once } func (w *closeCallbackPacketConn) Close() error { w.closeOnce.Do(w.closeFunc) return w.PacketConn.Close() } func (w *closeCallbackPacketConn) ReaderReplaceable() bool { return true } func (w *closeCallbackPacketConn) WriterReplaceable() bool { return true } func (w *closeCallbackPacketConn) Upstream() any { return w.PacketConn } func NewCloseCallbackPacketConn(conn C.PacketConn, callback func()) C.PacketConn { return &closeCallbackPacketConn{PacketConn: conn, closeFunc: callback} } ================================================ FILE: core/Clash.Meta/common/cmd/cmd.go ================================================ package cmd import ( "fmt" "os/exec" "runtime" "strings" ) func ExecCmd(cmdStr string) (string, error) { args := splitArgs(cmdStr) var cmd *exec.Cmd if len(args) == 1 { cmd = exec.Command(args[0]) } else { cmd = exec.Command(args[0], args[1:]...) } prepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("%v, %s", err, string(out)) } return string(out), nil } func splitArgs(cmd string) []string { args := strings.Split(cmd, " ") // use in pipeline if len(args) > 2 && strings.ContainsAny(cmd, "|") { suffix := strings.Join(args[2:], " ") args = append(args[:2], suffix) } return args } func ExecShell(shellStr string) (string, error) { var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("cmd.exe", "/C", shellStr) } else { cmd = exec.Command("sh", "-c", shellStr) } prepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("%v, %s", err, string(out)) } return string(out), nil } ================================================ FILE: core/Clash.Meta/common/cmd/cmd_other.go ================================================ //go:build !windows package cmd import ( "os/exec" ) func prepareBackgroundCommand(cmd *exec.Cmd) { } ================================================ FILE: core/Clash.Meta/common/cmd/cmd_test.go ================================================ package cmd import ( "runtime" "testing" "github.com/stretchr/testify/assert" ) func TestSplitArgs(t *testing.T) { args := splitArgs("ls") args1 := splitArgs("ls -la") args2 := splitArgs("bash -c ls") args3 := splitArgs("bash -c ls -lahF | grep 'cmd'") assert.Equal(t, 1, len(args)) assert.Equal(t, 2, len(args1)) assert.Equal(t, 3, len(args2)) assert.Equal(t, 3, len(args3)) } func TestExecCmd(t *testing.T) { if runtime.GOOS == "windows" { _, err := ExecCmd("cmd -c 'dir'") assert.Nil(t, err) return } _, err := ExecCmd("ls") _, err1 := ExecCmd("ls -la") _, err2 := ExecCmd("bash -c ls") _, err3 := ExecCmd("bash -c ls -la") _, err4 := ExecCmd("bash -c ls -la | grep 'cmd'") assert.Nil(t, err) assert.Nil(t, err1) assert.Nil(t, err2) assert.Nil(t, err3) assert.Nil(t, err4) } ================================================ FILE: core/Clash.Meta/common/cmd/cmd_windows.go ================================================ //go:build windows package cmd import ( "os/exec" "syscall" ) func prepareBackgroundCommand(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} } ================================================ FILE: core/Clash.Meta/common/contextutils/afterfunc_compact.go ================================================ package contextutils import ( "context" "sync" ) func afterFunc(ctx context.Context, f func()) (stop func() bool) { stopc := make(chan struct{}) once := sync.Once{} // either starts running f or stops f from running if ctx.Done() != nil { go func() { select { case <-ctx.Done(): once.Do(func() { go f() }) case <-stopc: } }() } return func() bool { stopped := false once.Do(func() { stopped = true close(stopc) }) return stopped } } ================================================ FILE: core/Clash.Meta/common/contextutils/afterfunc_go120.go ================================================ //go:build !go1.21 package contextutils import ( "context" ) func AfterFunc(ctx context.Context, f func()) (stop func() bool) { return afterFunc(ctx, f) } ================================================ FILE: core/Clash.Meta/common/contextutils/afterfunc_go121.go ================================================ //go:build go1.21 package contextutils import "context" func AfterFunc(ctx context.Context, f func()) (stop func() bool) { return context.AfterFunc(ctx, f) } ================================================ FILE: core/Clash.Meta/common/contextutils/afterfunc_test.go ================================================ package contextutils import ( "context" "testing" "time" ) const ( shortDuration = 1 * time.Millisecond // a reasonable duration to block in a test veryLongDuration = 1000 * time.Hour // an arbitrary upper bound on the test's running time ) func TestAfterFuncCalledAfterCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) donec := make(chan struct{}) stop := afterFunc(ctx, func() { close(donec) }) select { case <-donec: t.Fatalf("AfterFunc called before context is done") case <-time.After(shortDuration): } cancel() select { case <-donec: case <-time.After(veryLongDuration): t.Fatalf("AfterFunc not called after context is canceled") } if stop() { t.Fatalf("stop() = true, want false") } } func TestAfterFuncCalledAfterTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), shortDuration) defer cancel() donec := make(chan struct{}) afterFunc(ctx, func() { close(donec) }) select { case <-donec: case <-time.After(veryLongDuration): t.Fatalf("AfterFunc not called after context is canceled") } } func TestAfterFuncCalledImmediately(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() donec := make(chan struct{}) afterFunc(ctx, func() { close(donec) }) select { case <-donec: case <-time.After(veryLongDuration): t.Fatalf("AfterFunc not called for already-canceled context") } } func TestAfterFuncNotCalledAfterStop(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) donec := make(chan struct{}) stop := afterFunc(ctx, func() { close(donec) }) if !stop() { t.Fatalf("stop() = false, want true") } cancel() select { case <-donec: t.Fatalf("AfterFunc called for already-canceled context") case <-time.After(shortDuration): } if stop() { t.Fatalf("stop() = true, want false") } } // This test verifies that canceling a context does not block waiting for AfterFuncs to finish. func TestAfterFuncCalledAsynchronously(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) donec := make(chan struct{}) stop := afterFunc(ctx, func() { // The channel send blocks until donec is read from. donec <- struct{}{} }) defer stop() cancel() // After cancel returns, read from donec and unblock the AfterFunc. select { case <-donec: case <-time.After(veryLongDuration): t.Fatalf("AfterFunc not called after context is canceled") } } ================================================ FILE: core/Clash.Meta/common/contextutils/withoutcancel_compact.go ================================================ package contextutils import ( "context" "time" ) type withoutCancelCtx struct { c context.Context } func (withoutCancelCtx) Deadline() (deadline time.Time, ok bool) { return } func (withoutCancelCtx) Done() <-chan struct{} { return nil } func (withoutCancelCtx) Err() error { return nil } func (c withoutCancelCtx) Value(key any) any { return c.c.Value(key) } ================================================ FILE: core/Clash.Meta/common/contextutils/withoutcancel_go120.go ================================================ //go:build !go1.21 package contextutils import "context" func WithoutCancel(parent context.Context) context.Context { if parent == nil { panic("cannot create context from nil parent") } return withoutCancelCtx{parent} } ================================================ FILE: core/Clash.Meta/common/contextutils/withoutcancel_go121.go ================================================ //go:build go1.21 package contextutils import "context" func WithoutCancel(parent context.Context) context.Context { return context.WithoutCancel(parent) } ================================================ FILE: core/Clash.Meta/common/convert/base64.go ================================================ package convert import ( "encoding/base64" "fmt" "strings" ) var ( encRaw = base64.RawStdEncoding enc = base64.StdEncoding ) // DecodeBase64 try to decode content from the given bytes, // which can be in base64.RawStdEncoding, base64.StdEncoding or just plaintext. func DecodeBase64(buf []byte) []byte { result, err := tryDecodeBase64(buf) if err != nil { return buf } return result } func tryDecodeBase64(buf []byte) ([]byte, error) { dBuf := make([]byte, encRaw.DecodedLen(len(buf))) n, err := encRaw.Decode(dBuf, buf) if err != nil { n, err = enc.Decode(dBuf, buf) if err != nil { return nil, err } } return dBuf[:n], nil } func urlSafe(data string) string { return strings.NewReplacer("+", "-", "/", "_").Replace(data) } func decodeUrlSafe(data string) string { dcBuf, err := base64.RawURLEncoding.DecodeString(data) if err != nil { return "" } return string(dcBuf) } func TryDecodeBase64(s string) (decoded []byte, err error) { if len(s)%4 == 0 { if decoded, err = base64.StdEncoding.DecodeString(s); err == nil { return } if decoded, err = base64.URLEncoding.DecodeString(s); err == nil { return } } else { if decoded, err = base64.RawStdEncoding.DecodeString(s); err == nil { return } if decoded, err = base64.RawURLEncoding.DecodeString(s); err == nil { return } } return nil, fmt.Errorf("invalid base64-encoded string") } ================================================ FILE: core/Clash.Meta/common/convert/converter.go ================================================ package convert import ( "bytes" "encoding/base64" "encoding/json" "fmt" "net/url" "strconv" "strings" "github.com/metacubex/mihomo/log" ) // ConvertsV2Ray convert V2Ray subscribe proxies data to mihomo proxies config func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { data := DecodeBase64(buf) arr := strings.Split(string(data), "\n") proxies := make([]map[string]any, 0, len(arr)) names := make(map[string]int, 200) for _, line := range arr { line = strings.TrimRight(line, " \r") if line == "" { continue } scheme, body, found := strings.Cut(line, "://") if !found { continue } scheme = strings.ToLower(scheme) switch scheme { case "hysteria": urlHysteria, err := url.Parse(line) if err != nil { continue } query := urlHysteria.Query() name := uniqueName(names, urlHysteria.Fragment) hysteria := make(map[string]any, 20) hysteria["name"] = name hysteria["type"] = scheme hysteria["server"] = urlHysteria.Hostname() hysteria["port"] = urlHysteria.Port() hysteria["sni"] = query.Get("peer") hysteria["obfs"] = query.Get("obfs") if alpn := query.Get("alpn"); alpn != "" { hysteria["alpn"] = strings.Split(alpn, ",") } hysteria["auth_str"] = query.Get("auth") hysteria["protocol"] = query.Get("protocol") up := query.Get("up") down := query.Get("down") if up == "" { up = query.Get("upmbps") } if down == "" { down = query.Get("downmbps") } hysteria["down"] = down hysteria["up"] = up hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure")) proxies = append(proxies, hysteria) case "hysteria2", "hy2": urlHysteria2, err := url.Parse(line) if err != nil { continue } query := urlHysteria2.Query() name := uniqueName(names, urlHysteria2.Fragment) hysteria2 := make(map[string]any, 20) hysteria2["name"] = name hysteria2["type"] = "hysteria2" hysteria2["server"] = urlHysteria2.Hostname() if port := urlHysteria2.Port(); port != "" { hysteria2["port"] = port } else { hysteria2["port"] = "443" } hysteria2["obfs"] = query.Get("obfs") hysteria2["obfs-password"] = query.Get("obfs-password") hysteria2["sni"] = query.Get("sni") hysteria2["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure")) if alpn := query.Get("alpn"); alpn != "" { hysteria2["alpn"] = strings.Split(alpn, ",") } if auth := urlHysteria2.User.String(); auth != "" { hysteria2["password"] = auth } hysteria2["fingerprint"] = query.Get("pinSHA256") hysteria2["down"] = query.Get("down") hysteria2["up"] = query.Get("up") proxies = append(proxies, hysteria2) case "tuic": // A temporary unofficial TUIC share link standard // Modified from https://github.com/daeuniverse/dae/discussions/182 // Changes: // 1. Support TUICv4, just replace uuid:password with token // 2. Remove `allow_insecure` field urlTUIC, err := url.Parse(line) if err != nil { continue } query := urlTUIC.Query() tuic := make(map[string]any, 20) tuic["name"] = uniqueName(names, urlTUIC.Fragment) tuic["type"] = scheme tuic["server"] = urlTUIC.Hostname() tuic["port"] = urlTUIC.Port() tuic["udp"] = true password, v5 := urlTUIC.User.Password() if v5 { tuic["uuid"] = urlTUIC.User.Username() tuic["password"] = password } else { tuic["token"] = urlTUIC.User.Username() } if cc := query.Get("congestion_control"); cc != "" { tuic["congestion-controller"] = cc } if alpn := query.Get("alpn"); alpn != "" { tuic["alpn"] = strings.Split(alpn, ",") } if sni := query.Get("sni"); sni != "" { tuic["sni"] = sni } if query.Get("disable_sni") == "1" { tuic["disable-sni"] = true } if udpRelayMode := query.Get("udp_relay_mode"); udpRelayMode != "" { tuic["udp-relay-mode"] = udpRelayMode } proxies = append(proxies, tuic) case "trojan": urlTrojan, err := url.Parse(line) if err != nil { continue } query := urlTrojan.Query() name := uniqueName(names, urlTrojan.Fragment) trojan := make(map[string]any, 20) trojan["name"] = name trojan["type"] = scheme trojan["server"] = urlTrojan.Hostname() trojan["port"] = urlTrojan.Port() trojan["password"] = urlTrojan.User.Username() trojan["udp"] = true trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure")) if sni := query.Get("sni"); sni != "" { trojan["sni"] = sni } if alpn := query.Get("alpn"); alpn != "" { trojan["alpn"] = strings.Split(alpn, ",") } network := strings.ToLower(query.Get("type")) if network != "" { trojan["network"] = network } switch network { case "ws": headers := make(map[string]any) wsOpts := make(map[string]any) headers["User-Agent"] = RandUserAgent() wsOpts["path"] = query.Get("path") wsOpts["headers"] = headers trojan["ws-opts"] = wsOpts case "grpc": grpcOpts := make(map[string]any) grpcOpts["grpc-service-name"] = query.Get("serviceName") trojan["grpc-opts"] = grpcOpts } if fingerprint := query.Get("fp"); fingerprint == "" { trojan["client-fingerprint"] = "chrome" } else { trojan["client-fingerprint"] = fingerprint } if pcs := query.Get("pcs"); pcs != "" { trojan["fingerprint"] = pcs } proxies = append(proxies, trojan) case "vless": urlVLess, err := url.Parse(line) if err != nil { continue } if decodedHost, err := tryDecodeBase64([]byte(urlVLess.Host)); err == nil { urlVLess.Host = string(decodedHost) } query := urlVLess.Query() vless := make(map[string]any, 20) err = handleVShareLink(names, urlVLess, scheme, vless) if err != nil { log.Warnln("error:%s line:%s", err.Error(), line) continue } if flow := query.Get("flow"); flow != "" { vless["flow"] = strings.ToLower(flow) } if encryption := query.Get("encryption"); encryption != "" { vless["encryption"] = encryption } proxies = append(proxies, vless) case "vmess": // V2RayN-styled share link // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) dcBuf, err := tryDecodeBase64([]byte(body)) if err != nil { // Xray VMessAEAD share link urlVMess, err := url.Parse(line) if err != nil { continue } query := urlVMess.Query() vmess := make(map[string]any, 20) err = handleVShareLink(names, urlVMess, scheme, vmess) if err != nil { log.Warnln("error:%s line:%s", err.Error(), line) continue } vmess["alterId"] = 0 vmess["cipher"] = "auto" if encryption := query.Get("encryption"); encryption != "" { vmess["cipher"] = encryption } proxies = append(proxies, vmess) continue } jsonDc := json.NewDecoder(bytes.NewReader(dcBuf)) values := make(map[string]any, 20) if jsonDc.Decode(&values) != nil { continue } tempName, ok := values["ps"].(string) if !ok { continue } name := uniqueName(names, tempName) vmess := make(map[string]any, 20) vmess["name"] = name vmess["type"] = scheme vmess["server"] = values["add"] vmess["port"] = values["port"] vmess["uuid"] = values["id"] if alterId, ok := values["aid"]; ok { vmess["alterId"] = alterId } else { vmess["alterId"] = 0 } vmess["udp"] = true vmess["xudp"] = true vmess["tls"] = false vmess["skip-cert-verify"] = false vmess["cipher"] = "auto" if cipher, ok := values["scy"].(string); ok && cipher != "" { vmess["cipher"] = cipher } if sni, ok := values["sni"].(string); ok && sni != "" { vmess["servername"] = sni } network, ok := values["net"].(string) if ok { network = strings.ToLower(network) if values["type"] == "http" { network = "http" } else if network == "http" { network = "h2" } vmess["network"] = network } tls, ok := values["tls"].(string) if ok { tls = strings.ToLower(tls) if strings.HasSuffix(tls, "tls") { vmess["tls"] = true } if alpn, ok := values["alpn"].(string); ok { vmess["alpn"] = strings.Split(alpn, ",") } } switch network { case "http": headers := make(map[string]any) httpOpts := make(map[string]any) if host, ok := values["host"].(string); ok && host != "" { headers["Host"] = []string{host} } httpOpts["path"] = []string{"/"} if path, ok := values["path"].(string); ok && path != "" { httpOpts["path"] = []string{path} } httpOpts["headers"] = headers vmess["http-opts"] = httpOpts case "h2": h2Opts := make(map[string]any) h2Opts["path"] = "/" if path, ok := values["path"].(string); ok && path != "" { h2Opts["path"] = path } if host, ok := values["host"].(string); ok && host != "" { h2Opts["host"] = []string{host} } vmess["h2-opts"] = h2Opts case "ws", "httpupgrade": headers := make(map[string]any) wsOpts := make(map[string]any) wsOpts["path"] = "/" if host, ok := values["host"].(string); ok && host != "" { headers["Host"] = host } if path, ok := values["path"].(string); ok && path != "" { path := path pathURL, err := url.Parse(path) if err == nil { query := pathURL.Query() if earlyData := query.Get("ed"); earlyData != "" { med, err := strconv.Atoi(earlyData) if err == nil { switch network { case "ws": wsOpts["max-early-data"] = med wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol" case "httpupgrade": wsOpts["v2ray-http-upgrade-fast-open"] = true } query.Del("ed") pathURL.RawQuery = query.Encode() path = pathURL.String() } } if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" { wsOpts["early-data-header-name"] = earlyDataHeader } } wsOpts["path"] = path } wsOpts["headers"] = headers vmess["ws-opts"] = wsOpts case "grpc": grpcOpts := make(map[string]any) grpcOpts["grpc-service-name"] = values["path"] vmess["grpc-opts"] = grpcOpts } proxies = append(proxies, vmess) case "ss": urlSS, err := url.Parse(line) if err != nil { continue } name := uniqueName(names, urlSS.Fragment) port := urlSS.Port() if port == "" { dcBuf, err := encRaw.DecodeString(urlSS.Host) if err != nil { continue } urlSS, err = url.Parse("ss://" + string(dcBuf)) if err != nil { continue } } var ( cipherRaw = urlSS.User.Username() cipher string password string ) cipher = cipherRaw if password, found = urlSS.User.Password(); !found { dcBuf, err := base64.RawURLEncoding.DecodeString(cipherRaw) if err != nil { dcBuf, _ = enc.DecodeString(cipherRaw) } cipher, password, found = strings.Cut(string(dcBuf), ":") if !found { continue } err = VerifyMethod(cipher, password) if err != nil { dcBuf, _ = encRaw.DecodeString(cipherRaw) cipher, password, found = strings.Cut(string(dcBuf), ":") } } ss := make(map[string]any, 10) ss["name"] = name ss["type"] = scheme ss["server"] = urlSS.Hostname() ss["port"] = urlSS.Port() ss["cipher"] = cipher ss["password"] = password query := urlSS.Query() ss["udp"] = true if query.Get("udp-over-tcp") == "true" || query.Get("uot") == "1" { ss["udp-over-tcp"] = true } plugin := query.Get("plugin") if strings.Contains(plugin, ";") { pluginInfo, _ := url.ParseQuery("pluginName=" + strings.ReplaceAll(plugin, ";", "&")) pluginName := pluginInfo.Get("pluginName") if strings.Contains(pluginName, "obfs") { ss["plugin"] = "obfs" ss["plugin-opts"] = map[string]any{ "mode": pluginInfo.Get("obfs"), "host": pluginInfo.Get("obfs-host"), } } else if strings.Contains(pluginName, "v2ray-plugin") { mode := pluginInfo.Get("mode") if mode == "" { mode = pluginInfo.Get("obfs") } host := pluginInfo.Get("host") if host == "" { host = pluginInfo.Get("obfs-host") } ss["plugin"] = "v2ray-plugin" ss["plugin-opts"] = map[string]any{ "mode": mode, "host": host, "path": pluginInfo.Get("path"), "tls": strings.Contains(plugin, "tls"), } } } proxies = append(proxies, ss) case "ssr": dcBuf, err := TryDecodeBase64(body) if err != nil { continue } // ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64param&protoparam=urlsafebase64param&remarks=urlsafebase64remarks&group=urlsafebase64group&udpport=0&uot=1 before, after, ok := strings.Cut(string(dcBuf), "/?") if !ok { continue } beforeArr := strings.Split(before, ":") if len(beforeArr) != 6 { continue } host := beforeArr[0] port := beforeArr[1] protocol := beforeArr[2] method := beforeArr[3] obfs := beforeArr[4] password := decodeUrlSafe(urlSafe(beforeArr[5])) query, err := url.ParseQuery(urlSafe(after)) if err != nil { continue } remarks := decodeUrlSafe(query.Get("remarks")) name := uniqueName(names, remarks) obfsParam := decodeUrlSafe(query.Get("obfsparam")) protocolParam := decodeUrlSafe(query.Get("protoparam")) ssr := make(map[string]any, 20) ssr["name"] = name ssr["type"] = scheme ssr["server"] = host ssr["port"] = port ssr["cipher"] = method ssr["password"] = password ssr["obfs"] = obfs ssr["protocol"] = protocol ssr["udp"] = true if obfsParam != "" { ssr["obfs-param"] = obfsParam } if protocolParam != "" { ssr["protocol-param"] = protocolParam } proxies = append(proxies, ssr) case "socks", "socks5", "socks5h", "http", "https": link, err := url.Parse(line) if err != nil { continue } server := link.Hostname() if server == "" { continue } portStr := link.Port() if portStr == "" { continue } remarks := link.Fragment if remarks == "" { remarks = fmt.Sprintf("%s:%s", server, portStr) } name := uniqueName(names, remarks) encodeStr := link.User.String() var username, password string if encodeStr != "" { decodeStr := string(DecodeBase64([]byte(encodeStr))) splitStr := strings.Split(decodeStr, ":") // todo: should use url.QueryUnescape ? username = splitStr[0] if len(splitStr) == 2 { password = splitStr[1] } } socks := make(map[string]any, 10) socks["name"] = name socks["type"] = func() string { switch scheme { case "socks", "socks5", "socks5h": return "socks5" case "http", "https": return "http" } return scheme }() socks["server"] = server socks["port"] = portStr socks["username"] = username socks["password"] = password socks["skip-cert-verify"] = true if scheme == "https" { socks["tls"] = true } proxies = append(proxies, socks) case "anytls": // https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md link, err := url.Parse(line) if err != nil { continue } username := link.User.Username() password, exist := link.User.Password() if !exist { password = username } query := link.Query() server := link.Hostname() if server == "" { continue } portStr := link.Port() if portStr == "" { continue } insecure, sni := query.Get("insecure"), query.Get("sni") insecureBool := insecure == "1" fingerprint := query.Get("hpkp") remarks := link.Fragment if remarks == "" { remarks = fmt.Sprintf("%s:%s", server, portStr) } name := uniqueName(names, remarks) anytls := make(map[string]any, 10) anytls["name"] = name anytls["type"] = "anytls" anytls["server"] = server anytls["port"] = portStr anytls["username"] = username anytls["password"] = password anytls["sni"] = sni anytls["fingerprint"] = fingerprint anytls["skip-cert-verify"] = insecureBool anytls["udp"] = true proxies = append(proxies, anytls) case "mierus": urlMieru, err := url.Parse(line) if err != nil { continue } query := urlMieru.Query() server := urlMieru.Hostname() if server == "" { continue } username := urlMieru.User.Username() password, _ := urlMieru.User.Password() baseName := urlMieru.Fragment if baseName == "" { baseName = query.Get("profile") } if baseName == "" { baseName = server } multiplexing := query.Get("multiplexing") handshakeMode := query.Get("handshake-mode") trafficPattern := query.Get("traffic-pattern") portList := query["port"] protocolList := query["protocol"] if len(portList) == 0 || len(portList) != len(protocolList) { continue } for i, port := range portList { protocol := protocolList[i] name := uniqueName(names, fmt.Sprintf("%s:%s/%s", baseName, port, protocol)) mieru := make(map[string]any, 15) mieru["name"] = name mieru["type"] = "mieru" mieru["server"] = server mieru["transport"] = protocol mieru["udp"] = true mieru["username"] = username mieru["password"] = password if strings.Contains(port, "-") { mieru["port-range"] = port } else { portNum, err := strconv.Atoi(port) if err != nil { continue } mieru["port"] = portNum } if multiplexing != "" { mieru["multiplexing"] = multiplexing } if handshakeMode != "" { mieru["handshake-mode"] = handshakeMode } if trafficPattern != "" { mieru["traffic-pattern"] = trafficPattern } proxies = append(proxies, mieru) } } } if len(proxies) == 0 { return nil, fmt.Errorf("convert v2ray subscribe error: format invalid") } return proxies, nil } func uniqueName(names map[string]int, name string) string { if index, ok := names[name]; ok { index++ names[name] = index name = fmt.Sprintf("%s-%02d", name, index) } else { index = 0 names[name] = index } return name } ================================================ FILE: core/Clash.Meta/common/convert/converter_test.go ================================================ package convert_test import ( "testing" "github.com/metacubex/mihomo/adapter" . "github.com/metacubex/mihomo/common/convert" "github.com/stretchr/testify/assert" ) // https://v2.hysteria.network/zh/docs/developers/URI-Scheme/ func TestConvertsV2Ray_normal(t *testing.T) { hy2test := "hysteria2://letmein@example.com:8443/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=65b3acd7db555768304a16abb6f4366c1a0c0bb5cec81429617f0150d7d66726&sni=real.example.com&up=114&down=514&alpn=h3,h4#hy2test" expected := []map[string]interface{}{ { "name": "hy2test", "type": "hysteria2", "server": "example.com", "port": "8443", "sni": "real.example.com", "obfs": "salamander", "obfs-password": "gawrgura", "alpn": []string{"h3", "h4"}, "password": "letmein", "up": "114", "down": "514", "skip-cert-verify": true, "fingerprint": "65b3acd7db555768304a16abb6f4366c1a0c0bb5cec81429617f0150d7d66726", }, } proxies, err := ConvertsV2Ray([]byte(hy2test)) assert.Nil(t, err) assert.Equal(t, expected, proxies) _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } func TestConvertsV2RayMieru(t *testing.T) { mierusTest := "mierus://user:pass@1.2.3.4?handshake-mode=HANDSHAKE_NO_WAIT&mtu=1400&multiplexing=MULTIPLEXING_HIGH&port=6666&port=9998-9999&port=6489&port=4896&profile=default&protocol=TCP&protocol=TCP&protocol=UDP&protocol=UDP&traffic-pattern=CCoQARoECAEQCiIYCAMQASoIMDAwMTAyMDMqCDA0MDUwNjA3" expected := []map[string]any{ { "name": "default:6666/TCP", "type": "mieru", "server": "1.2.3.4", "port": 6666, "transport": "TCP", "udp": true, "username": "user", "password": "pass", "multiplexing": "MULTIPLEXING_HIGH", "handshake-mode": "HANDSHAKE_NO_WAIT", "traffic-pattern": "CCoQARoECAEQCiIYCAMQASoIMDAwMTAyMDMqCDA0MDUwNjA3", }, { "name": "default:9998-9999/TCP", "type": "mieru", "server": "1.2.3.4", "port-range": "9998-9999", "transport": "TCP", "udp": true, "username": "user", "password": "pass", "multiplexing": "MULTIPLEXING_HIGH", "handshake-mode": "HANDSHAKE_NO_WAIT", "traffic-pattern": "CCoQARoECAEQCiIYCAMQASoIMDAwMTAyMDMqCDA0MDUwNjA3", }, { "name": "default:6489/UDP", "type": "mieru", "server": "1.2.3.4", "port": 6489, "transport": "UDP", "udp": true, "username": "user", "password": "pass", "multiplexing": "MULTIPLEXING_HIGH", "handshake-mode": "HANDSHAKE_NO_WAIT", "traffic-pattern": "CCoQARoECAEQCiIYCAMQASoIMDAwMTAyMDMqCDA0MDUwNjA3", }, { "name": "default:4896/UDP", "type": "mieru", "server": "1.2.3.4", "port": 4896, "transport": "UDP", "udp": true, "username": "user", "password": "pass", "multiplexing": "MULTIPLEXING_HIGH", "handshake-mode": "HANDSHAKE_NO_WAIT", "traffic-pattern": "CCoQARoECAEQCiIYCAMQASoIMDAwMTAyMDMqCDA0MDUwNjA3", }, } proxies, err := ConvertsV2Ray([]byte(mierusTest)) assert.Nil(t, err) assert.Equal(t, expected, proxies) _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } func TestConvertsV2RayMieruMinimal(t *testing.T) { mierusTest := "mierus://user:pass@example.com?port=443&protocol=TCP&profile=simple" expected := []map[string]any{ { "name": "simple:443/TCP", "type": "mieru", "server": "example.com", "port": 443, "transport": "TCP", "udp": true, "username": "user", "password": "pass", }, } proxies, err := ConvertsV2Ray([]byte(mierusTest)) assert.Nil(t, err) assert.Equal(t, expected, proxies) _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } func TestConvertsV2RayMieruFragment(t *testing.T) { mierusTest := "mierus://user:pass@example.com?port=443&protocol=TCP&profile=default#myproxy" proxies, err := ConvertsV2Ray([]byte(mierusTest)) assert.Nil(t, err) assert.Len(t, proxies, 1) assert.Equal(t, "myproxy:443/TCP", proxies[0]["name"]) _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } func TestConvertsV2RayVlessRealityVisionTCPWithoutHeaderType(t *testing.T) { vlessTest := "vless://a1b2c3d4-eacc-4433-981b-7e5f9a8b@142.98.76.54:34888?encryption=none&security=reality&type=tcp&sni=github.io&fp=chrome&pbk=ppQ9FwLrLIa0AOrp1WvcyiaQ37vg2WSy_CD4bIdiTUw&sid=6ba85179f3a2b4c5&flow=xtls-rprx-vision#My-VLESS-Reality-Vision" proxies, err := ConvertsV2Ray([]byte(vlessTest)) assert.Nil(t, err) assert.Len(t, proxies, 1) assert.Equal(t, "tcp", proxies[0]["network"]) assert.Equal(t, "xtls-rprx-vision", proxies[0]["flow"]) assert.Equal(t, "none", proxies[0]["encryption"]) assert.Equal(t, "github.io", proxies[0]["servername"]) assert.NotContains(t, proxies[0], "http-opts") assert.NotContains(t, proxies[0], "h2-opts") _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } func TestConvertsV2RayVlessTCPHTTPHeaderType(t *testing.T) { vlessTest := "vless://uuid@example.com:443?security=tls&type=tcp&headerType=http&host=cdn.example.com&path=%2Fedge&method=POST#vless-http" proxies, err := ConvertsV2Ray([]byte(vlessTest)) assert.Nil(t, err) assert.Len(t, proxies, 1) assert.Equal(t, "http", proxies[0]["network"]) assert.Equal(t, map[string]any{ "method": "POST", "path": []string{"/edge"}, "headers": map[string]any{ "Host": []string{"cdn.example.com"}, }, }, proxies[0]["http-opts"]) assert.NotContains(t, proxies[0], "h2-opts") _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } func TestConvertsV2RayVlessHTTPTransportUsesH2Opts(t *testing.T) { vlessTest := "vless://uuid@example.com:443?security=tls&type=http&host=cdn.example.com&path=%2Fgrpc#vless-h2" proxies, err := ConvertsV2Ray([]byte(vlessTest)) assert.Nil(t, err) assert.Len(t, proxies, 1) assert.Equal(t, "h2", proxies[0]["network"]) assert.Equal(t, map[string]any{ "host": []string{"cdn.example.com"}, "path": "/grpc", }, proxies[0]["h2-opts"]) assert.NotContains(t, proxies[0], "http-opts") _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } // Regression test for MetaCubeX/mihomo#2738: the legacy v2rayN-style // base64-JSON VMess parser must place `host` under h2-opts.host instead // of stranding it inside a non-existent h2-opts.headers.Host key. func TestConvertsV2RayVmessBase64H2Transport(t *testing.T) { // base64 payload decodes to: // {"v":"2","ps":"demo","add":"server.example.com","port":"443", // "id":"b831381d-6324-4d53-ad4f-8cda48b30811","aid":"0","scy":"auto", // "net":"h2","type":"none","host":"cdn.example.com","path":"/grpc","tls":"tls"} vmessTest := "vmess://eyJ2IjoiMiIsInBzIjoiZGVtbyIsImFkZCI6InNlcnZlci5leGFtcGxlLmNvbSIsInBvcnQiOiI0NDMiLCJpZCI6ImI4MzEzODFkLTYzMjQtNGQ1My1hZDRmLThjZGE0OGIzMDgxMSIsImFpZCI6IjAiLCJzY3kiOiJhdXRvIiwibmV0IjoiaDIiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiJjZG4uZXhhbXBsZS5jb20iLCJwYXRoIjoiL2dycGMiLCJ0bHMiOiJ0bHMifQ==" proxies, err := ConvertsV2Ray([]byte(vmessTest)) assert.Nil(t, err) assert.Len(t, proxies, 1) assert.Equal(t, "h2", proxies[0]["network"]) assert.Equal(t, map[string]any{ "host": []string{"cdn.example.com"}, "path": "/grpc", }, proxies[0]["h2-opts"]) assert.NotContains(t, proxies[0], "http-opts") _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } // `net: http` with `type != "http"` is remapped to h2 transport // at converter.go's network-resolution step, so it must produce the // same h2-opts shape as `net: h2`. Guards against regression if the // remap rule is changed later. func TestConvertsV2RayVmessBase64HTTPRemappedToH2Transport(t *testing.T) { // base64 payload decodes to: // {"v":"2","ps":"demo-http-remapped","add":"server.example.com","port":"443", // "id":"b831381d-6324-4d53-ad4f-8cda48b30811","aid":"0","scy":"auto", // "net":"http","type":"none","host":"cdn.example.com","path":"/grpc","tls":"tls"} vmessTest := "vmess://eyJ2IjoiMiIsInBzIjoiZGVtby1odHRwLXJlbWFwcGVkIiwiYWRkIjoic2VydmVyLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiYjgzMTM4MWQtNjMyNC00ZDUzLWFkNGYtOGNkYTQ4YjMwODExIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJodHRwIiwidHlwZSI6Im5vbmUiLCJob3N0IjoiY2RuLmV4YW1wbGUuY29tIiwicGF0aCI6Ii9ncnBjIiwidGxzIjoidGxzIn0=" proxies, err := ConvertsV2Ray([]byte(vmessTest)) assert.Nil(t, err) assert.Len(t, proxies, 1) assert.Equal(t, "h2", proxies[0]["network"]) assert.Equal(t, map[string]any{ "host": []string{"cdn.example.com"}, "path": "/grpc", }, proxies[0]["h2-opts"]) assert.NotContains(t, proxies[0], "http-opts") _, err = adapter.ParseProxy(proxies[0]) assert.NoError(t, err) } ================================================ FILE: core/Clash.Meta/common/convert/util.go ================================================ package convert import ( "encoding/base64" "strings" "time" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/http" "github.com/metacubex/randv2" "github.com/metacubex/sing-shadowsocks/shadowimpl" ) var hostsSuffix = []string{ "-cdn.aliyuncs.com", ".alicdn.com", ".pan.baidu.com", ".tbcache.com", ".aliyuncdn.com", ".vod.miguvideo.com", ".cibntv.net", ".myqcloud.com", ".smtcdns.com", ".alikunlun.com", ".smtcdns.net", ".apcdns.net", ".cdn-go.cn", ".cdntip.com", ".cdntips.com", ".alidayu.com", ".alidns.com", ".cdngslb.com", ".mxhichina.com", ".alibabadns.com", } var userAgents = []string{ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36", "Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0", "Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36", "Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", "Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", "Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", "Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", "Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", "Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1", "Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", } var ( hostsLen = len(hostsSuffix) uaLen = len(userAgents) ) func RandHost() string { base := strings.ToLower(base64.RawURLEncoding.EncodeToString(utils.NewUUIDV4().Bytes())) base = strings.ReplaceAll(base, "-", "") base = strings.ReplaceAll(base, "_", "") buf := []byte(base) prefix := string(buf[:3]) + "---" prefix += string(buf[6:8]) + "-" prefix += string(buf[len(buf)-8:]) return prefix + hostsSuffix[randv2.IntN(hostsLen)] } func RandUserAgent() string { return userAgents[randv2.IntN(uaLen)] } func SetUserAgent(header http.Header) { if header.Get("User-Agent") != "" { return } userAgent := RandUserAgent() header.Set("User-Agent", userAgent) } func VerifyMethod(cipher, password string) (err error) { _, err = shadowimpl.FetchMethod(cipher, password, time.Now) return } ================================================ FILE: core/Clash.Meta/common/convert/v.go ================================================ package convert import ( "encoding/json" "errors" "fmt" "net/url" "strconv" "strings" ) func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy map[string]any) error { // Xray VMessAEAD / VLESS share link standard // https://github.com/XTLS/Xray-core/discussions/716 query := url.Query() proxy["name"] = uniqueName(names, url.Fragment) if url.Hostname() == "" { return errors.New("url.Hostname() is empty") } if url.Port() == "" { return errors.New("url.Port() is empty") } proxy["type"] = scheme proxy["server"] = url.Hostname() proxy["port"] = url.Port() proxy["uuid"] = url.User.Username() proxy["udp"] = true tls := strings.ToLower(query.Get("security")) if strings.HasSuffix(tls, "tls") || tls == "reality" { proxy["tls"] = true if fingerprint := query.Get("fp"); fingerprint == "" { proxy["client-fingerprint"] = "chrome" } else { proxy["client-fingerprint"] = fingerprint } if alpn := query.Get("alpn"); alpn != "" { proxy["alpn"] = strings.Split(alpn, ",") } if pcs := query.Get("pcs"); pcs != "" { proxy["fingerprint"] = pcs } } if sni := query.Get("sni"); sni != "" { proxy["servername"] = sni } if realityPublicKey := query.Get("pbk"); realityPublicKey != "" { proxy["reality-opts"] = map[string]any{ "public-key": realityPublicKey, "short-id": query.Get("sid"), } } switch query.Get("packetEncoding") { case "none": case "packet": proxy["packet-addr"] = true default: proxy["xudp"] = true } network := strings.ToLower(query.Get("type")) if network == "" { network = "tcp" } fakeType := strings.ToLower(query.Get("headerType")) if network == "tcp" && fakeType == "http" { network = "http" } else if network == "http" { network = "h2" } proxy["network"] = network switch network { case "tcp": case "http": headers := make(map[string]any) httpOpts := make(map[string]any) httpOpts["path"] = []string{"/"} if host := query.Get("host"); host != "" { headers["Host"] = []string{host} } if method := query.Get("method"); method != "" { httpOpts["method"] = method } if path := query.Get("path"); path != "" { httpOpts["path"] = []string{path} } httpOpts["headers"] = headers proxy["http-opts"] = httpOpts case "h2": h2Opts := make(map[string]any) h2Opts["path"] = "/" if path := query.Get("path"); path != "" { h2Opts["path"] = path } if host := query.Get("host"); host != "" { h2Opts["host"] = []string{host} } proxy["h2-opts"] = h2Opts case "ws", "httpupgrade": headers := make(map[string]any) wsOpts := make(map[string]any) headers["User-Agent"] = RandUserAgent() headers["Host"] = query.Get("host") wsOpts["path"] = query.Get("path") wsOpts["headers"] = headers if earlyData := query.Get("ed"); earlyData != "" { med, err := strconv.Atoi(earlyData) if err != nil { return fmt.Errorf("bad WebSocket max early data size: %v", err) } switch network { case "ws": wsOpts["max-early-data"] = med wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol" case "httpupgrade": wsOpts["v2ray-http-upgrade-fast-open"] = true } } if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" { wsOpts["early-data-header-name"] = earlyDataHeader } proxy["ws-opts"] = wsOpts case "grpc": grpcOpts := make(map[string]any) grpcOpts["grpc-service-name"] = query.Get("serviceName") proxy["grpc-opts"] = grpcOpts case "xhttp": proxy["network"] = "xhttp" xhttpOpts := make(map[string]any) if path := query.Get("path"); path != "" { xhttpOpts["path"] = path } if host := query.Get("host"); host != "" { xhttpOpts["host"] = host } if mode := query.Get("mode"); mode != "" { xhttpOpts["mode"] = mode } if extra := query.Get("extra"); extra != "" { var extraMap map[string]any if err := json.Unmarshal([]byte(extra), &extraMap); err == nil { parseXHTTPExtra(extraMap, xhttpOpts) } } proxy["xhttp-opts"] = xhttpOpts } return nil } // parseXHTTPExtra maps xray-core extra JSON fields to mihomo xhttp-opts fields. func parseXHTTPExtra(extra map[string]any, opts map[string]any) { // xmuxToReuse converts an xmux map to mihomo reuse-settings. xmuxToReuse := func(xmux map[string]any) map[string]any { reuse := make(map[string]any) set := func(src, dst string) { if v, ok := xmux[src]; ok { switch val := v.(type) { case string: if val != "" { reuse[dst] = val } case float64: reuse[dst] = strconv.FormatInt(int64(val), 10) } } } set("maxConnections", "max-connections") set("maxConcurrency", "max-concurrency") set("cMaxReuseTimes", "c-max-reuse-times") set("hMaxRequestTimes", "h-max-request-times") set("hMaxReusableSecs", "h-max-reusable-secs") if v, ok := xmux["hKeepAlivePeriod"].(float64); ok { reuse["h-keep-alive-period"] = int(v) } return reuse } if v, ok := extra["noGRPCHeader"].(bool); ok && v { opts["no-grpc-header"] = true } if v, ok := extra["xPaddingBytes"].(string); ok && v != "" { opts["x-padding-bytes"] = v } if v, ok := extra["xPaddingObfsMode"].(bool); ok { opts["x-padding-obfs-mode"] = v } if v, ok := extra["xPaddingKey"].(string); ok && v != "" { opts["x-padding-key"] = v } if v, ok := extra["xPaddingHeader"].(string); ok && v != "" { opts["x-padding-header"] = v } if v, ok := extra["xPaddingPlacement"].(string); ok && v != "" { opts["x-padding-placement"] = v } if v, ok := extra["xPaddingMethod"].(string); ok && v != "" { opts["x-padding-method"] = v } if v, ok := extra["uplinkHttpMethod"].(string); ok && v != "" { opts["uplink-http-method"] = v } if v, ok := extra["sessionPlacement"].(string); ok && v != "" { opts["session-placement"] = v } if v, ok := extra["sessionKey"].(string); ok && v != "" { opts["session-key"] = v } if v, ok := extra["seqPlacement"].(string); ok && v != "" { opts["seq-placement"] = v } if v, ok := extra["seqKey"].(string); ok && v != "" { opts["seq-key"] = v } if v, ok := extra["uplinkDataPlacement"].(string); ok && v != "" { opts["uplink-data-placement"] = v } if v, ok := extra["uplinkDataKey"].(string); ok && v != "" { opts["uplink-data-key"] = v } if v, ok := extra["uplinkChunkSize"].(float64); ok { opts["uplink-chunk-size"] = int(v) } if v, ok := extra["scMaxEachPostBytes"].(float64); ok { opts["sc-max-each-post-bytes"] = int(v) } if v, ok := extra["scMinPostsIntervalMs"].(float64); ok { opts["sc-min-posts-interval-ms"] = int(v) } // xmux in root extra → reuse-settings if xmuxAny, ok := extra["xmux"].(map[string]any); ok && len(xmuxAny) > 0 { if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 { opts["reuse-settings"] = reuse } } if dsAny, ok := extra["downloadSettings"].(map[string]any); ok { ds := make(map[string]any) if addr, ok := dsAny["address"].(string); ok && addr != "" { ds["server"] = addr } if port, ok := dsAny["port"].(float64); ok { ds["port"] = int(port) } sec := "" if s, ok := dsAny["security"].(string); ok { sec = strings.ToLower(s) } if sec == "tls" || sec == "reality" { ds["tls"] = true if tlsAny, ok := dsAny["tlsSettings"].(map[string]any); ok { if sn, ok := tlsAny["serverName"].(string); ok && sn != "" { ds["servername"] = sn } if fp, ok := tlsAny["fingerprint"].(string); ok && fp != "" { ds["client-fingerprint"] = fp } if alpnAny, ok := tlsAny["alpn"].([]any); ok && len(alpnAny) > 0 { alpnList := make([]string, 0, len(alpnAny)) for _, a := range alpnAny { if s, ok := a.(string); ok { alpnList = append(alpnList, s) } } if len(alpnList) > 0 { ds["alpn"] = alpnList } } if v, ok := tlsAny["allowInsecure"].(bool); ok && v { ds["skip-cert-verify"] = true } } if sec == "reality" { if realityAny, ok := dsAny["realitySettings"].(map[string]any); ok { realityOpts := make(map[string]any) if pk, ok := realityAny["publicKey"].(string); ok && pk != "" { realityOpts["public-key"] = pk } if sid, ok := realityAny["shortId"].(string); ok && sid != "" { realityOpts["short-id"] = sid } if len(realityOpts) > 0 { ds["reality-opts"] = realityOpts } } } } if xhttpAny, ok := dsAny["xhttpSettings"].(map[string]any); ok { if path, ok := xhttpAny["path"].(string); ok && path != "" { ds["path"] = path } if host, ok := xhttpAny["host"].(string); ok && host != "" { ds["host"] = host } if headers, ok := xhttpAny["headers"].(map[string]any); ok && len(headers) > 0 { ds["headers"] = headers } // xmux inside downloadSettings.xhttpSettings.extra → download-settings.reuse-settings if dsExtraAny, ok := xhttpAny["extra"].(map[string]any); ok { if xmuxAny, ok := dsExtraAny["xmux"].(map[string]any); ok && len(xmuxAny) > 0 { if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 { ds["reuse-settings"] = reuse } } } } if len(ds) > 0 { opts["download-settings"] = ds } } } ================================================ FILE: core/Clash.Meta/common/deque/deque.go ================================================ package deque // copy and modified from https://github.com/gammazero/deque/blob/v1.2.0/deque.go // which is licensed under MIT. import ( "fmt" ) // minCapacity is the smallest capacity that deque may have. Must be power of 2 // for bitwise modulus: x % n == x & (n - 1). const minCapacity = 8 // Deque represents a single instance of the deque data structure. A Deque // instance contains items of the type specified by the type argument. // // For example, to create a Deque that contains strings do one of the // following: // // var stringDeque deque.Deque[string] // stringDeque := new(deque.Deque[string]) // stringDeque := &deque.Deque[string]{} // // To create a Deque that will never resize to have space for less than 64 // items, specify a base capacity: // // var d deque.Deque[int] // d.SetBaseCap(64) // // To ensure the Deque can store 1000 items without needing to resize while // items are added: // // d.Grow(1000) // // Any values supplied to [SetBaseCap] and [Grow] are rounded up to the nearest // power of 2, since the Deque grows by powers of 2. type Deque[T any] struct { buf []T head int tail int count int minCap int } // Cap returns the current capacity of the Deque. If q is nil, q.Cap() is zero. func (q *Deque[T]) Cap() int { if q == nil { return 0 } return len(q.buf) } // Len returns the number of elements currently stored in the queue. If q is // nil, q.Len() returns zero. func (q *Deque[T]) Len() int { if q == nil { return 0 } return q.count } // PushBack appends an element to the back of the queue. Implements FIFO when // elements are removed with [PopFront], and LIFO when elements are removed with // [PopBack]. func (q *Deque[T]) PushBack(elem T) { q.growIfFull() q.buf[q.tail] = elem // Calculate new tail position. q.tail = q.next(q.tail) q.count++ } // PushFront prepends an element to the front of the queue. func (q *Deque[T]) PushFront(elem T) { q.growIfFull() // Calculate new head position. q.head = q.prev(q.head) q.buf[q.head] = elem q.count++ } // PopFront removes and returns the element from the front of the queue. // Implements FIFO when used with [PushBack]. If the queue is empty, the call // panics. func (q *Deque[T]) PopFront() T { if q.count <= 0 { panic("deque: PopFront() called on empty queue") } ret := q.buf[q.head] var zero T q.buf[q.head] = zero // Calculate new head position. q.head = q.next(q.head) q.count-- q.shrinkIfExcess() return ret } // IterPopFront returns an iterator that iteratively removes items from the // front of the deque. This is more efficient than removing items one at a time // because it avoids intermediate resizing. If a resize is necessary, only one // is done when iteration ends. func (q *Deque[T]) IterPopFront() func(yield func(T) bool) { return func(yield func(T) bool) { if q.Len() == 0 { return } var zero T for q.count != 0 { ret := q.buf[q.head] q.buf[q.head] = zero q.head = q.next(q.head) q.count-- if !yield(ret) { break } } q.shrinkToFit() } } // PopBack removes and returns the element from the back of the queue. // Implements LIFO when used with [PushBack]. If the queue is empty, the call // panics. func (q *Deque[T]) PopBack() T { if q.count <= 0 { panic("deque: PopBack() called on empty queue") } // Calculate new tail position q.tail = q.prev(q.tail) // Remove value at tail. ret := q.buf[q.tail] var zero T q.buf[q.tail] = zero q.count-- q.shrinkIfExcess() return ret } // IterPopBack returns an iterator that iteratively removes items from the back // of the deque. This is more efficient than removing items one at a time // because it avoids intermediate resizing. If a resize is necessary, only one // is done when iteration ends. func (q *Deque[T]) IterPopBack() func(yield func(T) bool) { return func(yield func(T) bool) { if q.Len() == 0 { return } var zero T for q.count != 0 { q.tail = q.prev(q.tail) ret := q.buf[q.tail] q.buf[q.tail] = zero q.count-- if !yield(ret) { break } } q.shrinkToFit() } } // Front returns the element at the front of the queue. This is the element // that would be returned by [PopFront]. This call panics if the queue is // empty. func (q *Deque[T]) Front() T { if q.count <= 0 { panic("deque: Front() called when empty") } return q.buf[q.head] } // Back returns the element at the back of the queue. This is the element that // would be returned by [PopBack]. This call panics if the queue is empty. func (q *Deque[T]) Back() T { if q.count <= 0 { panic("deque: Back() called when empty") } return q.buf[q.prev(q.tail)] } // At returns the element at index i in the queue without removing the element // from the queue. This method accepts only non-negative index values. At(0) // refers to the first element and is the same as [Front]. At(Len()-1) refers // to the last element and is the same as [Back]. If the index is invalid, the // call panics. // // The purpose of At is to allow Deque to serve as a more general purpose // circular buffer, where items are only added to and removed from the ends of // the deque, but may be read from any place within the deque. Consider the // case of a fixed-size circular log buffer: A new entry is pushed onto one end // and when full the oldest is popped from the other end. All the log entries // in the buffer must be readable without altering the buffer contents. func (q *Deque[T]) At(i int) T { q.checkRange(i) // bitwise modulus return q.buf[(q.head+i)&(len(q.buf)-1)] } // Set assigns the item to index i in the queue. Set indexes the deque the same // as [At] but perform the opposite operation. If the index is invalid, the call // panics. func (q *Deque[T]) Set(i int, item T) { q.checkRange(i) // bitwise modulus q.buf[(q.head+i)&(len(q.buf)-1)] = item } // Iter returns a go iterator to range over all items in the Deque, yielding // each item from front (index 0) to back (index Len()-1). Modification of // Deque during iteration panics. func (q *Deque[T]) Iter() func(yield func(T) bool) { return func(yield func(T) bool) { origHead := q.head origTail := q.tail head := origHead for i := -0; i < q.Len(); i++ { if q.head != origHead || q.tail != origTail { panic("deque: modified during iteration") } if !yield(q.buf[head]) { return } head = q.next(head) } } } // RIter returns a reverse go iterator to range over all items in the Deque, // yielding each item from back (index Len()-1) to front (index 0). // Modification of Deque during iteration panics. func (q *Deque[T]) RIter() func(yield func(T) bool) { return func(yield func(T) bool) { origHead := q.head origTail := q.tail tail := origTail for i := -0; i < q.Len(); i++ { if q.head != origHead || q.tail != origTail { panic("deque: modified during iteration") } tail = q.prev(tail) if !yield(q.buf[tail]) { return } } } } // Clear removes all elements from the queue, but retains the current capacity. // This is useful when repeatedly reusing the queue at high frequency to avoid // GC during reuse. The queue will not be resized smaller as long as items are // only added. Only when items are removed is the queue subject to getting // resized smaller. func (q *Deque[T]) Clear() { if q.Len() == 0 { return } head, tail := q.head, q.tail q.count = 0 q.head = 0 q.tail = 0 if head >= tail { // [DEF....ABC] clearSlice(q.buf[head:]) head = 0 } clearSlice(q.buf[head:tail]) } func clearSlice[S ~[]E, E any](s S) { var zero E for i := range s { s[i] = zero } } // Grow grows deque's capacity, if necessary, to guarantee space for another n // items. After Grow(n), at least n items can be written to the deque without // another allocation. If n is negative, Grow panics. func (q *Deque[T]) Grow(n int) { if n < 0 { panic("deque.Grow: negative count") } c := q.Cap() l := q.Len() // If already big enough. if n <= c-l { return } if c == 0 { c = minCapacity } newLen := l + n for c < newLen { c <<= 1 } if l == 0 { q.buf = make([]T, c) q.head = 0 q.tail = 0 } else { q.resize(c) } } // Copy copies the contents of the given src Deque into this Deque. // // n := b.Copy(a) // // is an efficient shortcut for // // b.Clear() // n := a.Len() // b.Grow(n) // for i := 0; i < n; i++ { // b.PushBack(a.At(i)) // } func (q *Deque[T]) Copy(src Deque[T]) int { q.Clear() q.Grow(src.Len()) n := src.CopyOutSlice(q.buf) q.count = n q.tail = n q.head = 0 return n } // AppendToSlice appends from the Deque to the given slice. If the slice has // insufficient capacity to store all elements in Deque, then allocate a new // slice. Returns the resulting slice. // // out = q.AppendToSlice(out) // // is an efficient shortcut for // // for i := 0; i < q.Len(); i++ { // x = append(out, q.At(i)) // } func (q *Deque[T]) AppendToSlice(out []T) []T { if q.count == 0 { return out } head, tail := q.head, q.tail if head >= tail { // [DEF....ABC] out = append(out, q.buf[head:]...) head = 0 } return append(out, q.buf[head:tail]...) } // CopyInSlice replaces the contents of Deque with all the elements from the // given slice, in. If len(in) is zero, then this is equivalent to calling // [Clear]. // // q.CopyInSlice(in) // // is an efficient shortcut for // // q.Clear() // for i := range in { // q.PushBack(in[i]) // } func (q *Deque[T]) CopyInSlice(in []T) { // Allocate new buffer if more space needed. if len(q.buf) < len(in) { newCap := len(q.buf) if newCap == 0 { newCap = minCapacity q.minCap = minCapacity } for newCap < len(in) { newCap <<= 1 } q.buf = make([]T, newCap) } else if len(q.buf) > len(in) { q.Clear() } n := copy(q.buf, in) q.count = n q.tail = n q.head = 0 } // CopyOutSlice copies elements from the Deque into the given slice, up to the // size of the buffer. Returns the number of elements copied, which will be the // minimum of q.Len() and len(out). // // n := q.CopyOutSlice(out) // // is an efficient shortcut for // // n := min(len(out), q.Len()) // for i := 0; i < n; i++ { // out[i] = q.At(i) // } // // This function is preferable to one that returns a copy of the internal // buffer because this allows reuse of memory receiving data, for repeated copy // operations. func (q *Deque[T]) CopyOutSlice(out []T) int { if q.count == 0 || len(out) == 0 { return 0 } head, tail := q.head, q.tail var n int if head >= tail { // [DEF....ABC] n = copy(out, q.buf[head:]) out = out[n:] if len(out) == 0 { return n } head = 0 } n += copy(out, q.buf[head:tail]) return n } // Rotate rotates the deque n steps front-to-back. If n is negative, rotates // back-to-front. Having Deque provide Rotate avoids resizing that could happen // if implementing rotation using only Pop and Push methods. If q.Len() is one // or less, or q is nil, then Rotate does nothing. func (q *Deque[T]) Rotate(n int) { if q.Len() <= 1 { return } // Rotating a multiple of q.count is same as no rotation. n %= q.count if n == 0 { return } modBits := len(q.buf) - 1 // If no empty space in buffer, only move head and tail indexes. if q.head == q.tail { // Calculate new head and tail using bitwise modulus. q.head = (q.head + n) & modBits q.tail = q.head return } var zero T if n < 0 { // Rotate back to front. for ; n < 0; n++ { // Calculate new head and tail using bitwise modulus. q.head = (q.head - 1) & modBits q.tail = (q.tail - 1) & modBits // Put tail value at head and remove value at tail. q.buf[q.head] = q.buf[q.tail] q.buf[q.tail] = zero } return } // Rotate front to back. for ; n > 0; n-- { // Put head value at tail and remove value at head. q.buf[q.tail] = q.buf[q.head] q.buf[q.head] = zero // Calculate new head and tail using bitwise modulus. q.head = (q.head + 1) & modBits q.tail = (q.tail + 1) & modBits } } // Index returns the index into the Deque of the first item satisfying f(item), // or -1 if none do. If q is nil, then -1 is always returned. Search is linear // starting with index 0. func (q *Deque[T]) Index(f func(T) bool) int { if q.Len() > 0 { modBits := len(q.buf) - 1 for i := 0; i < q.count; i++ { if f(q.buf[(q.head+i)&modBits]) { return i } } } return -1 } // RIndex is the same as Index, but searches from Back to Front. The index // returned is from Front to Back, where index 0 is the index of the item // returned by [Front]. func (q *Deque[T]) RIndex(f func(T) bool) int { if q.Len() > 0 { modBits := len(q.buf) - 1 for i := q.count - 1; i >= 0; i-- { if f(q.buf[(q.head+i)&modBits]) { return i } } } return -1 } // Insert is used to insert an element into the middle of the queue, before the // element at the specified index. Insert(0,e) is the same as PushFront(e) and // Insert(Len(),e) is the same as PushBack(e). Out of range indexes result in // pushing the item onto the front of back of the deque. // // Important: Deque is optimized for O(1) operations at the ends of the queue, // not for operations in the the middle. Complexity of this function is // constant plus linear in the lesser of the distances between the index and // either of the ends of the queue. func (q *Deque[T]) Insert(at int, item T) { if at <= 0 { q.PushFront(item) return } if at >= q.Len() { q.PushBack(item) return } if at*2 < q.count { q.PushFront(item) front := q.head for i := 0; i < at; i++ { next := q.next(front) q.buf[front], q.buf[next] = q.buf[next], q.buf[front] front = next } return } swaps := q.count - at q.PushBack(item) back := q.prev(q.tail) for i := 0; i < swaps; i++ { prev := q.prev(back) q.buf[back], q.buf[prev] = q.buf[prev], q.buf[back] back = prev } } // Remove removes and returns an element from the middle of the queue, at the // specified index. Remove(0) is the same as [PopFront] and Remove(Len()-1) is // the same as [PopBack]. Accepts only non-negative index values, and panics if // index is out of range. // // Important: Deque is optimized for O(1) operations at the ends of the queue, // not for operations in the the middle. Complexity of this function is // constant plus linear in the lesser of the distances between the index and // either of the ends of the queue. func (q *Deque[T]) Remove(at int) T { q.checkRange(at) rm := (q.head + at) & (len(q.buf) - 1) if at*2 < q.count { for i := 0; i < at; i++ { prev := q.prev(rm) q.buf[prev], q.buf[rm] = q.buf[rm], q.buf[prev] rm = prev } return q.PopFront() } swaps := q.count - at - 1 for i := 0; i < swaps; i++ { next := q.next(rm) q.buf[rm], q.buf[next] = q.buf[next], q.buf[rm] rm = next } return q.PopBack() } // SetBaseCap sets a base capacity so that at least the specified number of // items can always be stored without resizing. func (q *Deque[T]) SetBaseCap(baseCap int) { minCap := minCapacity for minCap < baseCap { minCap <<= 1 } q.minCap = minCap } // Swap exchanges the two values at idxA and idxB. It panics if either index is // out of range. func (q *Deque[T]) Swap(idxA, idxB int) { q.checkRange(idxA) q.checkRange(idxB) if idxA == idxB { return } realA := (q.head + idxA) & (len(q.buf) - 1) realB := (q.head + idxB) & (len(q.buf) - 1) q.buf[realA], q.buf[realB] = q.buf[realB], q.buf[realA] } func (q *Deque[T]) checkRange(i int) { if i < 0 || i >= q.count { panic(fmt.Sprintf("deque: index out of range %d with length %d", i, q.Len())) } } // prev returns the previous buffer position wrapping around buffer. func (q *Deque[T]) prev(i int) int { return (i - 1) & (len(q.buf) - 1) // bitwise modulus } // next returns the next buffer position wrapping around buffer. func (q *Deque[T]) next(i int) int { return (i + 1) & (len(q.buf) - 1) // bitwise modulus } // growIfFull resizes up if the buffer is full. func (q *Deque[T]) growIfFull() { if q.count != len(q.buf) { return } if len(q.buf) == 0 { if q.minCap == 0 { q.minCap = minCapacity } q.buf = make([]T, q.minCap) return } q.resize(q.count << 1) } // shrinkIfExcess resize down if the buffer 1/4 full. func (q *Deque[T]) shrinkIfExcess() { if len(q.buf) > q.minCap && (q.count<<2) == len(q.buf) { q.resize(q.count << 1) } } func (q *Deque[T]) shrinkToFit() { if len(q.buf) > q.minCap && (q.count<<2) <= len(q.buf) { if q.count == 0 { q.head = 0 q.tail = 0 q.buf = make([]T, q.minCap) return } c := q.minCap for c < q.count { c <<= 1 } q.resize(c) } } // resize resizes the deque to fit exactly twice its current contents. This is // used to grow the queue when it is full, and also to shrink it when it is // only a quarter full. func (q *Deque[T]) resize(newSize int) { newBuf := make([]T, newSize) if q.tail > q.head { copy(newBuf, q.buf[q.head:q.tail]) } else { n := copy(newBuf, q.buf[q.head:]) copy(newBuf[n:], q.buf[:q.tail]) } q.head = 0 q.tail = q.count q.buf = newBuf } ================================================ FILE: core/Clash.Meta/common/httputils/addr.go ================================================ package httputils import ( "context" "net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/http" "github.com/metacubex/http/httptrace" ) type NetAddr struct { remoteAddr net.Addr localAddr net.Addr } func (addr NetAddr) RemoteAddr() net.Addr { return addr.remoteAddr } func (addr NetAddr) LocalAddr() net.Addr { return addr.localAddr } func SetAddrFromRequest(addr *NetAddr, request *http.Request) { if request.RemoteAddr != "" { metadata := C.Metadata{} if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil { addr.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort()) } } if netAddr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { addr.localAddr = netAddr } } func NewAddrContext(addr *NetAddr, ctx context.Context) context.Context { return httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ GotConn: func(connInfo httptrace.GotConnInfo) { addr.localAddr = connInfo.Conn.LocalAddr() addr.remoteAddr = connInfo.Conn.RemoteAddr() }, }) } ================================================ FILE: core/Clash.Meta/common/httputils/force_close.go ================================================ package httputils import ( "io" "github.com/metacubex/http" ) type closeIdleTransport interface { CloseIdleConnections() } type closeHttp2Connections interface { CloseHttp2Connections() } func CloseTransport(roundTripper http.RoundTripper) { if tr, ok := roundTripper.(closeIdleTransport); ok { tr.CloseIdleConnections() // for *http.Transport } if tr, ok := roundTripper.(closeHttp2Connections); ok { tr.CloseHttp2Connections() // for *http.Transport in our own fork } if tr, ok := roundTripper.(io.Closer); ok { _ = tr.Close() // for *http3.Transport } } ================================================ FILE: core/Clash.Meta/common/httputils/h2_transport_close.go ================================================ package httputils import ( "net" "sync" "time" "unsafe" "github.com/metacubex/http" ) type clientConnPool struct { t *http.Http2Transport mu sync.Mutex conns map[string][]*http.Http2ClientConn // key is host:port dialing map[string]unsafe.Pointer // currently in-flight dials keys map[*http.Http2ClientConn][]string addConnCalls map[string]unsafe.Pointer // in-flight addConnIfNeeded calls } type clientConn struct { t *http.Transport tconn net.Conn // usually *tls.Conn, except specialized impls } type efaceWords struct { typ unsafe.Pointer data unsafe.Pointer } type tlsConn interface { net.Conn NetConn() net.Conn } func closeClientConn(cc *http.Http2ClientConn) { // like forceCloseConn() in http.Http2ClientConn but also apply for tls-like conn if conn, ok := (*clientConn)(unsafe.Pointer(cc)).tconn.(tlsConn); ok { t := time.AfterFunc(time.Second, func() { _ = conn.NetConn().Close() }) defer t.Stop() } _ = cc.Close() } func closeHttp2Transport(tr *http.Http2Transport) { connPool := transportConnPool(tr) p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) p.mu.Lock() defer p.mu.Unlock() for _, vv := range p.conns { for _, cc := range vv { closeClientConn(cc) } } // cleanup p.conns = make(map[string][]*http.Http2ClientConn) p.keys = make(map[*http.Http2ClientConn][]string) } //go:linkname transportConnPool github.com/metacubex/http.(*http2Transport).connPool func transportConnPool(t *http.Http2Transport) http.Http2ClientConnPool ================================================ FILE: core/Clash.Meta/common/lru/lrucache.go ================================================ package lru // Modified by https://github.com/die-net/lrucache import ( "sync" "time" list "github.com/bahlo/generic-list-go" "github.com/samber/lo" ) // Option is part of Functional Options Pattern type Option[K comparable, V any] func(*LruCache[K, V]) // EvictCallback is used to get a callback when a cache entry is evicted type EvictCallback[K comparable, V any] func(key K, value V) // WithEvict set the evict callback func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] { return func(l *LruCache[K, V]) { l.onEvict = cb } } // WithUpdateAgeOnGet update expires when Get element func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] { return func(l *LruCache[K, V]) { l.updateAgeOnGet = true } } // WithAge defined element max age (second) func WithAge[K comparable, V any](maxAge int64) Option[K, V] { return func(l *LruCache[K, V]) { l.maxAge = maxAge } } // WithSize defined max length of LruCache func WithSize[K comparable, V any](maxSize int) Option[K, V] { return func(l *LruCache[K, V]) { l.maxSize = maxSize } } // WithStale decide whether Stale return is enabled. // If this feature is enabled, element will not get Evicted according to `WithAge`. func WithStale[K comparable, V any](stale bool) Option[K, V] { return func(l *LruCache[K, V]) { l.staleReturn = stale } } // LruCache is a thread-safe, in-memory lru-cache that evicts the // least recently used entries from memory when (if set) the entries are // older than maxAge (in seconds). Use the New constructor to create one. type LruCache[K comparable, V any] struct { maxAge int64 maxSize int mu sync.Mutex cache map[K]*list.Element[*entry[K, V]] lru *list.List[*entry[K, V]] // Front is least-recent updateAgeOnGet bool staleReturn bool onEvict EvictCallback[K, V] } // New creates an LruCache func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { lc := &LruCache[K, V]{} lc.Clear() for _, option := range options { option(lc) } return lc } func (c *LruCache[K, V]) Clear() { c.mu.Lock() defer c.mu.Unlock() c.lru = list.New[*entry[K, V]]() c.cache = make(map[K]*list.Element[*entry[K, V]]) } // Get returns any representation of a cached response and a bool // set to true if the key was found. func (c *LruCache[K, V]) Get(key K) (V, bool) { c.mu.Lock() defer c.mu.Unlock() el := c.get(key) if el == nil { return lo.Empty[V](), false } value := el.value return value, true } func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) { c.mu.Lock() defer c.mu.Unlock() el := c.get(key) if el == nil { value := constructor() c.set(key, value) return value, false } value := el.value return value, true } // GetWithExpire returns any representation of a cached response, // a time.Time Give expected expires, // and a bool set to true if the key was found. // This method will NOT check the maxAge of element and will NOT update the expires. func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) { c.mu.Lock() defer c.mu.Unlock() el := c.get(key) if el == nil { return lo.Empty[V](), time.Time{}, false } return el.value, time.Unix(el.expires, 0), true } // Exist returns if key exist in cache but not put item to the head of linked list func (c *LruCache[K, V]) Exist(key K) bool { c.mu.Lock() defer c.mu.Unlock() _, ok := c.cache[key] return ok } // Set stores any representation of a response for a given key. func (c *LruCache[K, V]) Set(key K, value V) { c.mu.Lock() defer c.mu.Unlock() c.set(key, value) } func (c *LruCache[K, V]) set(key K, value V) { expires := int64(0) if c.maxAge > 0 { expires = time.Now().Unix() + c.maxAge } c.setWithExpire(key, value, time.Unix(expires, 0)) } // SetWithExpire stores any representation of a response for a given key and given expires. // The expires time will round to second. func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) { c.mu.Lock() defer c.mu.Unlock() c.setWithExpire(key, value, expires) } func (c *LruCache[K, V]) setWithExpire(key K, value V, expires time.Time) { if le, ok := c.cache[key]; ok { c.lru.MoveToBack(le) e := le.Value e.value = value e.expires = expires.Unix() } else { e := &entry[K, V]{key: key, value: value, expires: expires.Unix()} c.cache[key] = c.lru.PushBack(e) if c.maxSize > 0 { if elLen := c.lru.Len(); elLen > c.maxSize { c.deleteElement(c.lru.Front()) } } } c.maybeDeleteOldest() } // CloneTo clone and overwrite elements to another LruCache func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) { c.mu.Lock() defer c.mu.Unlock() n.mu.Lock() defer n.mu.Unlock() n.lru = list.New[*entry[K, V]]() n.cache = make(map[K]*list.Element[*entry[K, V]]) for e := c.lru.Front(); e != nil; e = e.Next() { elm := e.Value n.cache[elm.key] = n.lru.PushBack(elm) } } func (c *LruCache[K, V]) get(key K) *entry[K, V] { le, ok := c.cache[key] if !ok { return nil } if !c.staleReturn && c.maxAge > 0 && le.Value.expires <= time.Now().Unix() { c.deleteElement(le) c.maybeDeleteOldest() return nil } c.lru.MoveToBack(le) el := le.Value if c.maxAge > 0 && c.updateAgeOnGet { el.expires = time.Now().Unix() + c.maxAge } return el } // Delete removes the value associated with a key. func (c *LruCache[K, V]) Delete(key K) { c.mu.Lock() defer c.mu.Unlock() c.delete(key) } func (c *LruCache[K, V]) delete(key K) { if le, ok := c.cache[key]; ok { c.deleteElement(le) } } func (c *LruCache[K, V]) maybeDeleteOldest() { if !c.staleReturn && c.maxAge > 0 { now := time.Now().Unix() for le := c.lru.Front(); le != nil && le.Value.expires <= now; le = c.lru.Front() { c.deleteElement(le) } } } func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) { c.lru.Remove(le) e := le.Value delete(c.cache, e.key) if c.onEvict != nil { c.onEvict(e.key, e.value) } } // Compute either sets the computed new value for the key or deletes // the value for the key. When the delete result of the valueFn function // is set to true, the value will be deleted, if it exists. When delete // is set to false, the value is updated to the newValue. // The ok result indicates whether value was computed and stored, thus, is // present in the map. The actual result contains the new value in cases where // the value was computed and stored. func (c *LruCache[K, V]) Compute( key K, valueFn func(oldValue V, loaded bool) (newValue V, delete bool), ) (actual V, ok bool) { c.mu.Lock() defer c.mu.Unlock() if el := c.get(key); el != nil { actual, ok = el.value, true } if newValue, del := valueFn(actual, ok); del { if ok { // data not in cache, so needn't delete c.delete(key) } return lo.Empty[V](), false } else { c.set(key, newValue) return newValue, true } } type entry[K comparable, V any] struct { key K value V expires int64 } ================================================ FILE: core/Clash.Meta/common/lru/lrucache_test.go ================================================ package lru import ( "testing" "time" "github.com/stretchr/testify/assert" ) var entries = []struct { key string value string }{ {"1", "one"}, {"2", "two"}, {"3", "three"}, {"4", "four"}, {"5", "five"}, } func TestLRUCache(t *testing.T) { c := New[string, string]() for _, e := range entries { c.Set(e.key, e.value) } c.Delete("missing") _, ok := c.Get("missing") assert.False(t, ok) for _, e := range entries { value, ok := c.Get(e.key) if assert.True(t, ok) { assert.Equal(t, e.value, value) } } for _, e := range entries { c.Delete(e.key) _, ok := c.Get(e.key) assert.False(t, ok) } } func TestLRUMaxAge(t *testing.T) { c := New[string, string](WithAge[string, string](86400)) now := time.Now().Unix() expected := now + 86400 // Add one expired entry c.Set("foo", "bar") c.lru.Back().Value.expires = now // Reset c.Set("foo", "bar") e := c.lru.Back().Value assert.True(t, e.expires >= now) c.lru.Back().Value.expires = now // Set a few and verify expiration times for _, s := range entries { c.Set(s.key, s.value) e := c.lru.Back().Value assert.True(t, e.expires >= expected && e.expires <= expected+10) } // Make sure we can get them all for _, s := range entries { _, ok := c.Get(s.key) assert.True(t, ok) } // Expire all entries for _, s := range entries { le, ok := c.cache[s.key] if assert.True(t, ok) { le.Value.expires = now } } // Get one expired entry, which should clear all expired entries _, ok := c.Get("3") assert.False(t, ok) assert.Equal(t, c.lru.Len(), 0) } func TestLRUpdateOnGet(t *testing.T) { c := New[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]()) now := time.Now().Unix() expires := now + 86400/2 // Add one expired entry c.Set("foo", "bar") c.lru.Back().Value.expires = expires _, ok := c.Get("foo") assert.True(t, ok) assert.True(t, c.lru.Back().Value.expires > expires) } func TestMaxSize(t *testing.T) { c := New[string, string](WithSize[string, string](2)) // Add one expired entry c.Set("foo", "bar") _, ok := c.Get("foo") assert.True(t, ok) c.Set("bar", "foo") c.Set("baz", "foo") _, ok = c.Get("foo") assert.False(t, ok) } func TestExist(t *testing.T) { c := New[int, int](WithSize[int, int](1)) c.Set(1, 2) assert.True(t, c.Exist(1)) c.Set(2, 3) assert.False(t, c.Exist(1)) } func TestEvict(t *testing.T) { temp := 0 evict := func(key int, value int) { temp = key + value } c := New[int, int](WithEvict[int, int](evict), WithSize[int, int](1)) c.Set(1, 2) c.Set(2, 3) assert.Equal(t, temp, 3) } func TestSetWithExpire(t *testing.T) { c := New[int, *struct{}](WithAge[int, *struct{}](1)) now := time.Now().Unix() tenSecBefore := time.Unix(now-10, 0) c.SetWithExpire(1, &struct{}{}, tenSecBefore) // res is expected not to exist, and expires should be empty time.Time res, expires, exist := c.GetWithExpire(1) assert.True(t, nil == res) assert.Equal(t, time.Time{}, expires) assert.Equal(t, false, exist) } func TestStale(t *testing.T) { c := New[int, int](WithAge[int, int](1), WithStale[int, int](true)) now := time.Now().Unix() tenSecBefore := time.Unix(now-10, 0) c.SetWithExpire(1, 2, tenSecBefore) res, expires, exist := c.GetWithExpire(1) assert.Equal(t, 2, res) assert.Equal(t, tenSecBefore, expires) assert.Equal(t, true, exist) } func TestCloneTo(t *testing.T) { o := New[string, int](WithSize[string, int](10)) o.Set("1", 1) o.Set("2", 2) n := New[string, int](WithSize[string, int](2)) n.Set("3", 3) n.Set("4", 4) o.CloneTo(n) assert.False(t, n.Exist("3")) assert.True(t, n.Exist("1")) n.Set("5", 5) assert.False(t, n.Exist("1")) } ================================================ FILE: core/Clash.Meta/common/maphash/common.go ================================================ package maphash import "hash/maphash" type Seed = maphash.Seed func MakeSeed() Seed { return maphash.MakeSeed() } type Hash = maphash.Hash func Bytes(seed Seed, b []byte) uint64 { return maphash.Bytes(seed, b) } func String(seed Seed, s string) uint64 { return maphash.String(seed, s) } ================================================ FILE: core/Clash.Meta/common/maphash/comparable_go120.go ================================================ //go:build !go1.24 package maphash import "unsafe" func Comparable[T comparable](s Seed, v T) uint64 { return comparableHash(*(*seedTyp)(unsafe.Pointer(&s)), v) } func comparableHash[T comparable](seed seedTyp, v T) uint64 { s := seed.s var m map[T]struct{} mTyp := iTypeOf(m) var hasher func(unsafe.Pointer, uintptr) uintptr hasher = (*iMapType)(unsafe.Pointer(mTyp)).Hasher p := escape(unsafe.Pointer(&v)) if ptrSize == 8 { return uint64(hasher(p, uintptr(s))) } lo := hasher(p, uintptr(s)) hi := hasher(p, uintptr(s>>32)) return uint64(hi)<<32 | uint64(lo) } // WriteComparable adds x to the data hashed by h. func WriteComparable[T comparable](h *Hash, x T) { // writeComparable (not in purego mode) directly operates on h.state // without using h.buf. Mix in the buffer length so it won't // commute with a buffered write, which either changes h.n or changes // h.state. hash := (*hashTyp)(unsafe.Pointer(h)) if hash.n != 0 { hash.state.s = comparableHash(hash.state, hash.n) } hash.state.s = comparableHash(hash.state, x) } // go/src/hash/maphash/maphash.go type hashTyp struct { _ [0]func() // not comparable seed seedTyp // initial seed used for this hash state seedTyp // current hash of all flushed bytes buf [128]byte // unflushed byte buffer n int // number of unflushed bytes } type seedTyp struct { s uint64 } type iTFlag uint8 type iKind uint8 type iNameOff int32 // TypeOff is the offset to a type from moduledata.types. See resolveTypeOff in runtime. type iTypeOff int32 type iType struct { Size_ uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag iTFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ iKind // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // Normally, GCData points to a bitmask that describes the // ptr/nonptr fields of the type. The bitmask will have at // least PtrBytes/ptrSize bits. // If the TFlagGCMaskOnDemand bit is set, GCData is instead a // **byte and the pointer to the bitmask is one dereference away. // The runtime will build the bitmask if needed. // (See runtime/type.go:getGCMask.) // Note: multiple types may have the same value of GCData, // including when TFlagGCMaskOnDemand is set. The types will, of course, // have the same pointer layout (but not necessarily the same size). GCData *byte Str iNameOff // string form PtrToThis iTypeOff // type for pointer to this type, may be zero } type iMapType struct { iType Key *iType Elem *iType Group *iType // internal type representing a slot group // function for hashing keys (ptr to key, seed) -> hash Hasher func(unsafe.Pointer, uintptr) uintptr } func iTypeOf(a any) *iType { eface := *(*iEmptyInterface)(unsafe.Pointer(&a)) // Types are either static (for compiler-created types) or // heap-allocated but always reachable (for reflection-created // types, held in the central map). So there is no need to // escape types. noescape here help avoid unnecessary escape // of v. return (*iType)(noescape(unsafe.Pointer(eface.Type))) } type iEmptyInterface struct { Type *iType Data unsafe.Pointer } // noescape hides a pointer from escape analysis. noescape is // the identity function but escape analysis doesn't think the // output depends on the input. noescape is inlined and currently // compiles down to zero instructions. // USE CAREFULLY! // // nolint:all // //go:nosplit //goland:noinspection ALL func noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0) } var alwaysFalse bool var escapeSink any // escape forces any pointers in x to escape to the heap. func escape[T any](x T) T { if alwaysFalse { escapeSink = x } return x } // ptrSize is the size of a pointer in bytes - unsafe.Sizeof(uintptr(0)) but as an ideal constant. // It is also the size of the machine's native word size (that is, 4 on 32-bit systems, 8 on 64-bit). const ptrSize = 4 << (^uintptr(0) >> 63) const testComparableAllocations = false ================================================ FILE: core/Clash.Meta/common/maphash/comparable_go124.go ================================================ //go:build go1.24 package maphash import "hash/maphash" func Comparable[T comparable](seed Seed, v T) uint64 { return maphash.Comparable(seed, v) } func WriteComparable[T comparable](h *Hash, x T) { maphash.WriteComparable(h, x) } const testComparableAllocations = true ================================================ FILE: core/Clash.Meta/common/maphash/maphash_test.go ================================================ // Copyright 2019 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 maphash import ( "bytes" "fmt" "hash" "math" "reflect" "strings" "testing" "unsafe" rand "github.com/metacubex/randv2" ) func TestUnseededHash(t *testing.T) { m := map[uint64]struct{}{} for i := 0; i < 1000; i++ { h := new(Hash) m[h.Sum64()] = struct{}{} } if len(m) < 900 { t.Errorf("empty hash not sufficiently random: got %d, want 1000", len(m)) } } func TestSeededHash(t *testing.T) { s := MakeSeed() m := map[uint64]struct{}{} for i := 0; i < 1000; i++ { h := new(Hash) h.SetSeed(s) m[h.Sum64()] = struct{}{} } if len(m) != 1 { t.Errorf("seeded hash is random: got %d, want 1", len(m)) } } func TestHashGrouping(t *testing.T) { b := bytes.Repeat([]byte("foo"), 100) hh := make([]*Hash, 7) for i := range hh { hh[i] = new(Hash) } for _, h := range hh[1:] { h.SetSeed(hh[0].Seed()) } hh[0].Write(b) hh[1].WriteString(string(b)) writeByte := func(h *Hash, b byte) { err := h.WriteByte(b) if err != nil { t.Fatalf("WriteByte: %v", err) } } writeSingleByte := func(h *Hash, b byte) { _, err := h.Write([]byte{b}) if err != nil { t.Fatalf("Write single byte: %v", err) } } writeStringSingleByte := func(h *Hash, b byte) { _, err := h.WriteString(string([]byte{b})) if err != nil { t.Fatalf("WriteString single byte: %v", err) } } for i, x := range b { writeByte(hh[2], x) writeSingleByte(hh[3], x) if i == 0 { writeByte(hh[4], x) } else { writeSingleByte(hh[4], x) } writeStringSingleByte(hh[5], x) if i == 0 { writeByte(hh[6], x) } else { writeStringSingleByte(hh[6], x) } } sum := hh[0].Sum64() for i, h := range hh { if sum != h.Sum64() { t.Errorf("hash %d not identical to a single Write", i) } } if sum1 := Bytes(hh[0].Seed(), b); sum1 != hh[0].Sum64() { t.Errorf("hash using Bytes not identical to a single Write") } if sum1 := String(hh[0].Seed(), string(b)); sum1 != hh[0].Sum64() { t.Errorf("hash using String not identical to a single Write") } } func TestHashBytesVsString(t *testing.T) { s := "foo" b := []byte(s) h1 := new(Hash) h2 := new(Hash) h2.SetSeed(h1.Seed()) n1, err1 := h1.WriteString(s) if n1 != len(s) || err1 != nil { t.Fatalf("WriteString(s) = %d, %v, want %d, nil", n1, err1, len(s)) } n2, err2 := h2.Write(b) if n2 != len(b) || err2 != nil { t.Fatalf("Write(b) = %d, %v, want %d, nil", n2, err2, len(b)) } if h1.Sum64() != h2.Sum64() { t.Errorf("hash of string and bytes not identical") } } func TestHashHighBytes(t *testing.T) { // See issue 34925. const N = 10 m := map[uint64]struct{}{} for i := 0; i < N; i++ { h := new(Hash) h.WriteString("foo") m[h.Sum64()>>32] = struct{}{} } if len(m) < N/2 { t.Errorf("from %d seeds, wanted at least %d different hashes; got %d", N, N/2, len(m)) } } func TestRepeat(t *testing.T) { h1 := new(Hash) h1.WriteString("testing") sum1 := h1.Sum64() h1.Reset() h1.WriteString("testing") sum2 := h1.Sum64() if sum1 != sum2 { t.Errorf("different sum after resetting: %#x != %#x", sum1, sum2) } h2 := new(Hash) h2.SetSeed(h1.Seed()) h2.WriteString("testing") sum3 := h2.Sum64() if sum1 != sum3 { t.Errorf("different sum on the same seed: %#x != %#x", sum1, sum3) } } func TestSeedFromSum64(t *testing.T) { h1 := new(Hash) h1.WriteString("foo") x := h1.Sum64() // seed generated here h2 := new(Hash) h2.SetSeed(h1.Seed()) h2.WriteString("foo") y := h2.Sum64() if x != y { t.Errorf("hashes don't match: want %x, got %x", x, y) } } func TestSeedFromSeed(t *testing.T) { h1 := new(Hash) h1.WriteString("foo") _ = h1.Seed() // seed generated here x := h1.Sum64() h2 := new(Hash) h2.SetSeed(h1.Seed()) h2.WriteString("foo") y := h2.Sum64() if x != y { t.Errorf("hashes don't match: want %x, got %x", x, y) } } func TestSeedFromFlush(t *testing.T) { b := make([]byte, 65) h1 := new(Hash) h1.Write(b) // seed generated here x := h1.Sum64() h2 := new(Hash) h2.SetSeed(h1.Seed()) h2.Write(b) y := h2.Sum64() if x != y { t.Errorf("hashes don't match: want %x, got %x", x, y) } } func TestSeedFromReset(t *testing.T) { h1 := new(Hash) h1.WriteString("foo") h1.Reset() // seed generated here h1.WriteString("foo") x := h1.Sum64() h2 := new(Hash) h2.SetSeed(h1.Seed()) h2.WriteString("foo") y := h2.Sum64() if x != y { t.Errorf("hashes don't match: want %x, got %x", x, y) } } func negativeZero[T float32 | float64]() T { var f T f = -f return f } func TestComparable(t *testing.T) { testComparable(t, int64(2)) testComparable(t, uint64(8)) testComparable(t, uintptr(12)) testComparable(t, any("s")) testComparable(t, "s") testComparable(t, true) testComparable(t, new(float64)) testComparable(t, float64(9)) testComparable(t, complex128(9i+1)) testComparable(t, struct{}{}) testComparable(t, struct { i int u uint b bool f float64 p *int a any }{i: 9, u: 1, b: true, f: 9.9, p: new(int), a: 1}) type S struct { s string } s1 := S{s: heapStr(t)} s2 := S{s: heapStr(t)} if unsafe.StringData(s1.s) == unsafe.StringData(s2.s) { t.Fatalf("unexpected two heapStr ptr equal") } if s1.s != s2.s { t.Fatalf("unexpected two heapStr value not equal") } testComparable(t, s1, s2) testComparable(t, s1.s, s2.s) testComparable(t, float32(0), negativeZero[float32]()) testComparable(t, float64(0), negativeZero[float64]()) testComparableNoEqual(t, math.NaN(), math.NaN()) testComparableNoEqual(t, [2]string{"a", ""}, [2]string{"", "a"}) testComparableNoEqual(t, struct{ a, b string }{"foo", ""}, struct{ a, b string }{"", "foo"}) testComparableNoEqual(t, struct{ a, b any }{int(0), struct{}{}}, struct{ a, b any }{struct{}{}, int(0)}) } func testComparableNoEqual[T comparable](t *testing.T, v1, v2 T) { seed := MakeSeed() if Comparable(seed, v1) == Comparable(seed, v2) { t.Fatalf("Comparable(seed, %v) == Comparable(seed, %v)", v1, v2) } } var heapStrValue = []byte("aTestString") func heapStr(t *testing.T) string { return string(heapStrValue) } func testComparable[T comparable](t *testing.T, v T, v2 ...T) { t.Run(TypeFor[T]().String(), func(t *testing.T) { var a, b T = v, v if len(v2) != 0 { b = v2[0] } var pa *T = &a seed := MakeSeed() if Comparable(seed, a) != Comparable(seed, b) { t.Fatalf("Comparable(seed, %v) != Comparable(seed, %v)", a, b) } old := Comparable(seed, pa) stackGrow(8192) new := Comparable(seed, pa) if old != new { t.Fatal("Comparable(seed, ptr) != Comparable(seed, ptr)") } }) } var use byte //go:noinline func stackGrow(dep int) { if dep == 0 { return } var local [1024]byte // make sure local is allocated on the stack. local[rand.Uint64()%1024] = byte(rand.Uint64()) use = local[rand.Uint64()%1024] stackGrow(dep - 1) } func TestWriteComparable(t *testing.T) { testWriteComparable(t, int64(2)) testWriteComparable(t, uint64(8)) testWriteComparable(t, uintptr(12)) testWriteComparable(t, any("s")) testWriteComparable(t, "s") testComparable(t, true) testWriteComparable(t, new(float64)) testWriteComparable(t, float64(9)) testWriteComparable(t, complex128(9i+1)) testWriteComparable(t, struct{}{}) testWriteComparable(t, struct { i int u uint b bool f float64 p *int a any }{i: 9, u: 1, b: true, f: 9.9, p: new(int), a: 1}) type S struct { s string } s1 := S{s: heapStr(t)} s2 := S{s: heapStr(t)} if unsafe.StringData(s1.s) == unsafe.StringData(s2.s) { t.Fatalf("unexpected two heapStr ptr equal") } if s1.s != s2.s { t.Fatalf("unexpected two heapStr value not equal") } testWriteComparable(t, s1, s2) testWriteComparable(t, s1.s, s2.s) testWriteComparable(t, float32(0), negativeZero[float32]()) testWriteComparable(t, float64(0), negativeZero[float64]()) testWriteComparableNoEqual(t, math.NaN(), math.NaN()) testWriteComparableNoEqual(t, [2]string{"a", ""}, [2]string{"", "a"}) testWriteComparableNoEqual(t, struct{ a, b string }{"foo", ""}, struct{ a, b string }{"", "foo"}) testWriteComparableNoEqual(t, struct{ a, b any }{int(0), struct{}{}}, struct{ a, b any }{struct{}{}, int(0)}) } func testWriteComparableNoEqual[T comparable](t *testing.T, v1, v2 T) { seed := MakeSeed() h1 := Hash{} h2 := Hash{} *(*Seed)(unsafe.Pointer(&h1)), *(*Seed)(unsafe.Pointer(&h2)) = seed, seed WriteComparable(&h1, v1) WriteComparable(&h2, v2) if h1.Sum64() == h2.Sum64() { t.Fatalf("WriteComparable(seed, %v) == WriteComparable(seed, %v)", v1, v2) } } func testWriteComparable[T comparable](t *testing.T, v T, v2 ...T) { t.Run(TypeFor[T]().String(), func(t *testing.T) { var a, b T = v, v if len(v2) != 0 { b = v2[0] } var pa *T = &a h1 := Hash{} h2 := Hash{} *(*Seed)(unsafe.Pointer(&h1)) = MakeSeed() h2 = h1 WriteComparable(&h1, a) WriteComparable(&h2, b) if h1.Sum64() != h2.Sum64() { t.Fatalf("WriteComparable(h, %v) != WriteComparable(h, %v)", a, b) } WriteComparable(&h1, pa) old := h1.Sum64() stackGrow(8192) WriteComparable(&h2, pa) new := h2.Sum64() if old != new { t.Fatal("WriteComparable(seed, ptr) != WriteComparable(seed, ptr)") } }) } func TestComparableShouldPanic(t *testing.T) { s := []byte("s") a := any(s) defer func() { e := recover() err, ok := e.(error) if !ok { t.Fatalf("Comaparable(any([]byte)) should panic") } want := "hash of unhashable type []uint8" if s := err.Error(); !strings.Contains(s, want) { t.Fatalf("want %s, got %s", want, s) } }() Comparable(MakeSeed(), a) } func TestWriteComparableNoncommute(t *testing.T) { seed := MakeSeed() var h1, h2 Hash h1.SetSeed(seed) h2.SetSeed(seed) h1.WriteString("abc") WriteComparable(&h1, 123) WriteComparable(&h2, 123) h2.WriteString("abc") if h1.Sum64() == h2.Sum64() { t.Errorf("WriteComparable and WriteString unexpectedly commute") } } func TestComparableAllocations(t *testing.T) { if !testComparableAllocations { t.Skip("test broken in old golang version") } seed := MakeSeed() x := heapStr(t) allocs := testing.AllocsPerRun(10, func() { s := "s" + x Comparable(seed, s) }) if allocs > 0 { t.Errorf("got %v allocs, want 0", allocs) } type S struct { a int b string } allocs = testing.AllocsPerRun(10, func() { s := S{123, "s" + x} Comparable(seed, s) }) if allocs > 0 { t.Errorf("got %v allocs, want 0", allocs) } } // Make sure a Hash implements the hash.Hash and hash.Hash64 interfaces. var _ hash.Hash = &Hash{} var _ hash.Hash64 = &Hash{} func benchmarkSize(b *testing.B, size int) { h := &Hash{} buf := make([]byte, size) s := string(buf) b.Run("Write", func(b *testing.B) { b.SetBytes(int64(size)) for i := 0; i < b.N; i++ { h.Reset() h.Write(buf) h.Sum64() } }) b.Run("Bytes", func(b *testing.B) { b.SetBytes(int64(size)) seed := h.Seed() for i := 0; i < b.N; i++ { Bytes(seed, buf) } }) b.Run("String", func(b *testing.B) { b.SetBytes(int64(size)) seed := h.Seed() for i := 0; i < b.N; i++ { String(seed, s) } }) } func BenchmarkHash(b *testing.B) { sizes := []int{4, 8, 16, 32, 64, 256, 320, 1024, 4096, 16384} for _, size := range sizes { b.Run(fmt.Sprint("n=", size), func(b *testing.B) { benchmarkSize(b, size) }) } } func benchmarkComparable[T comparable](b *testing.B, v T) { b.Run(TypeFor[T]().String(), func(b *testing.B) { seed := MakeSeed() for i := 0; i < b.N; i++ { Comparable(seed, v) } }) } func BenchmarkComparable(b *testing.B) { type testStruct struct { i int u uint b bool f float64 p *int a any } benchmarkComparable(b, int64(2)) benchmarkComparable(b, uint64(8)) benchmarkComparable(b, uintptr(12)) benchmarkComparable(b, any("s")) benchmarkComparable(b, "s") benchmarkComparable(b, true) benchmarkComparable(b, new(float64)) benchmarkComparable(b, float64(9)) benchmarkComparable(b, complex128(9i+1)) benchmarkComparable(b, struct{}{}) benchmarkComparable(b, testStruct{i: 9, u: 1, b: true, f: 9.9, p: new(int), a: 1}) } // TypeFor returns the [Type] that represents the type argument T. func TypeFor[T any]() reflect.Type { var v T if t := reflect.TypeOf(v); t != nil { return t // optimize for T being a non-interface kind } return reflect.TypeOf((*T)(nil)).Elem() // only for an interface kind } ================================================ FILE: core/Clash.Meta/common/murmur3/murmur.go ================================================ package murmur3 type bmixer interface { bmix(p []byte) (tail []byte) Size() (n int) reset() } type digest struct { clen int // Digested input cumulative length. tail []byte // 0 to Size()-1 bytes view of `buf'. buf [16]byte // Expected (but not required) to be Size() large. seed uint32 // Seed for initializing the hash. bmixer } func (d *digest) BlockSize() int { return 1 } func (d *digest) Write(p []byte) (n int, err error) { n = len(p) d.clen += n if len(d.tail) > 0 { // Stick back pending bytes. nfree := d.Size() - len(d.tail) // nfree ∈ [1, d.Size()-1]. if nfree < len(p) { // One full block can be formed. block := append(d.tail, p[:nfree]...) p = p[nfree:] _ = d.bmix(block) // No tail. } else { // Tail's buf is large enough to prevent reallocs. p = append(d.tail, p...) } } d.tail = d.bmix(p) // Keep own copy of the 0 to Size()-1 pending bytes. nn := copy(d.buf[:], d.tail) d.tail = d.buf[:nn] return n, nil } func (d *digest) Reset() { d.clen = 0 d.tail = nil d.bmixer.reset() } ================================================ FILE: core/Clash.Meta/common/murmur3/murmur32.go ================================================ package murmur3 // https://github.com/spaolacci/murmur3/blob/master/murmur32.go import ( "hash" "math/bits" "unsafe" ) // Make sure interfaces are correctly implemented. var ( _ hash.Hash32 = new(digest32) _ bmixer = new(digest32) ) const ( c1_32 uint32 = 0xcc9e2d51 c2_32 uint32 = 0x1b873593 ) // digest32 represents a partial evaluation of a 32 bites hash. type digest32 struct { digest h1 uint32 // Unfinalized running hash. } // New32 returns new 32-bit hasher func New32() hash.Hash32 { return New32WithSeed(0) } // New32WithSeed returns new 32-bit hasher set with explicit seed value func New32WithSeed(seed uint32) hash.Hash32 { d := new(digest32) d.seed = seed d.bmixer = d d.Reset() return d } func (d *digest32) Size() int { return 4 } func (d *digest32) reset() { d.h1 = d.seed } func (d *digest32) Sum(b []byte) []byte { h := d.Sum32() return append(b, byte(h>>24), byte(h>>16), byte(h>>8), byte(h)) } // Digest as many blocks as possible. func (d *digest32) bmix(p []byte) (tail []byte) { h1 := d.h1 nblocks := len(p) / 4 for i := 0; i < nblocks; i++ { k1 := *(*uint32)(unsafe.Pointer(&p[i*4])) k1 *= c1_32 k1 = bits.RotateLeft32(k1, 15) k1 *= c2_32 h1 ^= k1 h1 = bits.RotateLeft32(h1, 13) h1 = h1*4 + h1 + 0xe6546b64 } d.h1 = h1 return p[nblocks*d.Size():] } func (d *digest32) Sum32() (h1 uint32) { h1 = d.h1 var k1 uint32 switch len(d.tail) & 3 { case 3: k1 ^= uint32(d.tail[2]) << 16 fallthrough case 2: k1 ^= uint32(d.tail[1]) << 8 fallthrough case 1: k1 ^= uint32(d.tail[0]) k1 *= c1_32 k1 = bits.RotateLeft32(k1, 15) k1 *= c2_32 h1 ^= k1 } h1 ^= uint32(d.clen) h1 ^= h1 >> 16 h1 *= 0x85ebca6b h1 ^= h1 >> 13 h1 *= 0xc2b2ae35 h1 ^= h1 >> 16 return h1 } func Sum32(data []byte) uint32 { return Sum32WithSeed(data, 0) } func Sum32WithSeed(data []byte, seed uint32) uint32 { h1 := seed nblocks := len(data) / 4 for i := 0; i < nblocks; i++ { k1 := *(*uint32)(unsafe.Pointer(&data[i*4])) k1 *= c1_32 k1 = bits.RotateLeft32(k1, 15) k1 *= c2_32 h1 ^= k1 h1 = bits.RotateLeft32(h1, 13) h1 = h1*4 + h1 + 0xe6546b64 } tail := data[nblocks*4:] var k1 uint32 switch len(tail) & 3 { case 3: k1 ^= uint32(tail[2]) << 16 fallthrough case 2: k1 ^= uint32(tail[1]) << 8 fallthrough case 1: k1 ^= uint32(tail[0]) k1 *= c1_32 k1 = bits.RotateLeft32(k1, 15) k1 *= c2_32 h1 ^= k1 } h1 ^= uint32(len(data)) h1 ^= h1 >> 16 h1 *= 0x85ebca6b h1 ^= h1 >> 13 h1 *= 0xc2b2ae35 h1 ^= h1 >> 16 return h1 } ================================================ FILE: core/Clash.Meta/common/net/addr.go ================================================ package net import ( "net" ) type CustomAddr interface { net.Addr RawAddr() net.Addr } type customAddr struct { networkStr string addrStr string rawAddr net.Addr } func (a customAddr) Network() string { return a.networkStr } func (a customAddr) String() string { return a.addrStr } func (a customAddr) RawAddr() net.Addr { return a.rawAddr } func NewCustomAddr(networkStr string, addrStr string, rawAddr net.Addr) CustomAddr { return customAddr{ networkStr: networkStr, addrStr: addrStr, rawAddr: rawAddr, } } ================================================ FILE: core/Clash.Meta/common/net/bind.go ================================================ package net import "net" type bindPacketConn struct { EnhancePacketConn rAddr net.Addr } func (c *bindPacketConn) Read(b []byte) (n int, err error) { n, _, err = c.EnhancePacketConn.ReadFrom(b) return n, err } func (c *bindPacketConn) WaitRead() (data []byte, put func(), err error) { data, put, _, err = c.EnhancePacketConn.WaitReadFrom() return } func (c *bindPacketConn) Write(b []byte) (n int, err error) { return c.EnhancePacketConn.WriteTo(b, c.rAddr) } func (c *bindPacketConn) RemoteAddr() net.Addr { return c.rAddr } func (c *bindPacketConn) LocalAddr() net.Addr { if c.EnhancePacketConn.LocalAddr() == nil { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} } else { return c.EnhancePacketConn.LocalAddr() } } func (c *bindPacketConn) Upstream() any { return c.EnhancePacketConn } func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn { return &bindPacketConn{ EnhancePacketConn: NewEnhancePacketConn(pc), rAddr: rAddr, } } ================================================ FILE: core/Clash.Meta/common/net/bufconn.go ================================================ package net import ( "bufio" "net" "github.com/metacubex/mihomo/common/buf" ) var _ ExtendedConn = (*BufferedConn)(nil) type BufferedConn struct { r *bufio.Reader ExtendedConn peeked bool } func NewBufferedConn(c net.Conn) *BufferedConn { if bc, ok := c.(*BufferedConn); ok { return bc } return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false} } func WarpConnWithBioReader(c net.Conn, br *bufio.Reader) net.Conn { if br != nil && br.Buffered() > 0 { if bc, ok := c.(*BufferedConn); ok && bc.r == br { return bc } return &BufferedConn{br, NewExtendedConn(c), true} } return c } // Reader returns the internal bufio.Reader. func (c *BufferedConn) Reader() *bufio.Reader { return c.r } func (c *BufferedConn) ResetPeeked() { c.peeked = false } func (c *BufferedConn) Peeked() bool { return c.peeked } // Peek returns the next n bytes without advancing the reader. func (c *BufferedConn) Peek(n int) ([]byte, error) { c.peeked = true return c.r.Peek(n) } func (c *BufferedConn) Discard(n int) (discarded int, err error) { return c.r.Discard(n) } func (c *BufferedConn) Read(p []byte) (int, error) { return c.r.Read(p) } func (c *BufferedConn) ReadByte() (byte, error) { return c.r.ReadByte() } func (c *BufferedConn) UnreadByte() error { return c.r.UnreadByte() } func (c *BufferedConn) Buffered() int { return c.r.Buffered() } func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) { if c.r != nil && c.r.Buffered() > 0 { _, err = buffer.ReadOnceFrom(c.r) return } return c.ExtendedConn.ReadBuffer(buffer) } func (c *BufferedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy if c.r != nil && c.r.Buffered() > 0 { length := c.r.Buffered() b, _ := c.r.Peek(length) _, _ = c.r.Discard(length) return buf.As(b) } c.r = nil // drop bufio.Reader to let gc can clean up its internal buf return nil } func (c *BufferedConn) Upstream() any { return c.ExtendedConn } func (c *BufferedConn) ReaderReplaceable() bool { if c.r != nil && c.r.Buffered() > 0 { return false } return true } func (c *BufferedConn) WriterReplaceable() bool { return true } ================================================ FILE: core/Clash.Meta/common/net/bufconn_unsafe.go ================================================ package net import ( "io" "unsafe" ) // bufioReader copy from stdlib bufio/bufio.go // This structure has remained unchanged from go1.5 to go1.21. type bufioReader struct { buf []byte rd io.Reader // reader provided by the client r, w int // buf read and write positions err error lastByte int // last byte read for UnreadByte; -1 means invalid lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid } func (c *BufferedConn) AppendData(buf []byte) (ok bool) { b := (*bufioReader)(unsafe.Pointer(c.r)) pos := len(b.buf) - b.w - len(buf) if pos >= -b.r { // len(b.buf)-(b.w - b.r) >= len(buf) if pos < 0 { // len(b.buf)-b.w < len(buf) // Slide existing data to beginning. copy(b.buf, b.buf[b.r:b.w]) b.w -= b.r b.r = 0 } b.w += copy(b.buf[b.w:], buf) return true } return false } ================================================ FILE: core/Clash.Meta/common/net/cached.go ================================================ package net import ( "net" "github.com/metacubex/mihomo/common/buf" ) var _ ExtendedConn = (*CachedConn)(nil) type CachedConn struct { ExtendedConn data []byte } func NewCachedConn(c net.Conn, data []byte) *CachedConn { return &CachedConn{NewExtendedConn(c), data} } func (c *CachedConn) Read(b []byte) (n int, err error) { if len(c.data) > 0 { n = copy(b, c.data) c.data = c.data[n:] return } return c.ExtendedConn.Read(b) } func (c *CachedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy if len(c.data) > 0 { return buf.As(c.data) } return nil } func (c *CachedConn) Upstream() any { return c.ExtendedConn } func (c *CachedConn) ReaderReplaceable() bool { if len(c.data) > 0 { return false } return true } func (c *CachedConn) WriterReplaceable() bool { return true } ================================================ FILE: core/Clash.Meta/common/net/context.go ================================================ package net import ( "context" "net" "github.com/metacubex/mihomo/common/contextutils" ) // SetupContextForConn is a helper function that starts connection I/O interrupter. // if ctx be canceled before done called, it will close the connection. // should use like this: // // func streamConn(ctx context.Context, conn net.Conn) (_ net.Conn, err error) { // if ctx.Done() != nil { // done := N.SetupContextForConn(ctx, conn) // defer done(&err) // } // conn, err := xxx // return conn, err // } func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) { stopc := make(chan struct{}) stop := contextutils.AfterFunc(ctx, func() { // Close the connection, discarding the error _ = conn.Close() close(stopc) }) return func(inputErr *error) { if !stop() { // The AfterFunc was started, wait for it to complete. <-stopc if ctxErr := ctx.Err(); ctxErr != nil && inputErr != nil { // Return context error to user. *inputErr = ctxErr } } } } ================================================ FILE: core/Clash.Meta/common/net/context_test.go ================================================ package net_test import ( "context" "errors" "net" "testing" "time" N "github.com/metacubex/mihomo/common/net" "github.com/stretchr/testify/assert" ) func testRead(ctx context.Context, conn net.Conn) (err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, conn) defer done(&err) } _, err = conn.Read(make([]byte, 1)) return err } func TestSetupContextForConnWithCancel(t *testing.T) { t.Parallel() c1, c2 := N.Pipe() defer c1.Close() defer c2.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() errc := make(chan error) go func() { errc <- testRead(ctx, c1) }() select { case <-errc: t.Fatal("conn closed before cancel") case <-time.After(100 * time.Millisecond): cancel() } select { case err := <-errc: assert.ErrorIs(t, err, context.Canceled) case <-time.After(100 * time.Millisecond): t.Fatal("conn not be canceled") } } func TestSetupContextForConnWithTimeout1(t *testing.T) { t.Parallel() c1, c2 := N.Pipe() defer c1.Close() defer c2.Close() ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() errc := make(chan error) go func() { errc <- testRead(ctx, c1) }() select { case err := <-errc: if !errors.Is(ctx.Err(), context.DeadlineExceeded) { t.Fatal("conn closed before timeout") } assert.ErrorIs(t, err, context.DeadlineExceeded) case <-time.After(200 * time.Millisecond): t.Fatal("conn not be canceled") } } func TestSetupContextForConnWithTimeout2(t *testing.T) { t.Parallel() c1, c2 := N.Pipe() defer c1.Close() defer c2.Close() ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() errc := make(chan error) go func() { errc <- testRead(ctx, c1) }() select { case <-errc: t.Fatal("conn closed before cancel") case <-time.After(100 * time.Millisecond): c2.Write(make([]byte, 1)) } select { case err := <-errc: assert.Nil(t, ctx.Err()) assert.Nil(t, err) case <-time.After(200 * time.Millisecond): t.Fatal("conn not be canceled") } } ================================================ FILE: core/Clash.Meta/common/net/deadline/conn.go ================================================ package deadline import ( "net" "os" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" "github.com/metacubex/sing/common/network" ) type connReadResult struct { buffer []byte err error } type Conn struct { network.ExtendedConn deadline atomic.TypedValue[time.Time] pipeDeadline PipeDeadline disablePipe atomic.Bool inRead atomic.Bool resultCh chan *connReadResult } func IsConn(conn any) bool { _, ok := conn.(*Conn) return ok } func NewConn(conn net.Conn) *Conn { c := &Conn{ ExtendedConn: bufio.NewExtendedConn(conn), pipeDeadline: MakePipeDeadline(), resultCh: make(chan *connReadResult, 1), } c.resultCh <- nil return c } func (c *Conn) Read(p []byte) (n int, err error) { select { case result := <-c.resultCh: if result != nil { n = copy(p, result.buffer) err = result.err if n >= len(result.buffer) { c.resultCh <- nil // finish cache read } else { result.buffer = result.buffer[n:] c.resultCh <- result // push back for next call } return } else { c.resultCh <- nil break } case <-c.pipeDeadline.Wait(): return 0, os.ErrDeadlineExceeded } if c.disablePipe.Load() { return c.ExtendedConn.Read(p) } else if c.deadline.Load().IsZero() { c.inRead.Store(true) defer c.inRead.Store(false) return c.ExtendedConn.Read(p) } <-c.resultCh go c.pipeRead(len(p)) return c.Read(p) } func (c *Conn) pipeRead(size int) { buffer := make([]byte, size) n, err := c.ExtendedConn.Read(buffer) buffer = buffer[:n] c.resultCh <- &connReadResult{ buffer: buffer, err: err, } } func (c *Conn) ReadBuffer(buffer *buf.Buffer) (err error) { select { case result := <-c.resultCh: if result != nil { n, _ := buffer.Write(result.buffer) err = result.err if n >= len(result.buffer) { c.resultCh <- nil // finish cache read } else { result.buffer = result.buffer[n:] c.resultCh <- result // push back for next call } return } else { c.resultCh <- nil break } case <-c.pipeDeadline.Wait(): return os.ErrDeadlineExceeded } if c.disablePipe.Load() { return c.ExtendedConn.ReadBuffer(buffer) } else if c.deadline.Load().IsZero() { c.inRead.Store(true) defer c.inRead.Store(false) return c.ExtendedConn.ReadBuffer(buffer) } <-c.resultCh go c.pipeRead(buffer.FreeLen()) return c.ReadBuffer(buffer) } func (c *Conn) SetReadDeadline(t time.Time) error { if c.disablePipe.Load() { return c.ExtendedConn.SetReadDeadline(t) } else if c.inRead.Load() { c.disablePipe.Store(true) return c.ExtendedConn.SetReadDeadline(t) } c.deadline.Store(t) c.pipeDeadline.Set(t) return nil } func (c *Conn) ReaderReplaceable() bool { select { case result := <-c.resultCh: c.resultCh <- result if result != nil { return false // cache reading } else { break } default: return false // pipe reading } return c.disablePipe.Load() || c.deadline.Load().IsZero() } func (c *Conn) WriterReplaceable() bool { return true } func (c *Conn) Upstream() any { return c.ExtendedConn } ================================================ FILE: core/Clash.Meta/common/net/deadline/packet.go ================================================ package deadline import ( "net" "os" "runtime" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/net/packet" ) type readResult struct { data []byte addr net.Addr err error } type NetPacketConn struct { net.PacketConn deadline atomic.TypedValue[time.Time] pipeDeadline PipeDeadline disablePipe atomic.Bool inRead atomic.Bool resultCh chan any } func NewNetPacketConn(pc net.PacketConn) net.PacketConn { npc := &NetPacketConn{ PacketConn: pc, pipeDeadline: MakePipeDeadline(), resultCh: make(chan any, 1), } npc.resultCh <- nil if enhancePC, isEnhance := pc.(packet.EnhancePacketConn); isEnhance { epc := &EnhancePacketConn{ NetPacketConn: npc, enhancePacketConn: enhancePacketConn{ netPacketConn: npc, enhancePacketConn: enhancePC, }, } if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC { return &EnhanceSingPacketConn{ EnhancePacketConn: epc, singPacketConn: singPacketConn{ netPacketConn: npc, singPacketConn: singPC, }, } } return epc } if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC { return &SingPacketConn{ NetPacketConn: npc, singPacketConn: singPacketConn{ netPacketConn: npc, singPacketConn: singPC, }, } } return npc } func (c *NetPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { FOR: for { select { case result := <-c.resultCh: if result != nil { if result, ok := result.(*readResult); ok { n = copy(p, result.data) addr = result.addr err = result.err c.resultCh <- nil // finish cache read return } c.resultCh <- result // another type of read runtime.Gosched() // allowing other goroutines to run continue FOR } else { c.resultCh <- nil break FOR } case <-c.pipeDeadline.Wait(): return 0, nil, os.ErrDeadlineExceeded } } if c.disablePipe.Load() { return c.PacketConn.ReadFrom(p) } else if c.deadline.Load().IsZero() { c.inRead.Store(true) defer c.inRead.Store(false) n, addr, err = c.PacketConn.ReadFrom(p) return } <-c.resultCh go c.pipeReadFrom(len(p)) return c.ReadFrom(p) } func (c *NetPacketConn) pipeReadFrom(size int) { buffer := make([]byte, size) n, addr, err := c.PacketConn.ReadFrom(buffer) buffer = buffer[:n] result := &readResult{} result.data = buffer result.addr = addr result.err = err c.resultCh <- result } func (c *NetPacketConn) SetReadDeadline(t time.Time) error { if c.disablePipe.Load() { return c.PacketConn.SetReadDeadline(t) } else if c.inRead.Load() { c.disablePipe.Store(true) return c.PacketConn.SetReadDeadline(t) } c.deadline.Store(t) c.pipeDeadline.Set(t) return nil } func (c *NetPacketConn) ReaderReplaceable() bool { select { case result := <-c.resultCh: c.resultCh <- result if result != nil { return false // cache reading } else { break } default: return false // pipe reading } return c.disablePipe.Load() || c.deadline.Load().IsZero() } func (c *NetPacketConn) WriterReplaceable() bool { return true } func (c *NetPacketConn) Upstream() any { return c.PacketConn } func (c *NetPacketConn) NeedAdditionalReadDeadline() bool { return false } ================================================ FILE: core/Clash.Meta/common/net/deadline/packet_enhance.go ================================================ package deadline import ( "net" "os" "runtime" "github.com/metacubex/mihomo/common/net/packet" ) type EnhancePacketConn struct { *NetPacketConn enhancePacketConn } var _ packet.EnhancePacketConn = (*EnhancePacketConn)(nil) func NewEnhancePacketConn(pc packet.EnhancePacketConn) packet.EnhancePacketConn { return NewNetPacketConn(pc).(packet.EnhancePacketConn) } type enhanceReadResult struct { data []byte put func() addr net.Addr err error } type enhancePacketConn struct { netPacketConn *NetPacketConn enhancePacketConn packet.EnhancePacketConn } func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { FOR: for { select { case result := <-c.netPacketConn.resultCh: if result != nil { if result, ok := result.(*enhanceReadResult); ok { data = result.data put = result.put addr = result.addr err = result.err c.netPacketConn.resultCh <- nil // finish cache read return } c.netPacketConn.resultCh <- result // another type of read runtime.Gosched() // allowing other goroutines to run continue FOR } else { c.netPacketConn.resultCh <- nil break FOR } case <-c.netPacketConn.pipeDeadline.Wait(): return nil, nil, nil, os.ErrDeadlineExceeded } } if c.netPacketConn.disablePipe.Load() { return c.enhancePacketConn.WaitReadFrom() } else if c.netPacketConn.deadline.Load().IsZero() { c.netPacketConn.inRead.Store(true) defer c.netPacketConn.inRead.Store(false) data, put, addr, err = c.enhancePacketConn.WaitReadFrom() return } <-c.netPacketConn.resultCh go c.pipeWaitReadFrom() return c.WaitReadFrom() } func (c *enhancePacketConn) pipeWaitReadFrom() { data, put, addr, err := c.enhancePacketConn.WaitReadFrom() result := &enhanceReadResult{} result.data = data result.put = put result.addr = addr result.err = err c.netPacketConn.resultCh <- result } ================================================ FILE: core/Clash.Meta/common/net/deadline/packet_sing.go ================================================ package deadline import ( "os" "runtime" "github.com/metacubex/mihomo/common/net/packet" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) type SingPacketConn struct { *NetPacketConn singPacketConn } var _ packet.SingPacketConn = (*SingPacketConn)(nil) func NewSingPacketConn(pc packet.SingPacketConn) packet.SingPacketConn { return NewNetPacketConn(pc).(packet.SingPacketConn) } type EnhanceSingPacketConn struct { *EnhancePacketConn singPacketConn } func NewEnhanceSingPacketConn(pc packet.EnhanceSingPacketConn) packet.EnhanceSingPacketConn { return NewNetPacketConn(pc).(packet.EnhanceSingPacketConn) } var _ packet.EnhanceSingPacketConn = (*EnhanceSingPacketConn)(nil) type singReadResult struct { buffer *buf.Buffer destination M.Socksaddr err error } type singPacketConn struct { netPacketConn *NetPacketConn singPacketConn packet.SingPacketConn } func (c *singPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { FOR: for { select { case result := <-c.netPacketConn.resultCh: if result != nil { if result, ok := result.(*singReadResult); ok { destination = result.destination err = result.err n, _ := buffer.Write(result.buffer.Bytes()) result.buffer.Advance(n) if result.buffer.IsEmpty() { result.buffer.Release() } c.netPacketConn.resultCh <- nil // finish cache read return } c.netPacketConn.resultCh <- result // another type of read runtime.Gosched() // allowing other goroutines to run continue FOR } else { c.netPacketConn.resultCh <- nil break FOR } case <-c.netPacketConn.pipeDeadline.Wait(): return M.Socksaddr{}, os.ErrDeadlineExceeded } } if c.netPacketConn.disablePipe.Load() { return c.singPacketConn.ReadPacket(buffer) } else if c.netPacketConn.deadline.Load().IsZero() { c.netPacketConn.inRead.Store(true) defer c.netPacketConn.inRead.Store(false) destination, err = c.singPacketConn.ReadPacket(buffer) return } <-c.netPacketConn.resultCh go c.pipeReadPacket(buffer.FreeLen()) return c.ReadPacket(buffer) } func (c *singPacketConn) pipeReadPacket(pLen int) { buffer := buf.NewSize(pLen) destination, err := c.singPacketConn.ReadPacket(buffer) result := &singReadResult{} result.destination = destination result.err = err c.netPacketConn.resultCh <- result } func (c *singPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { return c.singPacketConn.WritePacket(buffer, destination) } func (c *singPacketConn) CreateReadWaiter() (N.PacketReadWaiter, bool) { prw, isReadWaiter := bufio.CreatePacketReadWaiter(c.singPacketConn) if isReadWaiter { return &singPacketReadWaiter{ netPacketConn: c.netPacketConn, packetReadWaiter: prw, }, true } return nil, false } var _ N.PacketReadWaiter = (*singPacketReadWaiter)(nil) type singPacketReadWaiter struct { netPacketConn *NetPacketConn packetReadWaiter N.PacketReadWaiter } type singWaitReadResult singReadResult func (c *singPacketReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { return c.packetReadWaiter.InitializeReadWaiter(options) } func (c *singPacketReadWaiter) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { FOR: for { select { case result := <-c.netPacketConn.resultCh: if result != nil { if result, ok := result.(*singWaitReadResult); ok { buffer = result.buffer destination = result.destination err = result.err c.netPacketConn.resultCh <- nil // finish cache read return } c.netPacketConn.resultCh <- result // another type of read runtime.Gosched() // allowing other goroutines to run continue FOR } else { c.netPacketConn.resultCh <- nil break FOR } case <-c.netPacketConn.pipeDeadline.Wait(): return nil, M.Socksaddr{}, os.ErrDeadlineExceeded } } if c.netPacketConn.disablePipe.Load() { return c.packetReadWaiter.WaitReadPacket() } else if c.netPacketConn.deadline.Load().IsZero() { c.netPacketConn.inRead.Store(true) defer c.netPacketConn.inRead.Store(false) return c.packetReadWaiter.WaitReadPacket() } <-c.netPacketConn.resultCh go c.pipeWaitReadPacket() return c.WaitReadPacket() } func (c *singPacketReadWaiter) pipeWaitReadPacket() { buffer, destination, err := c.packetReadWaiter.WaitReadPacket() result := &singWaitReadResult{} result.buffer = buffer result.destination = destination result.err = err c.netPacketConn.resultCh <- result } func (c *singPacketReadWaiter) Upstream() any { return c.packetReadWaiter } ================================================ FILE: core/Clash.Meta/common/net/deadline/pipe.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. package deadline import ( "sync" "time" ) // PipeDeadline is an abstraction for handling timeouts. type PipeDeadline struct { mu sync.Mutex // Guards timer and cancel timer *time.Timer cancel chan struct{} // Must be non-nil } func MakePipeDeadline() PipeDeadline { return PipeDeadline{cancel: make(chan struct{})} } // Set sets the point in time when the deadline will time out. // A timeout event is signaled by closing the channel returned by waiter. // Once a timeout has occurred, the deadline can be refreshed by specifying a // t value in the future. // // A zero value for t prevents timeout. func (d *PipeDeadline) Set(t time.Time) { d.mu.Lock() defer d.mu.Unlock() if d.timer != nil && !d.timer.Stop() { <-d.cancel // Wait for the timer callback to finish and close cancel } d.timer = nil // Time is zero, then there is no deadline. closed := isClosedChan(d.cancel) if t.IsZero() { if closed { d.cancel = make(chan struct{}) } return } // Time in the future, setup a timer to cancel in the future. if dur := time.Until(t); dur > 0 { if closed { d.cancel = make(chan struct{}) } d.timer = time.AfterFunc(dur, func() { close(d.cancel) }) return } // Time in the past, so close immediately. if !closed { close(d.cancel) } } // Wait returns a channel that is closed when the deadline is exceeded. func (d *PipeDeadline) Wait() chan struct{} { d.mu.Lock() defer d.mu.Unlock() return d.cancel } func isClosedChan(c <-chan struct{}) bool { select { case <-c: return true default: return false } } func makeFilledChan() chan struct{} { ch := make(chan struct{}, 1) ch <- struct{}{} return ch } ================================================ FILE: core/Clash.Meta/common/net/deadline/pipe_sing.go ================================================ package deadline import ( "io" "net" "os" "sync" "time" "github.com/metacubex/sing/common/buf" N "github.com/metacubex/sing/common/network" ) type pipeAddr struct{} func (pipeAddr) Network() string { return "pipe" } func (pipeAddr) String() string { return "pipe" } type pipe struct { wrMu sync.Mutex // Serialize Write operations // Used by local Read to interact with remote Write. // Successful receive on rdRx is always followed by send on rdTx. rdRx <-chan []byte rdTx chan<- int // Used by local Write to interact with remote Read. // Successful send on wrTx is always followed by receive on wrRx. wrTx chan<- []byte wrRx <-chan int once sync.Once // Protects closing localDone localDone chan struct{} remoteDone <-chan struct{} readDeadline PipeDeadline writeDeadline PipeDeadline readWaitOptions N.ReadWaitOptions } // Pipe creates a synchronous, in-memory, full duplex // network connection; both ends implement the Conn interface. // Reads on one end are matched with writes on the other, // copying data directly between the two; there is no internal // buffering. func Pipe() (net.Conn, net.Conn) { cb1 := make(chan []byte) cb2 := make(chan []byte) cn1 := make(chan int) cn2 := make(chan int) done1 := make(chan struct{}) done2 := make(chan struct{}) p1 := &pipe{ rdRx: cb1, rdTx: cn1, wrTx: cb2, wrRx: cn2, localDone: done1, remoteDone: done2, readDeadline: MakePipeDeadline(), writeDeadline: MakePipeDeadline(), } p2 := &pipe{ rdRx: cb2, rdTx: cn2, wrTx: cb1, wrRx: cn1, localDone: done2, remoteDone: done1, readDeadline: MakePipeDeadline(), writeDeadline: MakePipeDeadline(), } return p1, p2 } func (*pipe) LocalAddr() net.Addr { return pipeAddr{} } func (*pipe) RemoteAddr() net.Addr { return pipeAddr{} } func (p *pipe) Read(b []byte) (int, error) { n, err := p.read(b) if err != nil && err != io.EOF && err != io.ErrClosedPipe { err = &net.OpError{Op: "read", Net: "pipe", Err: err} } return n, err } func (p *pipe) read(b []byte) (n int, err error) { switch { case isClosedChan(p.localDone): return 0, io.ErrClosedPipe case isClosedChan(p.remoteDone): return 0, io.EOF case isClosedChan(p.readDeadline.Wait()): return 0, os.ErrDeadlineExceeded } select { case bw := <-p.rdRx: nr := copy(b, bw) p.rdTx <- nr return nr, nil case <-p.localDone: return 0, io.ErrClosedPipe case <-p.remoteDone: return 0, io.EOF case <-p.readDeadline.Wait(): return 0, os.ErrDeadlineExceeded } } func (p *pipe) Write(b []byte) (int, error) { n, err := p.write(b) if err != nil && err != io.ErrClosedPipe { err = &net.OpError{Op: "write", Net: "pipe", Err: err} } return n, err } func (p *pipe) write(b []byte) (n int, err error) { switch { case isClosedChan(p.localDone): return 0, io.ErrClosedPipe case isClosedChan(p.remoteDone): return 0, io.ErrClosedPipe case isClosedChan(p.writeDeadline.Wait()): return 0, os.ErrDeadlineExceeded } p.wrMu.Lock() // Ensure entirety of b is written together defer p.wrMu.Unlock() for once := true; once || len(b) > 0; once = false { select { case p.wrTx <- b: nw := <-p.wrRx b = b[nw:] n += nw case <-p.localDone: return n, io.ErrClosedPipe case <-p.remoteDone: return n, io.ErrClosedPipe case <-p.writeDeadline.Wait(): return n, os.ErrDeadlineExceeded } } return n, nil } func (p *pipe) SetDeadline(t time.Time) error { if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { return io.ErrClosedPipe } p.readDeadline.Set(t) p.writeDeadline.Set(t) return nil } func (p *pipe) SetReadDeadline(t time.Time) error { if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { return io.ErrClosedPipe } p.readDeadline.Set(t) return nil } func (p *pipe) SetWriteDeadline(t time.Time) error { if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { return io.ErrClosedPipe } p.writeDeadline.Set(t) return nil } func (p *pipe) Close() error { p.once.Do(func() { close(p.localDone) }) return nil } var _ N.ReadWaiter = (*pipe)(nil) func (p *pipe) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { p.readWaitOptions = options return false } func (p *pipe) WaitReadBuffer() (buffer *buf.Buffer, err error) { buffer, err = p.waitReadBuffer() if err != nil && err != io.EOF && err != io.ErrClosedPipe { err = &net.OpError{Op: "read", Net: "pipe", Err: err} } return } func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) { switch { case isClosedChan(p.localDone): return nil, io.ErrClosedPipe case isClosedChan(p.remoteDone): return nil, io.EOF case isClosedChan(p.readDeadline.Wait()): return nil, os.ErrDeadlineExceeded } select { case bw := <-p.rdRx: buffer = p.readWaitOptions.NewBuffer() var nr int nr, err = buffer.Write(bw) if err != nil { buffer.Release() return } p.readWaitOptions.PostReturn(buffer) p.rdTx <- nr return case <-p.localDone: return nil, io.ErrClosedPipe case <-p.remoteDone: return nil, io.EOF case <-p.readDeadline.Wait(): return nil, os.ErrDeadlineExceeded } } func IsPipe(conn any) bool { _, ok := conn.(*pipe) return ok } ================================================ FILE: core/Clash.Meta/common/net/earlyconn.go ================================================ package net import ( "net" "sync" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/once" ) type earlyConn struct { ExtendedConn // only expose standard N.ExtendedConn function to outside resFunc func() error resOnce sync.Once resErr error } func (conn *earlyConn) Response() error { conn.resOnce.Do(func() { conn.resErr = conn.resFunc() }) return conn.resErr } func (conn *earlyConn) Read(b []byte) (n int, err error) { err = conn.Response() if err != nil { return 0, err } return conn.ExtendedConn.Read(b) } func (conn *earlyConn) ReadBuffer(buffer *buf.Buffer) (err error) { err = conn.Response() if err != nil { return err } return conn.ExtendedConn.ReadBuffer(buffer) } func (conn *earlyConn) Upstream() any { return conn.ExtendedConn } func (conn *earlyConn) Success() bool { return once.Done(&conn.resOnce) && conn.resErr == nil } func (conn *earlyConn) ReaderReplaceable() bool { return conn.Success() } func (conn *earlyConn) ReaderPossiblyReplaceable() bool { return !conn.Success() } func (conn *earlyConn) WriterReplaceable() bool { return true } var _ ExtendedConn = (*earlyConn)(nil) func NewEarlyConn(c net.Conn, f func() error) net.Conn { return &earlyConn{ExtendedConn: NewExtendedConn(c), resFunc: f} } ================================================ FILE: core/Clash.Meta/common/net/io.go ================================================ package net import "io" type ReadOnlyReader struct { io.Reader } type WriteOnlyWriter struct { io.Writer } ================================================ FILE: core/Clash.Meta/common/net/listener.go ================================================ package net import ( "context" "net" "sync" ) type handleContextListener struct { net.Listener ctx context.Context cancel context.CancelFunc conns chan net.Conn err error once sync.Once handle func(context.Context, net.Conn) (net.Conn, error) panicLog func(any) } func (l *handleContextListener) init() { go func() { for { c, err := l.Listener.Accept() if err != nil { l.err = err close(l.conns) return } go func() { defer func() { if r := recover(); r != nil { if l.panicLog != nil { l.panicLog(r) } } }() if conn, err := l.handle(l.ctx, c); err == nil { l.conns <- conn } else { // handle failed, close the underlying connection. _ = c.Close() } }() } }() } func (l *handleContextListener) Accept() (net.Conn, error) { l.once.Do(l.init) if c, ok := <-l.conns; ok { return c, nil } return nil, l.err } func (l *handleContextListener) Close() error { l.cancel() l.once.Do(func() { // l.init has not been called yet, so close related resources directly. l.err = net.ErrClosed close(l.conns) }) defer func() { // at here, listener has been closed, so we should close all connections in the channel for c := range l.conns { go func(c net.Conn) { defer func() { if r := recover(); r != nil { if l.panicLog != nil { l.panicLog(r) } } }() _ = c.Close() }(c) } }() return l.Listener.Close() } func NewHandleContextListener(ctx context.Context, l net.Listener, handle func(context.Context, net.Conn) (net.Conn, error), panicLog func(any)) net.Listener { ctx, cancel := context.WithCancel(ctx) return &handleContextListener{ Listener: l, ctx: ctx, cancel: cancel, conns: make(chan net.Conn), handle: handle, panicLog: panicLog, } } ================================================ FILE: core/Clash.Meta/common/net/packet/packet.go ================================================ package packet import ( "net" "github.com/metacubex/mihomo/common/pool" ) type WaitReadFrom interface { WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) } type EnhancePacketConn interface { net.PacketConn WaitReadFrom } func NewEnhancePacketConn(pc net.PacketConn) EnhancePacketConn { if udpConn, isUDPConn := pc.(*net.UDPConn); isUDPConn { return &enhanceUDPConn{UDPConn: udpConn} } if enhancePC, isEnhancePC := pc.(EnhancePacketConn); isEnhancePC { return enhancePC } if singPC, isSingPC := pc.(SingPacketConn); isSingPC { return newEnhanceSingPacketConn(singPC) } return &enhancePacketConn{PacketConn: pc} } type enhancePacketConn struct { net.PacketConn } func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { return waitReadFrom(c.PacketConn) } func (c *enhancePacketConn) Upstream() any { return c.PacketConn } func (c *enhancePacketConn) WriterReplaceable() bool { return true } func (c *enhancePacketConn) ReaderReplaceable() bool { return true } func (c *enhanceUDPConn) Upstream() any { return c.UDPConn } func (c *enhanceUDPConn) WriterReplaceable() bool { return true } func (c *enhanceUDPConn) ReaderReplaceable() bool { return true } func waitReadFrom(pc net.PacketConn) (data []byte, put func(), addr net.Addr, err error) { readBuf := pool.Get(pool.UDPBufferSize) put = func() { _ = pool.Put(readBuf) } var readN int readN, addr, err = pc.ReadFrom(readBuf) if readN > 0 { data = readBuf[:readN] } else { put() put = nil } return } ================================================ FILE: core/Clash.Meta/common/net/packet/packet_posix.go ================================================ //go:build !windows package packet import ( "net" "strconv" "syscall" "github.com/metacubex/mihomo/common/pool" ) type enhanceUDPConn struct { *net.UDPConn rawConn syscall.RawConn } func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { if c.rawConn == nil { c.rawConn, _ = c.UDPConn.SyscallConn() } var readErr error err = c.rawConn.Read(func(fd uintptr) (done bool) { readBuf := pool.Get(pool.UDPBufferSize) put = func() { _ = pool.Put(readBuf) } var readFrom syscall.Sockaddr var readN int readN, _, _, readFrom, readErr = syscall.Recvmsg(int(fd), readBuf, nil, 0) if readN > 0 { data = readBuf[:readN] } else { put() put = nil data = nil } if readErr == syscall.EAGAIN { return false } if readFrom != nil { switch from := readFrom.(type) { case *syscall.SockaddrInet4: ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 4 bytes addr = &net.UDPAddr{IP: ip[:], Port: from.Port} case *syscall.SockaddrInet6: ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes zone := "" if from.ZoneId != 0 { zone = strconv.FormatInt(int64(from.ZoneId), 10) } addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone} } } // udp should not convert readN == 0 to io.EOF //if readN == 0 { // readErr = io.EOF //} return true }) if err != nil { return } if readErr != nil { err = readErr return } return } ================================================ FILE: core/Clash.Meta/common/net/packet/packet_sing.go ================================================ package packet import ( "net" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) type SingPacketConn = N.NetPacketConn type EnhanceSingPacketConn interface { SingPacketConn EnhancePacketConn } type enhanceSingPacketConn struct { SingPacketConn packetReadWaiter N.PacketReadWaiter } func (c *enhanceSingPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { var buff *buf.Buffer var dest M.Socksaddr rwOptions := N.ReadWaitOptions{} if c.packetReadWaiter != nil { c.packetReadWaiter.InitializeReadWaiter(rwOptions) buff, dest, err = c.packetReadWaiter.WaitReadPacket() } else { buff = rwOptions.NewPacketBuffer() dest, err = c.SingPacketConn.ReadPacket(buff) if buff != nil { rwOptions.PostReturn(buff) } } if dest.IsFqdn() { addr = dest } else { addr = dest.UDPAddr() } if err != nil { buff.Release() return } if buff == nil { return } if buff.IsEmpty() { buff.Release() return } data = buff.Bytes() put = buff.Release return } func (c *enhanceSingPacketConn) Upstream() any { return c.SingPacketConn } func (c *enhanceSingPacketConn) WriterReplaceable() bool { return true } func (c *enhanceSingPacketConn) ReaderReplaceable() bool { return true } func newEnhanceSingPacketConn(conn SingPacketConn) *enhanceSingPacketConn { epc := &enhanceSingPacketConn{SingPacketConn: conn} if readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn); isReadWaiter { epc.packetReadWaiter = readWaiter } return epc } ================================================ FILE: core/Clash.Meta/common/net/packet/packet_windows.go ================================================ //go:build windows package packet import ( "net" "strconv" "syscall" "github.com/metacubex/mihomo/common/pool" "golang.org/x/sys/windows" ) type enhanceUDPConn struct { *net.UDPConn rawConn syscall.RawConn } func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { if c.rawConn == nil { c.rawConn, _ = c.UDPConn.SyscallConn() } var readErr error hasData := false err = c.rawConn.Read(func(fd uintptr) (done bool) { if !hasData { hasData = true // golang's internal/poll.FD.RawRead will Use a zero-byte read as a way to get notified when this // socket is readable if we return false. So the `recvfrom` syscall will not block the system thread. return false } readBuf := pool.Get(pool.UDPBufferSize) put = func() { _ = pool.Put(readBuf) } var readFrom windows.Sockaddr var readN int readN, readFrom, readErr = windows.Recvfrom(windows.Handle(fd), readBuf, 0) if readN > 0 { data = readBuf[:readN] } else { put() put = nil data = nil } if readErr == windows.WSAEWOULDBLOCK { return false } if readFrom != nil { switch from := readFrom.(type) { case *windows.SockaddrInet4: ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 4 bytes addr = &net.UDPAddr{IP: ip[:], Port: from.Port} case *windows.SockaddrInet6: ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes zone := "" if from.ZoneId != 0 { zone = strconv.FormatInt(int64(from.ZoneId), 10) } addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone} } } // udp should not convert readN == 0 to io.EOF //if readN == 0 { // readErr = io.EOF //} hasData = false return true }) if err != nil { return } if readErr != nil { err = readErr return } return } ================================================ FILE: core/Clash.Meta/common/net/packet/ref.go ================================================ package packet import ( "net" "runtime" "time" ) type refPacketConn struct { pc EnhancePacketConn ref any } func (c *refPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { defer runtime.KeepAlive(c.ref) return c.pc.WaitReadFrom() } func (c *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { defer runtime.KeepAlive(c.ref) return c.pc.ReadFrom(p) } func (c *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { defer runtime.KeepAlive(c.ref) return c.pc.WriteTo(p, addr) } func (c *refPacketConn) Close() error { defer runtime.KeepAlive(c.ref) return c.pc.Close() } func (c *refPacketConn) LocalAddr() net.Addr { defer runtime.KeepAlive(c.ref) return c.pc.LocalAddr() } func (c *refPacketConn) SetDeadline(t time.Time) error { defer runtime.KeepAlive(c.ref) return c.pc.SetDeadline(t) } func (c *refPacketConn) SetReadDeadline(t time.Time) error { defer runtime.KeepAlive(c.ref) return c.pc.SetReadDeadline(t) } func (c *refPacketConn) SetWriteDeadline(t time.Time) error { defer runtime.KeepAlive(c.ref) return c.pc.SetWriteDeadline(t) } func (c *refPacketConn) Upstream() any { return c.pc } func (c *refPacketConn) ReaderReplaceable() bool { // Relay() will handle reference return true } func (c *refPacketConn) WriterReplaceable() bool { // Relay() will handle reference return true } func NewRefPacketConn(pc net.PacketConn, ref any) EnhancePacketConn { rPC := &refPacketConn{pc: NewEnhancePacketConn(pc), ref: ref} if singPC, isSingPC := pc.(SingPacketConn); isSingPC { return &refSingPacketConn{ refPacketConn: rPC, singPacketConn: singPC, } } return rPC } ================================================ FILE: core/Clash.Meta/common/net/packet/ref_sing.go ================================================ package packet import ( "runtime" "github.com/metacubex/sing/common/buf" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) type refSingPacketConn struct { *refPacketConn singPacketConn SingPacketConn } var _ N.NetPacketConn = (*refSingPacketConn)(nil) func (c *refSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { defer runtime.KeepAlive(c.ref) return c.singPacketConn.WritePacket(buffer, destination) } func (c *refSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { defer runtime.KeepAlive(c.ref) return c.singPacketConn.ReadPacket(buffer) } ================================================ FILE: core/Clash.Meta/common/net/packet/thread.go ================================================ package packet import ( "net" "sync" ) type threadSafePacketConn struct { EnhancePacketConn access sync.Mutex } func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { c.access.Lock() defer c.access.Unlock() return c.EnhancePacketConn.WriteTo(b, addr) } func (c *threadSafePacketConn) Upstream() any { return c.EnhancePacketConn } func (c *threadSafePacketConn) ReaderReplaceable() bool { return true } func NewThreadSafePacketConn(pc net.PacketConn) EnhancePacketConn { tsPC := &threadSafePacketConn{EnhancePacketConn: NewEnhancePacketConn(pc)} if singPC, isSingPC := pc.(SingPacketConn); isSingPC { return &threadSafeSingPacketConn{ threadSafePacketConn: tsPC, singPacketConn: singPC, } } return tsPC } ================================================ FILE: core/Clash.Meta/common/net/packet/thread_sing.go ================================================ package packet import ( "github.com/metacubex/sing/common/buf" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) type threadSafeSingPacketConn struct { *threadSafePacketConn singPacketConn SingPacketConn } var _ N.NetPacketConn = (*threadSafeSingPacketConn)(nil) func (c *threadSafeSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { c.access.Lock() defer c.access.Unlock() return c.singPacketConn.WritePacket(buffer, destination) } func (c *threadSafeSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { return c.singPacketConn.ReadPacket(buffer) } ================================================ FILE: core/Clash.Meta/common/net/packet.go ================================================ package net import ( "github.com/metacubex/mihomo/common/net/deadline" "github.com/metacubex/mihomo/common/net/packet" ) type EnhancePacketConn = packet.EnhancePacketConn type WaitReadFrom = packet.WaitReadFrom var NewEnhancePacketConn = packet.NewEnhancePacketConn var NewThreadSafePacketConn = packet.NewThreadSafePacketConn var NewRefPacketConn = packet.NewRefPacketConn var NewDeadlineNetPacketConn = deadline.NewNetPacketConn var NewDeadlineEnhancePacketConn = deadline.NewEnhancePacketConn var NewDeadlineSingPacketConn = deadline.NewSingPacketConn var NewDeadlineEnhanceSingPacketConn = deadline.NewEnhanceSingPacketConn ================================================ FILE: core/Clash.Meta/common/net/refconn.go ================================================ package net import ( "net" "runtime" "time" "github.com/metacubex/mihomo/common/buf" ) type refConn struct { conn ExtendedConn ref any } func (c *refConn) Read(b []byte) (n int, err error) { defer runtime.KeepAlive(c.ref) return c.conn.Read(b) } func (c *refConn) Write(b []byte) (n int, err error) { defer runtime.KeepAlive(c.ref) return c.conn.Write(b) } func (c *refConn) Close() error { defer runtime.KeepAlive(c.ref) return c.conn.Close() } func (c *refConn) LocalAddr() net.Addr { defer runtime.KeepAlive(c.ref) return c.conn.LocalAddr() } func (c *refConn) RemoteAddr() net.Addr { defer runtime.KeepAlive(c.ref) return c.conn.RemoteAddr() } func (c *refConn) SetDeadline(t time.Time) error { defer runtime.KeepAlive(c.ref) return c.conn.SetDeadline(t) } func (c *refConn) SetReadDeadline(t time.Time) error { defer runtime.KeepAlive(c.ref) return c.conn.SetReadDeadline(t) } func (c *refConn) SetWriteDeadline(t time.Time) error { defer runtime.KeepAlive(c.ref) return c.conn.SetWriteDeadline(t) } func (c *refConn) Upstream() any { return c.conn } func (c *refConn) ReadBuffer(buffer *buf.Buffer) error { defer runtime.KeepAlive(c.ref) return c.conn.ReadBuffer(buffer) } func (c *refConn) WriteBuffer(buffer *buf.Buffer) error { defer runtime.KeepAlive(c.ref) return c.conn.WriteBuffer(buffer) } func (c *refConn) ReaderReplaceable() bool { // Relay() will handle reference return true } func (c *refConn) WriterReplaceable() bool { // Relay() will handle reference return true } var _ ExtendedConn = (*refConn)(nil) func NewRefConn(conn net.Conn, ref any) ExtendedConn { return &refConn{conn: NewExtendedConn(conn), ref: ref} } ================================================ FILE: core/Clash.Meta/common/net/relay.go ================================================ package net //import ( // "io" // "net" // "time" //) // //// Relay copies between left and right bidirectionally. //func Relay(leftConn, rightConn net.Conn) { // ch := make(chan error) // // go func() { // // Wrapping to avoid using *net.TCPConn.(ReadFrom) // // See also https://github.com/metacubex/mihomo/pull/1209 // _, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn}) // leftConn.SetReadDeadline(time.Now()) // ch <- err // }() // // _, _ = io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn}) // rightConn.SetReadDeadline(time.Now()) // <-ch //} ================================================ FILE: core/Clash.Meta/common/net/sing.go ================================================ package net import ( "io" "net" "github.com/metacubex/mihomo/common/net/deadline" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/bufio" "github.com/metacubex/sing/common/network" ) var NewExtendedConn = bufio.NewExtendedConn var NewExtendedWriter = bufio.NewExtendedWriter var NewExtendedReader = bufio.NewExtendedReader type ExtendedConn = network.ExtendedConn type ExtendedWriter = network.ExtendedWriter type ExtendedReader = network.ExtendedReader var WriteBuffer = bufio.WriteBuffer type ReadWaitOptions = network.ReadWaitOptions var NewReadWaitOptions = network.NewReadWaitOptions var CalculateFrontHeadroom = network.CalculateFrontHeadroom var CalculateRearHeadroom = network.CalculateRearHeadroom type ReaderWithUpstream = network.ReaderWithUpstream type WithUpstreamReader = network.WithUpstreamReader type WriterWithUpstream = network.WriterWithUpstream type WithUpstreamWriter = network.WithUpstreamWriter type WithUpstream = common.WithUpstream var UnwrapReader = network.UnwrapReader var UnwrapWriter = network.UnwrapWriter func NewDeadlineConn(conn net.Conn) ExtendedConn { if deadline.IsPipe(conn) || deadline.IsPipe(UnwrapReader(conn)) { return NewExtendedConn(conn) // pipe always have correctly deadline implement } if deadline.IsConn(conn) || deadline.IsConn(UnwrapReader(conn)) { return NewExtendedConn(conn) // was a *deadline.Conn } return deadline.NewConn(conn) } func NeedHandshake(conn any) bool { if earlyConn, isEarlyConn := common.Cast[network.EarlyConn](conn); isEarlyConn && earlyConn.NeedHandshake() { return true } return false } type CountFunc = network.CountFunc var Pipe = deadline.Pipe func closeWrite(writer io.Closer) error { if c, ok := common.Cast[network.WriteCloser](writer); ok { return c.CloseWrite() } return writer.Close() } // Relay copies between left and right bidirectionally. // like [bufio.CopyConn] but remove unneeded [context.Context] handle and the cost of [task.Group] func Relay(leftConn, rightConn net.Conn) { defer func() { _ = leftConn.Close() _ = rightConn.Close() }() ch := make(chan struct{}) go func() { _, err := bufio.Copy(leftConn, rightConn) if err == nil { _ = closeWrite(leftConn) } else { _ = leftConn.Close() } close(ch) }() _, err := bufio.Copy(rightConn, leftConn) if err == nil { _ = closeWrite(rightConn) } else { _ = rightConn.Close() } <-ch } ================================================ FILE: core/Clash.Meta/common/net/tcpip.go ================================================ package net import ( "fmt" "net" "strings" ) func SplitNetworkType(s string) (string, string, error) { var ( shecme string hostPort string ) result := strings.Split(s, "://") if len(result) == 2 { shecme = result[0] hostPort = result[1] } else if len(result) == 1 { hostPort = result[0] } else { return "", "", fmt.Errorf("tcp/udp style error") } if len(shecme) == 0 { shecme = "udp" } if shecme != "tcp" && shecme != "udp" { return "", "", fmt.Errorf("scheme should be tcp:// or udp://") } else { return shecme, hostPort, nil } } func SplitHostPort(s string) (host, port string, hasPort bool, err error) { temp := s hasPort = true if !strings.Contains(s, ":") && !strings.Contains(s, "]:") { temp += ":0" hasPort = false } host, port, err = net.SplitHostPort(temp) return } ================================================ FILE: core/Clash.Meta/common/net/websocket.go ================================================ package net import ( "crypto/sha1" "encoding/base64" "encoding/binary" "math/bits" ) // kanged from https://github.com/nhooyr/websocket/blob/master/frame.go // License: MIT // MaskWebSocket applies the WebSocket masking algorithm to p // with the given key. // See https://tools.ietf.org/html/rfc6455#section-5.3 // // The returned value is the correctly rotated key to // to continue to mask/unmask the message. // // It is optimized for LittleEndian and expects the key // to be in little endian. // // See https://github.com/golang/go/issues/31586 func MaskWebSocket(key uint32, b []byte) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) // At some point in the future we can clean these unrolled loops up. // See https://github.com/golang/go/issues/31586#issuecomment-487436401 // Then we xor until b is less than 128 bytes. for len(b) >= 128 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:16]) binary.LittleEndian.PutUint64(b[8:16], v^key64) v = binary.LittleEndian.Uint64(b[16:24]) binary.LittleEndian.PutUint64(b[16:24], v^key64) v = binary.LittleEndian.Uint64(b[24:32]) binary.LittleEndian.PutUint64(b[24:32], v^key64) v = binary.LittleEndian.Uint64(b[32:40]) binary.LittleEndian.PutUint64(b[32:40], v^key64) v = binary.LittleEndian.Uint64(b[40:48]) binary.LittleEndian.PutUint64(b[40:48], v^key64) v = binary.LittleEndian.Uint64(b[48:56]) binary.LittleEndian.PutUint64(b[48:56], v^key64) v = binary.LittleEndian.Uint64(b[56:64]) binary.LittleEndian.PutUint64(b[56:64], v^key64) v = binary.LittleEndian.Uint64(b[64:72]) binary.LittleEndian.PutUint64(b[64:72], v^key64) v = binary.LittleEndian.Uint64(b[72:80]) binary.LittleEndian.PutUint64(b[72:80], v^key64) v = binary.LittleEndian.Uint64(b[80:88]) binary.LittleEndian.PutUint64(b[80:88], v^key64) v = binary.LittleEndian.Uint64(b[88:96]) binary.LittleEndian.PutUint64(b[88:96], v^key64) v = binary.LittleEndian.Uint64(b[96:104]) binary.LittleEndian.PutUint64(b[96:104], v^key64) v = binary.LittleEndian.Uint64(b[104:112]) binary.LittleEndian.PutUint64(b[104:112], v^key64) v = binary.LittleEndian.Uint64(b[112:120]) binary.LittleEndian.PutUint64(b[112:120], v^key64) v = binary.LittleEndian.Uint64(b[120:128]) binary.LittleEndian.PutUint64(b[120:128], v^key64) b = b[128:] } // Then we xor until b is less than 64 bytes. for len(b) >= 64 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:16]) binary.LittleEndian.PutUint64(b[8:16], v^key64) v = binary.LittleEndian.Uint64(b[16:24]) binary.LittleEndian.PutUint64(b[16:24], v^key64) v = binary.LittleEndian.Uint64(b[24:32]) binary.LittleEndian.PutUint64(b[24:32], v^key64) v = binary.LittleEndian.Uint64(b[32:40]) binary.LittleEndian.PutUint64(b[32:40], v^key64) v = binary.LittleEndian.Uint64(b[40:48]) binary.LittleEndian.PutUint64(b[40:48], v^key64) v = binary.LittleEndian.Uint64(b[48:56]) binary.LittleEndian.PutUint64(b[48:56], v^key64) v = binary.LittleEndian.Uint64(b[56:64]) binary.LittleEndian.PutUint64(b[56:64], v^key64) b = b[64:] } // Then we xor until b is less than 32 bytes. for len(b) >= 32 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:16]) binary.LittleEndian.PutUint64(b[8:16], v^key64) v = binary.LittleEndian.Uint64(b[16:24]) binary.LittleEndian.PutUint64(b[16:24], v^key64) v = binary.LittleEndian.Uint64(b[24:32]) binary.LittleEndian.PutUint64(b[24:32], v^key64) b = b[32:] } // Then we xor until b is less than 16 bytes. for len(b) >= 16 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:16]) binary.LittleEndian.PutUint64(b[8:16], v^key64) b = b[16:] } // Then we xor until b is less than 8 bytes. for len(b) >= 8 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) b = b[8:] } } // Then we xor until b is less than 4 bytes. for len(b) >= 4 { v := binary.LittleEndian.Uint32(b) binary.LittleEndian.PutUint32(b, v^key) b = b[4:] } // xor remaining bytes. for i := range b { b[i] ^= byte(key) key = bits.RotateLeft32(key, -8) } return key } func GetWebSocketSecAccept(secKey string) string { const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" const nonceSize = 24 // base64.StdEncoding.EncodedLen(nonceKeySize) p := make([]byte, nonceSize+len(magic)) copy(p[:nonceSize], secKey) copy(p[nonceSize:], magic) sum := sha1.Sum(p) return base64.StdEncoding.EncodeToString(sum[:]) } ================================================ FILE: core/Clash.Meta/common/observable/iterable.go ================================================ package observable type Iterable[T any] <-chan T ================================================ FILE: core/Clash.Meta/common/observable/observable.go ================================================ package observable import ( "errors" "sync" ) type Observable[T any] struct { iterable Iterable[T] listener map[Subscription[T]]*Subscriber[T] mux sync.Mutex done bool stopCh chan struct{} } func (o *Observable[T]) process() { for item := range o.iterable { o.mux.Lock() for _, sub := range o.listener { sub.Emit(item) } o.mux.Unlock() } o.close() } func (o *Observable[T]) close() { o.mux.Lock() defer o.mux.Unlock() o.done = true for _, sub := range o.listener { sub.Close() } close(o.stopCh) } func (o *Observable[T]) Subscribe() (Subscription[T], error) { o.mux.Lock() defer o.mux.Unlock() if o.done { return nil, errors.New("observable is closed") } subscriber := newSubscriber[T]() o.listener[subscriber.Out()] = subscriber return subscriber.Out(), nil } func (o *Observable[T]) UnSubscribe(sub Subscription[T]) { o.mux.Lock() defer o.mux.Unlock() subscriber, exist := o.listener[sub] if !exist { return } delete(o.listener, sub) subscriber.Close() } func NewObservable[T any](iter Iterable[T]) *Observable[T] { observable := &Observable[T]{ iterable: iter, listener: map[Subscription[T]]*Subscriber[T]{}, stopCh: make(chan struct{}), } go observable.process() return observable } ================================================ FILE: core/Clash.Meta/common/observable/observable_test.go ================================================ package observable import ( "sync" "testing" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/stretchr/testify/assert" ) func iterator[T any](item []T) chan T { ch := make(chan T) go func() { time.Sleep(100 * time.Millisecond) for _, elm := range item { ch <- elm } close(ch) }() return ch } func TestObservable(t *testing.T) { iter := iterator[int]([]int{1, 2, 3, 4, 5}) src := NewObservable[int](iter) data, err := src.Subscribe() assert.Nil(t, err) count := 0 for range data { count++ } assert.Equal(t, count, 5) } func TestObservable_MultiSubscribe(t *testing.T) { iter := iterator[int]([]int{1, 2, 3, 4, 5}) src := NewObservable[int](iter) ch1, _ := src.Subscribe() ch2, _ := src.Subscribe() count := atomic.NewInt32(0) var wg sync.WaitGroup wg.Add(2) waitCh := func(ch <-chan int) { for range ch { count.Add(1) } wg.Done() } go waitCh(ch1) go waitCh(ch2) wg.Wait() assert.Equal(t, int32(10), count.Load()) } func TestObservable_UnSubscribe(t *testing.T) { iter := iterator[int]([]int{1, 2, 3, 4, 5}) src := NewObservable[int](iter) data, err := src.Subscribe() assert.Nil(t, err) src.UnSubscribe(data) _, open := <-data assert.False(t, open) } func TestObservable_SubscribeClosedSource(t *testing.T) { iter := iterator[int]([]int{1}) src := NewObservable[int](iter) data, _ := src.Subscribe() <-data select { case <-src.stopCh: case <-time.After(time.Second): assert.Fail(t, "timeout not stop") } } func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) { sub := Subscription[int](make(chan int)) iter := iterator[int]([]int{1}) src := NewObservable[int](iter) src.UnSubscribe(sub) } func TestObservable_SubscribeGoroutineLeak(t *testing.T) { iter := iterator[int]([]int{1, 2, 3, 4, 5}) src := NewObservable[int](iter) max := 100 var list []Subscription[int] for i := 0; i < max; i++ { ch, _ := src.Subscribe() list = append(list, ch) } var wg sync.WaitGroup wg.Add(max) waitCh := func(ch <-chan int) { for range ch { } wg.Done() } for _, ch := range list { go waitCh(ch) } wg.Wait() for _, sub := range list { _, more := <-sub assert.False(t, more) } _, more := <-list[0] assert.False(t, more) } func Benchmark_Observable_1000(b *testing.B) { ch := make(chan int) o := NewObservable[int](ch) num := 1000 subs := []Subscription[int]{} for i := 0; i < num; i++ { sub, _ := o.Subscribe() subs = append(subs, sub) } wg := sync.WaitGroup{} wg.Add(num) b.ResetTimer() for _, sub := range subs { go func(s Subscription[int]) { for range s { } wg.Done() }(sub) } for i := 0; i < b.N; i++ { ch <- i } close(ch) wg.Wait() } ================================================ FILE: core/Clash.Meta/common/observable/subscriber.go ================================================ package observable import ( "sync" ) type Subscription[T any] <-chan T type Subscriber[T any] struct { buffer chan T once sync.Once } func (s *Subscriber[T]) Emit(item T) { s.buffer <- item } func (s *Subscriber[T]) Out() Subscription[T] { return s.buffer } func (s *Subscriber[T]) Close() { s.once.Do(func() { close(s.buffer) }) } func newSubscriber[T any]() *Subscriber[T] { sub := &Subscriber[T]{ buffer: make(chan T, 200), } return sub } ================================================ FILE: core/Clash.Meta/common/once/once_go120.go ================================================ //go:build !go1.22 package once import ( "sync" "sync/atomic" "unsafe" ) type Once struct { done uint32 m sync.Mutex } func Done(once *sync.Once) bool { // atomic visit sync.Once.done return atomic.LoadUint32((*uint32)(unsafe.Pointer(once))) == 1 } func Reset(once *sync.Once) { o := (*Once)(unsafe.Pointer(once)) o.m.Lock() defer o.m.Unlock() atomic.StoreUint32(&o.done, 0) } ================================================ FILE: core/Clash.Meta/common/once/once_go122.go ================================================ //go:build go1.22 package once import ( "sync" "sync/atomic" "unsafe" ) type Once struct { done atomic.Uint32 m sync.Mutex } func Done(once *sync.Once) bool { // atomic visit sync.Once.done return (*atomic.Uint32)(unsafe.Pointer(once)).Load() == 1 } func Reset(once *sync.Once) { o := (*Once)(unsafe.Pointer(once)) o.m.Lock() defer o.m.Unlock() o.done.Store(0) } ================================================ FILE: core/Clash.Meta/common/once/oncefunc.go ================================================ // Copyright 2022 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 once import "sync" // OnceFunc returns a function that invokes f only once. The returned function // may be called concurrently. // // If f panics, the returned function will panic with the same value on every call. func OnceFunc(f func()) func() { var ( once sync.Once valid bool p any ) // Construct the inner closure just once to reduce costs on the fast path. g := func() { defer func() { p = recover() if !valid { // Re-panic immediately so on the first call the user gets a // complete stack trace into f. panic(p) } }() f() f = nil // Do not keep f alive after invoking it. valid = true // Set only if f does not panic. } return func() { once.Do(g) if !valid { panic(p) } } } // OnceValue returns a function that invokes f only once and returns the value // returned by f. The returned function may be called concurrently. // // If f panics, the returned function will panic with the same value on every call. func OnceValue[T any](f func() T) func() T { var ( once sync.Once valid bool p any result T ) g := func() { defer func() { p = recover() if !valid { panic(p) } }() result = f() f = nil valid = true } return func() T { once.Do(g) if !valid { panic(p) } return result } } // OnceValues returns a function that invokes f only once and returns the values // returned by f. The returned function may be called concurrently. // // If f panics, the returned function will panic with the same value on every call. func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { var ( once sync.Once valid bool p any r1 T1 r2 T2 ) g := func() { defer func() { p = recover() if !valid { panic(p) } }() r1, r2 = f() f = nil valid = true } return func() (T1, T2) { once.Do(g) if !valid { panic(p) } return r1, r2 } } ================================================ FILE: core/Clash.Meta/common/orderedmap/doc.go ================================================ package orderedmap // copy and modified from https://github.com/wk8/go-ordered-map/tree/v2.1.8 // which is licensed under Apache v2. // // mihomo modified: // 1. remove dependence of mailru/easyjson for MarshalJSON // 2. remove dependence of buger/jsonparser for UnmarshalJSON ================================================ FILE: core/Clash.Meta/common/orderedmap/json.go ================================================ package orderedmap import ( "bytes" "encoding" "encoding/json" "errors" "fmt" "reflect" ) var ( _ json.Marshaler = &OrderedMap[int, any]{} _ json.Unmarshaler = &OrderedMap[int, any]{} ) // MarshalJSON implements the json.Marshaler interface. func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen if om == nil || om.list == nil { return []byte("null"), nil } var buf bytes.Buffer buf.WriteByte('{') enc := json.NewEncoder(&buf) for pair, firstIteration := om.Oldest(), true; pair != nil; pair = pair.Next() { if firstIteration { firstIteration = false } else { buf.WriteByte(',') } switch key := any(pair.Key).(type) { case string, encoding.TextMarshaler: if err := enc.Encode(pair.Key); err != nil { return nil, err } case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: buf.WriteByte('"') buf.WriteString(fmt.Sprint(key)) buf.WriteByte('"') default: // this switch takes care of wrapper types around primitive types, such as // type myType string switch keyValue := reflect.ValueOf(key); keyValue.Type().Kind() { case reflect.String: if err := enc.Encode(pair.Key); err != nil { return nil, err } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: buf.WriteByte('"') buf.WriteString(fmt.Sprint(key)) buf.WriteByte('"') default: return nil, fmt.Errorf("unsupported key type: %T", key) } } buf.WriteByte(':') if err := enc.Encode(pair.Value); err != nil { return nil, err } } buf.WriteByte('}') return buf.Bytes(), nil } // UnmarshalJSON implements the json.Unmarshaler interface. func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error { if om.list == nil { om.initialize(0) } d := json.NewDecoder(bytes.NewReader(data)) tok, err := d.Token() if err != nil { return err } if tok != json.Delim('{') { return errors.New("expect JSON object open with '{'") } for d.More() { // key tok, err = d.Token() if err != nil { return err } keyStr, ok := tok.(string) if !ok { return fmt.Errorf("key must be a string, got %T\n", tok) } var key K switch typedKey := any(&key).(type) { case *string: *typedKey = keyStr case encoding.TextUnmarshaler: err = typedKey.UnmarshalText([]byte(keyStr)) case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64: err = json.Unmarshal([]byte(keyStr), typedKey) default: // this switch takes care of wrapper types around primitive types, such as // type myType string switch reflect.TypeOf(key).Kind() { case reflect.String: convertedKeyData := reflect.ValueOf(keyStr).Convert(reflect.TypeOf(key)) reflect.ValueOf(&key).Elem().Set(convertedKeyData) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: err = json.Unmarshal([]byte(keyStr), &key) default: err = fmt.Errorf("unsupported key type: %T", key) } } if err != nil { return err } // value value, _ := om.Get(key) err = d.Decode(&value) if err != nil { return err } om.Set(key, value) } tok, err = d.Token() if err != nil { return err } if tok != json.Delim('}') { return errors.New("expect JSON object close with '}'") } return nil } ================================================ FILE: core/Clash.Meta/common/orderedmap/json_fuzz_test.go ================================================ package orderedmap // Adapted from https://github.com/dvyukov/go-fuzz-corpus/blob/c42c1b2/json/json.go import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func FuzzRoundTripJSON(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { for _, testCase := range []struct { name string constructor func() any // should be a function that asserts that 2 objects of the type returned by constructor are equal equalityAssertion func(*testing.T, any, any) bool }{ { name: "with a string -> string map", constructor: func() any { return &OrderedMap[string, string]{} }, equalityAssertion: assertOrderedMapsEqual[string, string], }, { name: "with a string -> int map", constructor: func() any { return &OrderedMap[string, int]{} }, equalityAssertion: assertOrderedMapsEqual[string, int], }, { name: "with a string -> any map", constructor: func() any { return &OrderedMap[string, any]{} }, equalityAssertion: assertOrderedMapsEqual[string, any], }, { name: "with a struct with map fields", constructor: func() any { return new(testFuzzStruct) }, equalityAssertion: assertTestFuzzStructEqual, }, } { t.Run(testCase.name, func(t *testing.T) { v1 := testCase.constructor() if json.Unmarshal(data, v1) != nil { return } jsonData, err := json.Marshal(v1) require.NoError(t, err) v2 := testCase.constructor() require.NoError(t, json.Unmarshal(jsonData, v2)) if !assert.True(t, testCase.equalityAssertion(t, v1, v2), "failed with input data %q", string(data)) { // look at that what the standard lib does with regular map, to help with debugging var m1 map[string]any require.NoError(t, json.Unmarshal(data, &m1)) mapJsonData, err := json.Marshal(m1) require.NoError(t, err) var m2 map[string]any require.NoError(t, json.Unmarshal(mapJsonData, &m2)) t.Logf("initial data = %s", string(data)) t.Logf("unmarshalled map = %v", m1) t.Logf("re-marshalled from map = %s", string(mapJsonData)) t.Logf("re-marshalled from test obj = %s", string(jsonData)) t.Logf("re-unmarshalled map = %s", m2) } }) } }) } // only works for fairly basic maps, that's why it's just in this file func assertOrderedMapsEqual[K comparable, V any](t *testing.T, v1, v2 any) bool { om1, ok1 := v1.(*OrderedMap[K, V]) om2, ok2 := v2.(*OrderedMap[K, V]) if !assert.True(t, ok1, "v1 not an orderedmap") || !assert.True(t, ok2, "v2 not an orderedmap") { return false } success := assert.Equal(t, om1.Len(), om2.Len(), "om1 and om2 have different lengths: %d vs %d", om1.Len(), om2.Len()) for i, pair1, pair2 := 0, om1.Oldest(), om2.Oldest(); pair1 != nil && pair2 != nil; i, pair1, pair2 = i+1, pair1.Next(), pair2.Next() { success = assert.Equal(t, pair1.Key, pair2.Key, "different keys at position %d: %v vs %v", i, pair1.Key, pair2.Key) && success success = assert.Equal(t, pair1.Value, pair2.Value, "different values at position %d: %v vs %v", i, pair1.Value, pair2.Value) && success } return success } type testFuzzStruct struct { M1 *OrderedMap[int, any] M2 *OrderedMap[int, string] M3 *OrderedMap[string, string] } func assertTestFuzzStructEqual(t *testing.T, v1, v2 any) bool { s1, ok := v1.(*testFuzzStruct) s2, ok := v2.(*testFuzzStruct) if !assert.True(t, ok, "v1 not an testFuzzStruct") || !assert.True(t, ok, "v2 not an testFuzzStruct") { return false } success := assertOrderedMapsEqual[int, any](t, s1.M1, s2.M1) success = assertOrderedMapsEqual[int, string](t, s1.M2, s2.M2) && success success = assertOrderedMapsEqual[string, string](t, s1.M3, s2.M3) && success return success } ================================================ FILE: core/Clash.Meta/common/orderedmap/json_test.go ================================================ package orderedmap import ( "encoding/json" "errors" "fmt" "strconv" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // to test marshalling TextMarshalers and unmarshalling TextUnmarshalers type marshallable int func (m marshallable) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("#%d#", m)), nil } func (m *marshallable) UnmarshalText(text []byte) error { if len(text) < 3 { return errors.New("too short") } if text[0] != '#' || text[len(text)-1] != '#' { return errors.New("missing prefix or suffix") } value, err := strconv.Atoi(string(text[1 : len(text)-1])) if err != nil { return err } *m = marshallable(value) return nil } func TestMarshalJSON(t *testing.T) { t.Run("int key", func(t *testing.T) { om := New[int, any]() om.Set(1, "bar") om.Set(7, "baz") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") om.Set(5, "28") om.Set(6, "100") om.Set(8, "baz") om.Set(8, "baz") om.Set(9, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem.") b, err := json.Marshal(om) assert.NoError(t, err) assert.Equal(t, `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz","9":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem."}`, string(b)) }) t.Run("string key", func(t *testing.T) { om := New[string, any]() om.Set("test", "bar") om.Set("abc", true) b, err := json.Marshal(om) assert.NoError(t, err) assert.Equal(t, `{"test":"bar","abc":true}`, string(b)) }) t.Run("typed string key", func(t *testing.T) { type myString string om := New[myString, any]() om.Set("test", "bar") om.Set("abc", true) b, err := json.Marshal(om) assert.NoError(t, err) assert.Equal(t, `{"test":"bar","abc":true}`, string(b)) }) t.Run("typed int key", func(t *testing.T) { type myInt uint32 om := New[myInt, any]() om.Set(1, "bar") om.Set(7, "baz") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") b, err := json.Marshal(om) assert.NoError(t, err) assert.Equal(t, `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz"}`, string(b)) }) t.Run("TextMarshaller key", func(t *testing.T) { om := New[marshallable, any]() om.Set(marshallable(1), "bar") om.Set(marshallable(28), true) b, err := json.Marshal(om) assert.NoError(t, err) assert.Equal(t, `{"#1#":"bar","#28#":true}`, string(b)) }) t.Run("empty map", func(t *testing.T) { om := New[string, any]() b, err := json.Marshal(om) assert.NoError(t, err) assert.Equal(t, `{}`, string(b)) }) } func TestUnmarshallJSON(t *testing.T) { t.Run("int key", func(t *testing.T) { data := `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz"}` om := New[int, any]() require.NoError(t, json.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []int{1, 7, 2, 3, 4, 5, 6, 8}, []any{"bar", "baz", float64(28), float64(100), "baz", "28", "100", "baz"}) }) t.Run("string key", func(t *testing.T) { data := `{"test":"bar","abc":true}` om := New[string, any]() require.NoError(t, json.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []string{"test", "abc"}, []any{"bar", true}) }) t.Run("typed string key", func(t *testing.T) { data := `{"test":"bar","abc":true}` type myString string om := New[myString, any]() require.NoError(t, json.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []myString{"test", "abc"}, []any{"bar", true}) }) t.Run("typed int key", func(t *testing.T) { data := `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz"}` type myInt uint32 om := New[myInt, any]() require.NoError(t, json.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []myInt{1, 7, 2, 3, 4, 5, 6, 8}, []any{"bar", "baz", float64(28), float64(100), "baz", "28", "100", "baz"}) }) t.Run("TextUnmarshaler key", func(t *testing.T) { data := `{"#1#":"bar","#28#":true}` om := New[marshallable, any]() require.NoError(t, json.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []marshallable{1, 28}, []any{"bar", true}) }) t.Run("when fed with an input that's not an object", func(t *testing.T) { for _, data := range []string{"true", `["foo"]`, "42", `"foo"`} { om := New[int, any]() require.Error(t, json.Unmarshal([]byte(data), &om)) } }) t.Run("empty map", func(t *testing.T) { data := `{}` om := New[int, any]() require.NoError(t, json.Unmarshal([]byte(data), &om)) assertLenEqual(t, om, 0) }) } // const specialCharacters = "\\\\/\"\b\f\n\r\t\x00\uffff\ufffd世界\u007f\u00ff\U0010FFFF" const specialCharacters = "\uffff\ufffd世界\u007f\u00ff\U0010FFFF" func TestJSONSpecialCharacters(t *testing.T) { baselineMap := map[string]any{specialCharacters: specialCharacters} baselineData, err := json.Marshal(baselineMap) require.NoError(t, err) // baseline proves this key is supported by official json library t.Logf("specialCharacters: %#v as []rune:%v", specialCharacters, []rune(specialCharacters)) t.Logf("baseline json data: %s", baselineData) t.Run("marshal special characters", func(t *testing.T) { om := New[string, any]() om.Set(specialCharacters, specialCharacters) b, err := json.Marshal(om) require.NoError(t, err) require.Equal(t, baselineData, b) type myString string om2 := New[myString, myString]() om2.Set(specialCharacters, specialCharacters) b, err = json.Marshal(om2) require.NoError(t, err) require.Equal(t, baselineData, b) }) t.Run("unmarshall special characters", func(t *testing.T) { om := New[string, any]() require.NoError(t, json.Unmarshal(baselineData, &om)) assertOrderedPairsEqual(t, om, []string{specialCharacters}, []any{specialCharacters}) type myString string om2 := New[myString, myString]() require.NoError(t, json.Unmarshal(baselineData, &om2)) assertOrderedPairsEqual(t, om2, []myString{specialCharacters}, []myString{specialCharacters}) }) } // to test structs that have nested map fields type nestedMaps struct { X int `json:"x" yaml:"x"` M *OrderedMap[string, []*OrderedMap[int, *OrderedMap[string, any]]] `json:"m" yaml:"m"` } func TestJSONRoundTrip(t *testing.T) { for _, testCase := range []struct { name string input string targetFactory func() any isPrettyPrinted bool }{ { name: "", input: `{ "x": 28, "m": { "foo": [ { "12": { "i": 12, "b": true, "n": null, "m": { "a": "b", "c": 28 } }, "28": { "a": false, "b": [ 1, 2, 3 ] } }, { "3": { "c": null, "d": 87 }, "4": { "e": true }, "5": { "f": 4, "g": 5, "h": 6 } } ], "bar": [ { "5": { "foo": "bar" } } ] } }`, targetFactory: func() any { return &nestedMaps{} }, isPrettyPrinted: true, }, { name: "with UTF-8 special chars in key", input: `{"�":0}`, targetFactory: func() any { return &OrderedMap[string, int]{} }, }, } { t.Run(testCase.name, func(t *testing.T) { target := testCase.targetFactory() require.NoError(t, json.Unmarshal([]byte(testCase.input), target)) var ( out []byte err error ) if testCase.isPrettyPrinted { out, err = json.MarshalIndent(target, "", " ") } else { out, err = json.Marshal(target) } if assert.NoError(t, err) { assert.Equal(t, strings.TrimSpace(testCase.input), string(out)) } }) } } func BenchmarkMarshalJSON(b *testing.B) { om := New[int, any]() om.Set(1, "bar") om.Set(7, "baz") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") om.Set(5, "28") om.Set(6, "100") om.Set(8, "baz") om.Set(8, "baz") b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = json.Marshal(om) } } ================================================ FILE: core/Clash.Meta/common/orderedmap/orderedmap.go ================================================ // Package orderedmap implements an ordered map, i.e. a map that also keeps track of // the order in which keys were inserted. // // All operations are constant-time. // // Github repo: https://github.com/wk8/go-ordered-map package orderedmap import ( "fmt" list "github.com/bahlo/generic-list-go" ) type Pair[K comparable, V any] struct { Key K Value V element *list.Element[*Pair[K, V]] } type OrderedMap[K comparable, V any] struct { pairs map[K]*Pair[K, V] list *list.List[*Pair[K, V]] } type initConfig[K comparable, V any] struct { capacity int initialData []Pair[K, V] } type InitOption[K comparable, V any] func(config *initConfig[K, V]) // WithCapacity allows giving a capacity hint for the map, akin to the standard make(map[K]V, capacity). func WithCapacity[K comparable, V any](capacity int) InitOption[K, V] { return func(c *initConfig[K, V]) { c.capacity = capacity } } // WithInitialData allows passing in initial data for the map. func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[K, V] { return func(c *initConfig[K, V]) { c.initialData = initialData if c.capacity < len(initialData) { c.capacity = len(initialData) } } } // New creates a new OrderedMap. // options can either be one or several InitOption[K, V], or a single integer, // which is then interpreted as a capacity hint, à la make(map[K]V, capacity). func New[K comparable, V any](options ...any) *OrderedMap[K, V] { //nolint:varnamelen orderedMap := &OrderedMap[K, V]{} var config initConfig[K, V] for _, untypedOption := range options { switch option := untypedOption.(type) { case int: if len(options) != 1 { invalidOption() } config.capacity = option case InitOption[K, V]: option(&config) default: invalidOption() } } orderedMap.initialize(config.capacity) orderedMap.AddPairs(config.initialData...) return orderedMap } const invalidOptionMessage = `when using orderedmap.New[K,V]() with options, either provide one or several InitOption[K, V]; or a single integer which is then interpreted as a capacity hint, à la make(map[K]V, capacity).` //nolint:lll func invalidOption() { panic(invalidOptionMessage) } func (om *OrderedMap[K, V]) initialize(capacity int) { om.pairs = make(map[K]*Pair[K, V], capacity) om.list = list.New[*Pair[K, V]]() } // Get looks for the given key, and returns the value associated with it, // or V's nil value if not found. The boolean it returns says whether the key is present in the map. func (om *OrderedMap[K, V]) Get(key K) (val V, present bool) { if pair, present := om.pairs[key]; present { return pair.Value, true } return } // Load is an alias for Get, mostly to present an API similar to `sync.Map`'s. func (om *OrderedMap[K, V]) Load(key K) (V, bool) { return om.Get(key) } // Value returns the value associated with the given key or the zero value. func (om *OrderedMap[K, V]) Value(key K) (val V) { if pair, present := om.pairs[key]; present { val = pair.Value } return } // GetPair looks for the given key, and returns the pair associated with it, // or nil if not found. The Pair struct can then be used to iterate over the ordered map // from that point, either forward or backward. func (om *OrderedMap[K, V]) GetPair(key K) *Pair[K, V] { return om.pairs[key] } // Set sets the key-value pair, and returns what `Get` would have returned // on that key prior to the call to `Set`. func (om *OrderedMap[K, V]) Set(key K, value V) (val V, present bool) { if pair, present := om.pairs[key]; present { oldValue := pair.Value pair.Value = value return oldValue, true } pair := &Pair[K, V]{ Key: key, Value: value, } pair.element = om.list.PushBack(pair) om.pairs[key] = pair return } // AddPairs allows setting multiple pairs at a time. It's equivalent to calling // Set on each pair sequentially. func (om *OrderedMap[K, V]) AddPairs(pairs ...Pair[K, V]) { for _, pair := range pairs { om.Set(pair.Key, pair.Value) } } // Store is an alias for Set, mostly to present an API similar to `sync.Map`'s. func (om *OrderedMap[K, V]) Store(key K, value V) (V, bool) { return om.Set(key, value) } // Delete removes the key-value pair, and returns what `Get` would have returned // on that key prior to the call to `Delete`. func (om *OrderedMap[K, V]) Delete(key K) (val V, present bool) { if pair, present := om.pairs[key]; present { om.list.Remove(pair.element) delete(om.pairs, key) return pair.Value, true } return } // Len returns the length of the ordered map. func (om *OrderedMap[K, V]) Len() int { if om == nil || om.pairs == nil { return 0 } return len(om.pairs) } // Oldest returns a pointer to the oldest pair. It's meant to be used to iterate on the ordered map's // pairs from the oldest to the newest, e.g.: // for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) } func (om *OrderedMap[K, V]) Oldest() *Pair[K, V] { if om == nil || om.list == nil { return nil } return listElementToPair(om.list.Front()) } // Newest returns a pointer to the newest pair. It's meant to be used to iterate on the ordered map's // pairs from the newest to the oldest, e.g.: // for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) } func (om *OrderedMap[K, V]) Newest() *Pair[K, V] { if om == nil || om.list == nil { return nil } return listElementToPair(om.list.Back()) } // Next returns a pointer to the next pair. func (p *Pair[K, V]) Next() *Pair[K, V] { return listElementToPair(p.element.Next()) } // Prev returns a pointer to the previous pair. func (p *Pair[K, V]) Prev() *Pair[K, V] { return listElementToPair(p.element.Prev()) } func listElementToPair[K comparable, V any](element *list.Element[*Pair[K, V]]) *Pair[K, V] { if element == nil { return nil } return element.Value } // KeyNotFoundError may be returned by functions in this package when they're called with keys that are not present // in the map. type KeyNotFoundError[K comparable] struct { MissingKey K } func (e *KeyNotFoundError[K]) Error() string { return fmt.Sprintf("missing key: %v", e.MissingKey) } // MoveAfter moves the value associated with key to its new position after the one associated with markKey. // Returns an error iff key or markKey are not present in the map. If an error is returned, // it will be a KeyNotFoundError. func (om *OrderedMap[K, V]) MoveAfter(key, markKey K) error { elements, err := om.getElements(key, markKey) if err != nil { return err } om.list.MoveAfter(elements[0], elements[1]) return nil } // MoveBefore moves the value associated with key to its new position before the one associated with markKey. // Returns an error iff key or markKey are not present in the map. If an error is returned, // it will be a KeyNotFoundError. func (om *OrderedMap[K, V]) MoveBefore(key, markKey K) error { elements, err := om.getElements(key, markKey) if err != nil { return err } om.list.MoveBefore(elements[0], elements[1]) return nil } func (om *OrderedMap[K, V]) getElements(keys ...K) ([]*list.Element[*Pair[K, V]], error) { elements := make([]*list.Element[*Pair[K, V]], len(keys)) for i, k := range keys { pair, present := om.pairs[k] if !present { return nil, &KeyNotFoundError[K]{k} } elements[i] = pair.element } return elements, nil } // MoveToBack moves the value associated with key to the back of the ordered map, // i.e. makes it the newest pair in the map. // Returns an error iff key is not present in the map. If an error is returned, // it will be a KeyNotFoundError. func (om *OrderedMap[K, V]) MoveToBack(key K) error { _, err := om.GetAndMoveToBack(key) return err } // MoveToFront moves the value associated with key to the front of the ordered map, // i.e. makes it the oldest pair in the map. // Returns an error iff key is not present in the map. If an error is returned, // it will be a KeyNotFoundError. func (om *OrderedMap[K, V]) MoveToFront(key K) error { _, err := om.GetAndMoveToFront(key) return err } // GetAndMoveToBack combines Get and MoveToBack in the same call. If an error is returned, // it will be a KeyNotFoundError. func (om *OrderedMap[K, V]) GetAndMoveToBack(key K) (val V, err error) { if pair, present := om.pairs[key]; present { val = pair.Value om.list.MoveToBack(pair.element) } else { err = &KeyNotFoundError[K]{key} } return } // GetAndMoveToFront combines Get and MoveToFront in the same call. If an error is returned, // it will be a KeyNotFoundError. func (om *OrderedMap[K, V]) GetAndMoveToFront(key K) (val V, err error) { if pair, present := om.pairs[key]; present { val = pair.Value om.list.MoveToFront(pair.element) } else { err = &KeyNotFoundError[K]{key} } return } ================================================ FILE: core/Clash.Meta/common/orderedmap/orderedmap_test.go ================================================ package orderedmap import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestBasicFeatures(t *testing.T) { n := 100 om := New[int, int]() // set(i, 2 * i) for i := 0; i < n; i++ { assertLenEqual(t, om, i) oldValue, present := om.Set(i, 2*i) assertLenEqual(t, om, i+1) assert.Equal(t, 0, oldValue) assert.False(t, present) } // get what we just set for i := 0; i < n; i++ { value, present := om.Get(i) assert.Equal(t, 2*i, value) assert.Equal(t, value, om.Value(i)) assert.True(t, present) } // get pairs of what we just set for i := 0; i < n; i++ { pair := om.GetPair(i) assert.NotNil(t, pair) assert.Equal(t, 2*i, pair.Value) } // forward iteration i := 0 for pair := om.Oldest(); pair != nil; pair = pair.Next() { assert.Equal(t, i, pair.Key) assert.Equal(t, 2*i, pair.Value) i++ } // backward iteration i = n - 1 for pair := om.Newest(); pair != nil; pair = pair.Prev() { assert.Equal(t, i, pair.Key) assert.Equal(t, 2*i, pair.Value) i-- } // forward iteration starting from known key i = 42 for pair := om.GetPair(i); pair != nil; pair = pair.Next() { assert.Equal(t, i, pair.Key) assert.Equal(t, 2*i, pair.Value) i++ } // double values for pairs with even keys for j := 0; j < n/2; j++ { i = 2 * j oldValue, present := om.Set(i, 4*i) assert.Equal(t, 2*i, oldValue) assert.True(t, present) } // and delete pairs with odd keys for j := 0; j < n/2; j++ { i = 2*j + 1 assertLenEqual(t, om, n-j) value, present := om.Delete(i) assertLenEqual(t, om, n-j-1) assert.Equal(t, 2*i, value) assert.True(t, present) // deleting again shouldn't change anything value, present = om.Delete(i) assertLenEqual(t, om, n-j-1) assert.Equal(t, 0, value) assert.False(t, present) } // get the whole range for j := 0; j < n/2; j++ { i = 2 * j value, present := om.Get(i) assert.Equal(t, 4*i, value) assert.Equal(t, value, om.Value(i)) assert.True(t, present) i = 2*j + 1 value, present = om.Get(i) assert.Equal(t, 0, value) assert.Equal(t, value, om.Value(i)) assert.False(t, present) } // check iterations again i = 0 for pair := om.Oldest(); pair != nil; pair = pair.Next() { assert.Equal(t, i, pair.Key) assert.Equal(t, 4*i, pair.Value) i += 2 } i = 2 * ((n - 1) / 2) for pair := om.Newest(); pair != nil; pair = pair.Prev() { assert.Equal(t, i, pair.Key) assert.Equal(t, 4*i, pair.Value) i -= 2 } } func TestUpdatingDoesntChangePairsOrder(t *testing.T) { om := New[string, any]() om.Set("foo", "bar") om.Set("wk", 28) om.Set("po", 100) om.Set("bar", "baz") oldValue, present := om.Set("po", 102) assert.Equal(t, 100, oldValue) assert.True(t, present) assertOrderedPairsEqual(t, om, []string{"foo", "wk", "po", "bar"}, []any{"bar", 28, 102, "baz"}) } func TestDeletingAndReinsertingChangesPairsOrder(t *testing.T) { om := New[string, any]() om.Set("foo", "bar") om.Set("wk", 28) om.Set("po", 100) om.Set("bar", "baz") // delete a pair oldValue, present := om.Delete("po") assert.Equal(t, 100, oldValue) assert.True(t, present) // re-insert the same pair oldValue, present = om.Set("po", 100) assert.Nil(t, oldValue) assert.False(t, present) assertOrderedPairsEqual(t, om, []string{"foo", "wk", "bar", "po"}, []any{"bar", 28, "baz", 100}) } func TestEmptyMapOperations(t *testing.T) { om := New[string, any]() oldValue, present := om.Get("foo") assert.Nil(t, oldValue) assert.Nil(t, om.Value("foo")) assert.False(t, present) oldValue, present = om.Delete("bar") assert.Nil(t, oldValue) assert.False(t, present) assertLenEqual(t, om, 0) assert.Nil(t, om.Oldest()) assert.Nil(t, om.Newest()) } type dummyTestStruct struct { value string } func TestPackUnpackStructs(t *testing.T) { om := New[string, dummyTestStruct]() om.Set("foo", dummyTestStruct{"foo!"}) om.Set("bar", dummyTestStruct{"bar!"}) value, present := om.Get("foo") assert.True(t, present) assert.Equal(t, value, om.Value("foo")) if assert.NotNil(t, value) { assert.Equal(t, "foo!", value.value) } value, present = om.Set("bar", dummyTestStruct{"baz!"}) assert.True(t, present) if assert.NotNil(t, value) { assert.Equal(t, "bar!", value.value) } value, present = om.Get("bar") assert.Equal(t, value, om.Value("bar")) assert.True(t, present) if assert.NotNil(t, value) { assert.Equal(t, "baz!", value.value) } } // shamelessly stolen from https://github.com/python/cpython/blob/e19a91e45fd54a56e39c2d12e6aaf4757030507f/Lib/test/test_ordered_dict.py#L55-L61 func TestShuffle(t *testing.T) { ranLen := 100 for _, n := range []int{0, 10, 20, 100, 1000, 10000} { t.Run(fmt.Sprintf("shuffle test with %d items", n), func(t *testing.T) { om := New[string, string]() keys := make([]string, n) values := make([]string, n) for i := 0; i < n; i++ { // we prefix with the number to ensure that we don't get any duplicates keys[i] = fmt.Sprintf("%d_%s", i, randomHexString(t, ranLen)) values[i] = randomHexString(t, ranLen) value, present := om.Set(keys[i], values[i]) assert.Equal(t, "", value) assert.False(t, present) } assertOrderedPairsEqual(t, om, keys, values) }) } } func TestMove(t *testing.T) { om := New[int, any]() om.Set(1, "bar") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") om.Set(5, "28") om.Set(6, "100") om.Set(7, "baz") om.Set(8, "baz") err := om.MoveAfter(2, 3) assert.Nil(t, err) assertOrderedPairsEqual(t, om, []int{1, 3, 2, 4, 5, 6, 7, 8}, []any{"bar", 100, 28, "baz", "28", "100", "baz", "baz"}) err = om.MoveBefore(6, 4) assert.Nil(t, err) assertOrderedPairsEqual(t, om, []int{1, 3, 2, 6, 4, 5, 7, 8}, []any{"bar", 100, 28, "100", "baz", "28", "baz", "baz"}) err = om.MoveToBack(3) assert.Nil(t, err) assertOrderedPairsEqual(t, om, []int{1, 2, 6, 4, 5, 7, 8, 3}, []any{"bar", 28, "100", "baz", "28", "baz", "baz", 100}) err = om.MoveToFront(5) assert.Nil(t, err) assertOrderedPairsEqual(t, om, []int{5, 1, 2, 6, 4, 7, 8, 3}, []any{"28", "bar", 28, "100", "baz", "baz", "baz", 100}) err = om.MoveToFront(100) assert.Equal(t, &KeyNotFoundError[int]{100}, err) } func TestGetAndMove(t *testing.T) { om := New[int, any]() om.Set(1, "bar") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") om.Set(5, "28") om.Set(6, "100") om.Set(7, "baz") om.Set(8, "baz") value, err := om.GetAndMoveToBack(3) assert.Nil(t, err) assert.Equal(t, 100, value) assertOrderedPairsEqual(t, om, []int{1, 2, 4, 5, 6, 7, 8, 3}, []any{"bar", 28, "baz", "28", "100", "baz", "baz", 100}) value, err = om.GetAndMoveToFront(5) assert.Nil(t, err) assert.Equal(t, "28", value) assertOrderedPairsEqual(t, om, []int{5, 1, 2, 4, 6, 7, 8, 3}, []any{"28", "bar", 28, "baz", "100", "baz", "baz", 100}) value, err = om.GetAndMoveToBack(100) assert.Equal(t, &KeyNotFoundError[int]{100}, err) } func TestAddPairs(t *testing.T) { om := New[int, any]() om.AddPairs( Pair[int, any]{ Key: 28, Value: "foo", }, Pair[int, any]{ Key: 12, Value: "bar", }, Pair[int, any]{ Key: 28, Value: "baz", }, ) assertOrderedPairsEqual(t, om, []int{28, 12}, []any{"baz", "bar"}) } // sadly, we can't test the "actual" capacity here, see https://github.com/golang/go/issues/52157 func TestNewWithCapacity(t *testing.T) { zero := New[int, string](0) assert.Empty(t, zero.Len()) assert.PanicsWithValue(t, invalidOptionMessage, func() { _ = New[int, string](1, 2) }) assert.PanicsWithValue(t, invalidOptionMessage, func() { _ = New[int, string](1, 2, 3) }) om := New[int, string](-1) om.Set(1337, "quarante-deux") assert.Equal(t, 1, om.Len()) } func TestNewWithOptions(t *testing.T) { t.Run("wih capacity", func(t *testing.T) { om := New[string, any](WithCapacity[string, any](98)) assert.Equal(t, 0, om.Len()) }) t.Run("with initial data", func(t *testing.T) { om := New[string, int](WithInitialData( Pair[string, int]{ Key: "a", Value: 1, }, Pair[string, int]{ Key: "b", Value: 2, }, Pair[string, int]{ Key: "c", Value: 3, }, )) assertOrderedPairsEqual(t, om, []string{"a", "b", "c"}, []int{1, 2, 3}) }) t.Run("with an invalid option type", func(t *testing.T) { assert.PanicsWithValue(t, invalidOptionMessage, func() { _ = New[int, string]("foo") }) }) } func TestNilMap(t *testing.T) { // we want certain behaviors of a nil ordered map to be the same as they are for standard nil maps var om *OrderedMap[int, any] t.Run("len", func(t *testing.T) { assert.Equal(t, 0, om.Len()) }) t.Run("iterating - akin to range", func(t *testing.T) { assert.Nil(t, om.Oldest()) assert.Nil(t, om.Newest()) }) } ================================================ FILE: core/Clash.Meta/common/orderedmap/utils_test.go ================================================ package orderedmap import ( "crypto/rand" "encoding/hex" "fmt" "testing" "github.com/stretchr/testify/assert" ) // assertOrderedPairsEqual asserts that the map contains the given keys and values // from oldest to newest. func assertOrderedPairsEqual[K comparable, V any]( t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V, ) { t.Helper() assertOrderedPairsEqualFromNewest(t, orderedMap, expectedKeys, expectedValues) assertOrderedPairsEqualFromOldest(t, orderedMap, expectedKeys, expectedValues) } func assertOrderedPairsEqualFromNewest[K comparable, V any]( t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V, ) { t.Helper() if assert.Equal(t, len(expectedKeys), len(expectedValues)) && assert.Equal(t, len(expectedKeys), orderedMap.Len()) { i := orderedMap.Len() - 1 for pair := orderedMap.Newest(); pair != nil; pair = pair.Prev() { assert.Equal(t, expectedKeys[i], pair.Key, "from newest index=%d on key", i) assert.Equal(t, expectedValues[i], pair.Value, "from newest index=%d on value", i) i-- } } } func assertOrderedPairsEqualFromOldest[K comparable, V any]( t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V, ) { t.Helper() if assert.Equal(t, len(expectedKeys), len(expectedValues)) && assert.Equal(t, len(expectedKeys), orderedMap.Len()) { i := 0 for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { assert.Equal(t, expectedKeys[i], pair.Key, "from oldest index=%d on key", i) assert.Equal(t, expectedValues[i], pair.Value, "from oldest index=%d on value", i) i++ } } } func assertLenEqual[K comparable, V any](t *testing.T, orderedMap *OrderedMap[K, V], expectedLen int) { t.Helper() assert.Equal(t, expectedLen, orderedMap.Len()) // also check the list length, for good measure assert.Equal(t, expectedLen, orderedMap.list.Len()) } func randomHexString(t *testing.T, length int) string { t.Helper() b := length / 2 //nolint:gomnd randBytes := make([]byte, b) if n, err := rand.Read(randBytes); err != nil || n != b { if err == nil { err = fmt.Errorf("only got %v random bytes, expected %v", n, b) } t.Fatal(err) } return hex.EncodeToString(randBytes) } ================================================ FILE: core/Clash.Meta/common/orderedmap/yaml.go ================================================ package orderedmap import ( "fmt" "gopkg.in/yaml.v3" ) var ( _ yaml.Marshaler = &OrderedMap[int, any]{} _ yaml.Unmarshaler = &OrderedMap[int, any]{} ) // MarshalYAML implements the yaml.Marshaler interface. func (om *OrderedMap[K, V]) MarshalYAML() (interface{}, error) { if om == nil { return []byte("null"), nil } node := yaml.Node{ Kind: yaml.MappingNode, } for pair := om.Oldest(); pair != nil; pair = pair.Next() { key, value := pair.Key, pair.Value keyNode := &yaml.Node{} // serialize key to yaml, then deserialize it back into the node // this is a hack to get the correct tag for the key if err := keyNode.Encode(key); err != nil { return nil, err } valueNode := &yaml.Node{} if err := valueNode.Encode(value); err != nil { return nil, err } node.Content = append(node.Content, keyNode, valueNode) } return &node, nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { return fmt.Errorf("pipeline must contain YAML mapping, has %v", value.Kind) } if om.list == nil { om.initialize(0) } for index := 0; index < len(value.Content); index += 2 { var key K var val V if err := value.Content[index].Decode(&key); err != nil { return err } if err := value.Content[index+1].Decode(&val); err != nil { return err } om.Set(key, val) } return nil } ================================================ FILE: core/Clash.Meta/common/orderedmap/yaml_fuzz_test.go ================================================ package orderedmap // Adapted from https://github.com/dvyukov/go-fuzz-corpus/blob/c42c1b2/json/json.go import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) func FuzzRoundTripYAML(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { for _, testCase := range []struct { name string constructor func() any // should be a function that asserts that 2 objects of the type returned by constructor are equal equalityAssertion func(*testing.T, any, any) bool }{ { name: "with a string -> string map", constructor: func() any { return &OrderedMap[string, string]{} }, equalityAssertion: assertOrderedMapsEqual[string, string], }, { name: "with a string -> int map", constructor: func() any { return &OrderedMap[string, int]{} }, equalityAssertion: assertOrderedMapsEqual[string, int], }, { name: "with a string -> any map", constructor: func() any { return &OrderedMap[string, any]{} }, equalityAssertion: assertOrderedMapsEqual[string, any], }, { name: "with a struct with map fields", constructor: func() any { return new(testFuzzStruct) }, equalityAssertion: assertTestFuzzStructEqual, }, } { t.Run(testCase.name, func(t *testing.T) { v1 := testCase.constructor() if yaml.Unmarshal(data, v1) != nil { return } t.Log(data) t.Log(v1) yamlData, err := yaml.Marshal(v1) require.NoError(t, err) t.Log(string(yamlData)) v2 := testCase.constructor() err = yaml.Unmarshal(yamlData, v2) if err != nil { t.Log(string(yamlData)) t.Fatal(err) } if !assert.True(t, testCase.equalityAssertion(t, v1, v2), "failed with input data %q", string(data)) { // look at that what the standard lib does with regular map, to help with debugging var m1 map[string]any require.NoError(t, yaml.Unmarshal(data, &m1)) mapJsonData, err := yaml.Marshal(m1) require.NoError(t, err) var m2 map[string]any require.NoError(t, yaml.Unmarshal(mapJsonData, &m2)) t.Logf("initial data = %s", string(data)) t.Logf("unmarshalled map = %v", m1) t.Logf("re-marshalled from map = %s", string(mapJsonData)) t.Logf("re-marshalled from test obj = %s", string(yamlData)) t.Logf("re-unmarshalled map = %s", m2) } }) } }) } ================================================ FILE: core/Clash.Meta/common/orderedmap/yaml_test.go ================================================ package orderedmap import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) func TestMarshalYAML(t *testing.T) { t.Run("int key", func(t *testing.T) { om := New[int, any]() om.Set(1, "bar") om.Set(7, "baz") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") om.Set(5, "28") om.Set(6, "100") om.Set(8, "baz") om.Set(8, "baz") om.Set(9, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem.") b, err := yaml.Marshal(om) expected := `1: bar 7: baz 2: 28 3: 100 4: baz 5: "28" 6: "100" 8: baz 9: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem. ` assert.NoError(t, err) assert.Equal(t, expected, string(b)) }) t.Run("string key", func(t *testing.T) { om := New[string, any]() om.Set("test", "bar") om.Set("abc", true) b, err := yaml.Marshal(om) assert.NoError(t, err) expected := `test: bar abc: true ` assert.Equal(t, expected, string(b)) }) t.Run("typed string key", func(t *testing.T) { type myString string om := New[myString, any]() om.Set("test", "bar") om.Set("abc", true) b, err := yaml.Marshal(om) assert.NoError(t, err) assert.Equal(t, `test: bar abc: true `, string(b)) }) t.Run("typed int key", func(t *testing.T) { type myInt uint32 om := New[myInt, any]() om.Set(1, "bar") om.Set(7, "baz") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") b, err := yaml.Marshal(om) assert.NoError(t, err) assert.Equal(t, `1: bar 7: baz 2: 28 3: 100 4: baz `, string(b)) }) t.Run("TextMarshaller key", func(t *testing.T) { om := New[marshallable, any]() om.Set(marshallable(1), "bar") om.Set(marshallable(28), true) b, err := yaml.Marshal(om) assert.NoError(t, err) assert.Equal(t, `'#1#': bar '#28#': true `, string(b)) }) t.Run("empty map with 0 elements", func(t *testing.T) { om := New[string, any]() b, err := yaml.Marshal(om) assert.NoError(t, err) assert.Equal(t, "{}\n", string(b)) }) t.Run("empty map with no elements (null)", func(t *testing.T) { om := &OrderedMap[string, string]{} b, err := yaml.Marshal(om) assert.NoError(t, err) assert.Equal(t, "{}\n", string(b)) }) } func TestUnmarshallYAML(t *testing.T) { t.Run("int key", func(t *testing.T) { data := ` 1: bar 7: baz 2: 28 3: 100 4: baz 5: "28" 6: "100" 8: baz ` om := New[int, any]() require.NoError(t, yaml.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []int{1, 7, 2, 3, 4, 5, 6, 8}, []any{"bar", "baz", 28, 100, "baz", "28", "100", "baz"}) // serialize back to yaml to make sure things are equal }) t.Run("string key", func(t *testing.T) { data := `{"test":"bar","abc":true}` om := New[string, any]() require.NoError(t, yaml.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []string{"test", "abc"}, []any{"bar", true}) }) t.Run("typed string key", func(t *testing.T) { data := `{"test":"bar","abc":true}` type myString string om := New[myString, any]() require.NoError(t, yaml.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []myString{"test", "abc"}, []any{"bar", true}) }) t.Run("typed int key", func(t *testing.T) { data := ` 1: bar 7: baz 2: 28 3: 100 4: baz 5: "28" 6: "100" 8: baz ` type myInt uint32 om := New[myInt, any]() require.NoError(t, yaml.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []myInt{1, 7, 2, 3, 4, 5, 6, 8}, []any{"bar", "baz", 28, 100, "baz", "28", "100", "baz"}) }) t.Run("TextUnmarshaler key", func(t *testing.T) { data := `{"#1#":"bar","#28#":true}` om := New[marshallable, any]() require.NoError(t, yaml.Unmarshal([]byte(data), &om)) assertOrderedPairsEqual(t, om, []marshallable{1, 28}, []any{"bar", true}) }) t.Run("when fed with an input that's not an object", func(t *testing.T) { for _, data := range []string{"true", `["foo"]`, "42", `"foo"`} { om := New[int, any]() require.Error(t, yaml.Unmarshal([]byte(data), &om)) } }) t.Run("empty map", func(t *testing.T) { data := `{}` om := New[int, any]() require.NoError(t, yaml.Unmarshal([]byte(data), &om)) assertLenEqual(t, om, 0) }) } func TestYAMLSpecialCharacters(t *testing.T) { baselineMap := map[string]any{specialCharacters: specialCharacters} baselineData, err := yaml.Marshal(baselineMap) require.NoError(t, err) // baseline proves this key is supported by official yaml library t.Logf("specialCharacters: %#v as []rune:%v", specialCharacters, []rune(specialCharacters)) t.Logf("baseline yaml data: %s", baselineData) t.Run("marshal special characters", func(t *testing.T) { om := New[string, any]() om.Set(specialCharacters, specialCharacters) b, err := yaml.Marshal(om) require.NoError(t, err) require.Equal(t, baselineData, b) type myString string om2 := New[myString, myString]() om2.Set(specialCharacters, specialCharacters) b, err = yaml.Marshal(om2) require.NoError(t, err) require.Equal(t, baselineData, b) }) t.Run("unmarshall special characters", func(t *testing.T) { om := New[string, any]() require.NoError(t, yaml.Unmarshal(baselineData, &om)) assertOrderedPairsEqual(t, om, []string{specialCharacters}, []any{specialCharacters}) type myString string om2 := New[myString, myString]() require.NoError(t, yaml.Unmarshal(baselineData, &om2)) assertOrderedPairsEqual(t, om2, []myString{specialCharacters}, []myString{specialCharacters}) }) } func TestYAMLRoundTrip(t *testing.T) { for _, testCase := range []struct { name string input string targetFactory func() any }{ { name: "empty map", input: "{}\n", targetFactory: func() any { return &OrderedMap[string, any]{} }, }, { name: "", input: `x: 28 m: bar: - 5: foo: bar foo: - 12: b: true i: 12 m: a: b c: 28 "n": null 28: a: false b: - 1 - 2 - 3 - 3: c: null d: 87 4: e: true 5: f: 4 g: 5 h: 6 `, targetFactory: func() any { return &nestedMaps{} }, }, { name: "with UTF-8 special chars in key", input: "�: 0\n", targetFactory: func() any { return &OrderedMap[string, int]{} }, }, } { t.Run(testCase.name, func(t *testing.T) { target := testCase.targetFactory() require.NoError(t, yaml.Unmarshal([]byte(testCase.input), target)) var ( out []byte err error ) out, err = yaml.Marshal(target) if assert.NoError(t, err) { assert.Equal(t, testCase.input, string(out)) } }) } } func BenchmarkMarshalYAML(b *testing.B) { om := New[int, any]() om.Set(1, "bar") om.Set(7, "baz") om.Set(2, 28) om.Set(3, 100) om.Set(4, "baz") om.Set(5, "28") om.Set(6, "100") om.Set(8, "baz") om.Set(8, "baz") b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = yaml.Marshal(om) } } ================================================ FILE: core/Clash.Meta/common/picker/picker.go ================================================ package picker import ( "context" "sync" "time" ) // Picker provides synchronization, and Context cancelation // for groups of goroutines working on subtasks of a common task. // Inspired by errGroup type Picker[T any] struct { ctx context.Context cancel func() wg sync.WaitGroup once sync.Once errOnce sync.Once result T err error } func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] { return &Picker[T]{ ctx: ctx, cancel: cancel, } } // WithContext returns a new Picker and an associated Context derived from ctx. // and cancel when first element return. func WithContext[T any](ctx context.Context) (*Picker[T], context.Context) { ctx, cancel := context.WithCancel(ctx) return newPicker[T](ctx, cancel), ctx } // WithTimeout returns a new Picker and an associated Context derived from ctx with timeout. func WithTimeout[T any](ctx context.Context, timeout time.Duration) (*Picker[T], context.Context) { ctx, cancel := context.WithTimeout(ctx, timeout) return newPicker[T](ctx, cancel), ctx } // Wait blocks until all function calls from the Go method have returned, // then returns the first nil error result (if any) from them. func (p *Picker[T]) Wait() T { p.wg.Wait() if p.cancel != nil { p.cancel() p.cancel = nil } return p.result } // Error return the first error (if all success return nil) func (p *Picker[T]) Error() error { return p.err } // Go calls the given function in a new goroutine. // The first call to return a nil error cancels the group; its result will be returned by Wait. func (p *Picker[T]) Go(f func() (T, error)) { p.wg.Add(1) go func() { defer p.wg.Done() if ret, err := f(); err == nil { p.once.Do(func() { p.result = ret if p.cancel != nil { p.cancel() p.cancel = nil } }) } else { p.errOnce.Do(func() { p.err = err }) } }() } // Close cancels the picker context and releases resources associated with it. // If Wait has been called, then there is no need to call Close. func (p *Picker[T]) Close() error { if p.cancel != nil { p.cancel() p.cancel = nil } return nil } ================================================ FILE: core/Clash.Meta/common/picker/picker_test.go ================================================ package picker import ( "context" "testing" "time" "github.com/samber/lo" "github.com/stretchr/testify/assert" ) func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, error) { return func() (T, error) { timer := time.NewTimer(time.Millisecond * time.Duration(delay)) select { case <-timer.C: return input, nil case <-ctx.Done(): return lo.Empty[T](), ctx.Err() } } } func TestPicker_Basic(t *testing.T) { t.Parallel() picker, ctx := WithContext[int](context.Background()) picker.Go(sleepAndSend(ctx, 200, 2)) picker.Go(sleepAndSend(ctx, 100, 1)) number := picker.Wait() assert.NotNil(t, number) assert.Equal(t, number, 1) } func TestPicker_Timeout(t *testing.T) { t.Parallel() picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5) picker.Go(sleepAndSend(ctx, 100, 1)) number := picker.Wait() assert.Equal(t, number, lo.Empty[int]()) assert.NotNil(t, picker.Error()) } ================================================ FILE: core/Clash.Meta/common/pool/alloc.go ================================================ package pool // Inspired by https://github.com/xtaci/smux/blob/master/alloc.go import ( "errors" "math/bits" "sync" ) var DefaultAllocator = NewAllocator() type Allocator interface { Get(size int) []byte Put(buf []byte) error } // defaultAllocator for incoming frames, optimized to prevent overwriting after zeroing type defaultAllocator struct { buffers [11]sync.Pool } // NewAllocator initiates a []byte allocator for frames less than 65536 bytes, // the waste(memory fragmentation) of space allocation is guaranteed to be // no more than 50%. func NewAllocator() Allocator { return &defaultAllocator{ buffers: [...]sync.Pool{ // 64B -> 64K {New: func() any { return new([1 << 6]byte) }}, {New: func() any { return new([1 << 7]byte) }}, {New: func() any { return new([1 << 8]byte) }}, {New: func() any { return new([1 << 9]byte) }}, {New: func() any { return new([1 << 10]byte) }}, {New: func() any { return new([1 << 11]byte) }}, {New: func() any { return new([1 << 12]byte) }}, {New: func() any { return new([1 << 13]byte) }}, {New: func() any { return new([1 << 14]byte) }}, {New: func() any { return new([1 << 15]byte) }}, {New: func() any { return new([1 << 16]byte) }}, }, } } // Get a []byte from pool with most appropriate cap func (alloc *defaultAllocator) Get(size int) []byte { switch { case size < 0: panic("alloc.Get: len out of range") case size == 0: return nil case size > 65536: return make([]byte, size) default: var index uint16 if size > 64 { index = msb(size) if size != 1< 65536 { return nil } bits := msb(cap(buf)) if cap(buf) != 1< 0 { item := q.items[0] q.items = q.items[1:] q.lock.Unlock() consumed <- item } else { q.lock.Unlock() } // Small sleep to increase chance of race conditions time.Sleep(time.Microsecond) } }() } // Wait for all goroutines to finish wg.Wait() // Close the consumed channel close(consumed) // Count the number of consumed items consumedCount := 0 for range consumed { consumedCount++ } // Check that the queue is in a consistent state totalItems := goroutines * operations remaining := int(q.Len()) assert.Equal(t, totalItems, consumedCount+remaining, "Total items should equal consumed items plus remaining items") } // TestQueueWithDifferentTypes tests the Queue with different types func TestQueueWithDifferentTypes(t *testing.T) { // Test with string type qString := New[string](5) qString.Put("hello", "world") assert.Equal(t, int64(2), qString.Len(), "Queue length should be 2") assert.Equal(t, "hello", qString.Pop(), "First item should be 'hello'") assert.Equal(t, "world", qString.Pop(), "Second item should be 'world'") // Test with struct type type Person struct { Name string Age int } qStruct := New[Person](5) qStruct.Put(Person{Name: "Alice", Age: 30}, Person{Name: "Bob", Age: 25}) assert.Equal(t, int64(2), qStruct.Len(), "Queue length should be 2") firstPerson := qStruct.Pop() assert.Equal(t, "Alice", firstPerson.Name, "First person's name should be 'Alice'") secondPerson := qStruct.Pop() assert.Equal(t, "Bob", secondPerson.Name, "Second person's name should be 'Bob'") } ================================================ FILE: core/Clash.Meta/common/singledo/singledo.go ================================================ package singledo import ( "sync" "time" ) type call[T any] struct { wg sync.WaitGroup val T err error } type Single[T any] struct { mux sync.Mutex wait time.Duration call *call[T] result *Result[T] } type Result[T any] struct { Val T Err error Time time.Time } // Do single.Do likes sync.singleFlight func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) { s.mux.Lock() result := s.result if result != nil && time.Since(result.Time) < s.wait { s.mux.Unlock() return result.Val, result.Err, true } s.result = nil // The result has expired, clear it if callM := s.call; callM != nil { s.mux.Unlock() callM.wg.Wait() return callM.val, callM.err, true } callM := &call[T]{} callM.wg.Add(1) s.call = callM s.mux.Unlock() callM.val, callM.err = fn() callM.wg.Done() s.mux.Lock() if s.call == callM { // maybe reset when fn is running s.call = nil s.result = &Result[T]{callM.val, callM.err, time.Now()} } s.mux.Unlock() return callM.val, callM.err, false } func (s *Single[T]) Reset() { s.mux.Lock() s.call = nil s.result = nil s.mux.Unlock() } func NewSingle[T any](wait time.Duration) *Single[T] { return &Single[T]{wait: wait} } ================================================ FILE: core/Clash.Meta/common/singledo/singledo_test.go ================================================ package singledo import ( "sync" "testing" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/stretchr/testify/assert" ) func TestBasic(t *testing.T) { t.Parallel() single := NewSingle[int](time.Millisecond * 200) foo := 0 shardCount := atomic.NewInt32(0) call := func() (int, error) { foo++ time.Sleep(time.Millisecond * 20) return 0, nil } var wg sync.WaitGroup const n = 5 wg.Add(n) for i := 0; i < n; i++ { go func() { _, _, shard := single.Do(call) if shard { shardCount.Add(1) } wg.Done() }() } wg.Wait() assert.Equal(t, 1, foo) assert.Equal(t, int32(4), shardCount.Load()) } func TestTimer(t *testing.T) { t.Parallel() single := NewSingle[int](time.Millisecond * 200) foo := 0 callM := func() (int, error) { foo++ return 0, nil } _, _, _ = single.Do(callM) time.Sleep(100 * time.Millisecond) _, _, shard := single.Do(callM) assert.Equal(t, 1, foo) assert.True(t, shard) } func TestReset(t *testing.T) { t.Parallel() single := NewSingle[int](time.Millisecond * 200) foo := 0 callM := func() (int, error) { foo++ return 0, nil } _, _, _ = single.Do(callM) single.Reset() _, _, _ = single.Do(callM) assert.Equal(t, 2, foo) } ================================================ FILE: core/Clash.Meta/common/singleflight/singleflight.go ================================================ // copy and modify from "golang.org/x/sync/singleflight" // Copyright 2013 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 singleflight provides a duplicate function call suppression // mechanism. package singleflight import ( "bytes" "errors" "fmt" "runtime" "runtime/debug" "sync" ) // errGoexit indicates the runtime.Goexit was called in // the user given function. var errGoexit = errors.New("runtime.Goexit was called") // A panicError is an arbitrary value recovered from a panic // with the stack trace during the execution of given function. type panicError struct { value interface{} stack []byte } // Error implements error interface. func (p *panicError) Error() string { return fmt.Sprintf("%v\n\n%s", p.value, p.stack) } func (p *panicError) Unwrap() error { err, ok := p.value.(error) if !ok { return nil } return err } func newPanicError(v interface{}) error { stack := debug.Stack() // The first line of the stack trace is of the form "goroutine N [status]:" // but by the time the panic reaches Do the goroutine may no longer exist // and its status will have changed. Trim out the misleading line. if line := bytes.IndexByte(stack[:], '\n'); line >= 0 { stack = stack[line+1:] } return &panicError{value: v, stack: stack} } // call is an in-flight or completed singleflight.Do call type call[T any] struct { wg sync.WaitGroup // These fields are written once before the WaitGroup is done // and are only read after the WaitGroup is done. val T err error // These fields are read and written with the singleflight // mutex held before the WaitGroup is done, and are read but // not written after the WaitGroup is done. dups int chans []chan<- Result[T] } // Group represents a class of work and forms a namespace in // which units of work can be executed with duplicate suppression. type Group[T any] struct { mu sync.Mutex // protects m m map[string]*call[T] // lazily initialized StoreResult bool } // Result holds the results of Do, so they can be passed // on a channel. type Result[T any] struct { Val T Err error Shared bool } // Do executes and returns the results of the given function, making // sure that only one execution is in-flight for a given key at a // time. If a duplicate comes in, the duplicate caller waits for the // original to complete and receives the same results. // The return value shared indicates whether v was given to multiple callers. func (g *Group[T]) Do(key string, fn func() (T, error)) (v T, err error, shared bool) { g.mu.Lock() if g.m == nil { g.m = make(map[string]*call[T]) } if c, ok := g.m[key]; ok { c.dups++ g.mu.Unlock() c.wg.Wait() if e, ok := c.err.(*panicError); ok { panic(e) } else if c.err == errGoexit { runtime.Goexit() } return c.val, c.err, true } c := new(call[T]) c.wg.Add(1) g.m[key] = c g.mu.Unlock() g.doCall(c, key, fn) return c.val, c.err, c.dups > 0 } // DoChan is like Do but returns a channel that will receive the // results when they are ready. // // The returned channel will not be closed. func (g *Group[T]) DoChan(key string, fn func() (T, error)) <-chan Result[T] { ch := make(chan Result[T], 1) g.mu.Lock() if g.m == nil { g.m = make(map[string]*call[T]) } if c, ok := g.m[key]; ok { c.dups++ c.chans = append(c.chans, ch) g.mu.Unlock() return ch } c := &call[T]{chans: []chan<- Result[T]{ch}} c.wg.Add(1) g.m[key] = c g.mu.Unlock() go g.doCall(c, key, fn) return ch } // doCall handles the single call for a key. func (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) { normalReturn := false recovered := false // use double-defer to distinguish panic from runtime.Goexit, // more details see https://golang.org/cl/134395 defer func() { // the given function invoked runtime.Goexit if !normalReturn && !recovered { c.err = errGoexit } g.mu.Lock() defer g.mu.Unlock() c.wg.Done() if g.m[key] == c && !g.StoreResult { delete(g.m, key) } if e, ok := c.err.(*panicError); ok { // In order to prevent the waiting channels from being blocked forever, // needs to ensure that this panic cannot be recovered. if len(c.chans) > 0 { go panic(e) select {} // Keep this goroutine around so that it will appear in the crash dump. } else { panic(e) } } else if c.err == errGoexit { // Already in the process of goexit, no need to call again } else { // Normal return for _, ch := range c.chans { ch <- Result[T]{c.val, c.err, c.dups > 0} } } }() func() { defer func() { if !normalReturn { // Ideally, we would wait to take a stack trace until we've determined // whether this is a panic or a runtime.Goexit. // // Unfortunately, the only way we can distinguish the two is to see // whether the recover stopped the goroutine from terminating, and by // the time we know that, the part of the stack trace relevant to the // panic has been discarded. if r := recover(); r != nil { c.err = newPanicError(r) } } }() c.val, c.err = fn() normalReturn = true }() if !normalReturn { recovered = true } } // Forget tells the singleflight to forget about a key. Future calls // to Do for this key will call the function rather than waiting for // an earlier call to complete. func (g *Group[T]) Forget(key string) { g.mu.Lock() delete(g.m, key) g.mu.Unlock() } func (g *Group[T]) Reset() { g.mu.Lock() g.m = nil g.mu.Unlock() } ================================================ FILE: core/Clash.Meta/common/sockopt/reuse_common.go ================================================ package sockopt import ( "net" "syscall" ) func RawConnReuseaddr(rc syscall.RawConn) (err error) { var innerErr error err = rc.Control(func(fd uintptr) { innerErr = reuseControl(fd) }) if innerErr != nil { err = innerErr } return } func UDPReuseaddr(c net.PacketConn) error { if c, ok := c.(syscall.Conn); ok { rc, err := c.SyscallConn() if err != nil { return err } return RawConnReuseaddr(rc) } return nil } ================================================ FILE: core/Clash.Meta/common/sockopt/reuse_other.go ================================================ //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows package sockopt func reuseControl(fd uintptr) error { return nil } ================================================ FILE: core/Clash.Meta/common/sockopt/reuse_unix.go ================================================ //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris package sockopt import ( "golang.org/x/sys/unix" ) func reuseControl(fd uintptr) error { e1 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) e2 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) if e1 != nil { return e1 } if e2 != nil { return e2 } return nil } ================================================ FILE: core/Clash.Meta/common/sockopt/reuse_windows.go ================================================ package sockopt import ( "golang.org/x/sys/windows" ) func reuseControl(fd uintptr) error { return windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) } ================================================ FILE: core/Clash.Meta/common/structure/structure.go ================================================ package structure // references: https://github.com/mitchellh/mapstructure import ( "encoding" "encoding/base64" "fmt" "reflect" "sort" "strconv" "strings" ) // Option is the configuration that is used to create a new decoder type Option struct { TagName string WeaklyTypedInput bool KeyReplacer *strings.Replacer } var DefaultKeyReplacer = strings.NewReplacer("_", "-") // Decoder is the core of structure type Decoder struct { option *Option } // NewDecoder return a Decoder by Option func NewDecoder(option Option) *Decoder { if option.TagName == "" { option.TagName = "structure" } return &Decoder{option: &option} } // Decode transform a map[string]any to a struct func (d *Decoder) Decode(src map[string]any, dst any) error { if reflect.TypeOf(dst).Kind() != reflect.Ptr { return fmt.Errorf("decode must recive a ptr struct") } return d.decode("", src, reflect.ValueOf(dst).Elem()) } // isNil returns true if the input is nil or a typed nil pointer. func isNil(input any) bool { if input == nil { return true } val := reflect.ValueOf(input) return val.Kind() == reflect.Pointer && val.IsNil() } func (d *Decoder) decode(name string, data any, val reflect.Value) error { if isNil(data) { // If the data is nil, then we don't set anything // Maybe we should set to zero value? return nil } if !reflect.ValueOf(data).IsValid() { // If the input value is invalid, then we just set the value // to be the zero value. val.Set(reflect.Zero(val.Type())) return nil } for { kind := val.Kind() if kind == reflect.Pointer && val.IsNil() { val.Set(reflect.New(val.Type().Elem())) } if ok, err := d.decodeTextUnmarshaller(name, data, val); ok { return err } switch { case isInt(kind): return d.decodeInt(name, data, val) case isUint(kind): return d.decodeUint(name, data, val) case isFloat(kind): return d.decodeFloat(name, data, val) } switch kind { case reflect.Pointer: val = val.Elem() continue case reflect.String: return d.decodeString(name, data, val) case reflect.Bool: return d.decodeBool(name, data, val) case reflect.Slice: return d.decodeSlice(name, data, val) case reflect.Map: return d.decodeMap(name, data, val) case reflect.Interface: return d.setInterface(name, data, val) case reflect.Struct: return d.decodeStruct(name, data, val) default: return fmt.Errorf("type %s not support", val.Kind().String()) } } } func isInt(kind reflect.Kind) bool { switch kind { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return true default: return false } } func isUint(kind reflect.Kind) bool { switch kind { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return true default: return false } } func isFloat(kind reflect.Kind) bool { switch kind { case reflect.Float32, reflect.Float64: return true default: return false } } func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) kind := dataVal.Kind() switch { case isInt(kind): val.SetInt(dataVal.Int()) case isUint(kind) && d.option.WeaklyTypedInput: val.SetInt(int64(dataVal.Uint())) case isFloat(kind) && d.option.WeaklyTypedInput: val.SetInt(int64(dataVal.Float())) case kind == reflect.String && d.option.WeaklyTypedInput: var i int64 i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) if err == nil { val.SetInt(i) } else { err = fmt.Errorf("cannot parse '%s' as int: %s", name, err) } default: err = fmt.Errorf( "'%s' expected type '%s', got unconvertible type '%s'", name, val.Type(), dataVal.Type(), ) } return err } func (d *Decoder) decodeUint(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) kind := dataVal.Kind() switch { case isUint(kind): val.SetUint(dataVal.Uint()) case isInt(kind) && d.option.WeaklyTypedInput: val.SetUint(uint64(dataVal.Int())) case isFloat(kind) && d.option.WeaklyTypedInput: val.SetUint(uint64(dataVal.Float())) case kind == reflect.String && d.option.WeaklyTypedInput: var i uint64 i, err = strconv.ParseUint(dataVal.String(), 0, val.Type().Bits()) if err == nil { val.SetUint(i) } else { err = fmt.Errorf("cannot parse '%s' as int: %s", name, err) } default: err = fmt.Errorf( "'%s' expected type '%s', got unconvertible type '%s'", name, val.Type(), dataVal.Type(), ) } return err } func (d *Decoder) decodeFloat(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) kind := dataVal.Kind() switch { case isFloat(kind): val.SetFloat(dataVal.Float()) case isUint(kind): val.SetFloat(float64(dataVal.Uint())) case isInt(kind) && d.option.WeaklyTypedInput: val.SetFloat(float64(dataVal.Int())) case kind == reflect.String && d.option.WeaklyTypedInput: var i float64 i, err = strconv.ParseFloat(dataVal.String(), val.Type().Bits()) if err == nil { val.SetFloat(i) } else { err = fmt.Errorf("cannot parse '%s' as int: %s", name, err) } default: err = fmt.Errorf( "'%s' expected type '%s', got unconvertible type '%s'", name, val.Type(), dataVal.Type(), ) } return err } func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) kind := dataVal.Kind() switch { case kind == reflect.String: val.SetString(dataVal.String()) case isInt(kind) && d.option.WeaklyTypedInput: val.SetString(strconv.FormatInt(dataVal.Int(), 10)) case isUint(kind) && d.option.WeaklyTypedInput: val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) case isFloat(kind) && d.option.WeaklyTypedInput: val.SetString(strconv.FormatFloat(dataVal.Float(), 'E', -1, dataVal.Type().Bits())) default: err = fmt.Errorf( "'%s' expected type '%s', got unconvertible type '%s'", name, val.Type(), dataVal.Type(), ) } return err } func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) kind := dataVal.Kind() switch { case kind == reflect.Bool: val.SetBool(dataVal.Bool()) case isInt(kind) && d.option.WeaklyTypedInput: val.SetBool(dataVal.Int() != 0) case isUint(kind) && d.option.WeaklyTypedInput: val.SetBool(dataVal.Uint() != 0) default: err = fmt.Errorf( "'%s' expected type '%s', got unconvertible type '%s'", name, val.Type(), dataVal.Type(), ) } return err } func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) valType := val.Type() valElemType := valType.Elem() if dataVal.Kind() == reflect.String && valElemType.Kind() == reflect.Uint8 { // from encoding/json s := []byte(dataVal.String()) b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) n, err := base64.StdEncoding.Decode(b, s) if err != nil { return fmt.Errorf("try decode '%s' by base64 error: %w", name, err) } val.SetBytes(b[:n]) return nil } if dataVal.Kind() != reflect.Slice { return fmt.Errorf("'%s' is not a slice", name) } valSlice := val // make a new slice with cap(val)==cap(dataVal) // the caller can determine whether the original configuration contains this item by judging whether the value is nil. valSlice = reflect.MakeSlice(valType, 0, dataVal.Len()) for i := 0; i < dataVal.Len(); i++ { currentData := dataVal.Index(i).Interface() for valSlice.Len() <= i { valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) } fieldName := fmt.Sprintf("%s[%d]", name, i) if currentData == nil { // in weakly type mode, null will convert to zero value if d.option.WeaklyTypedInput { continue } // in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice { continue } return fmt.Errorf("'%s' can not be null", fieldName) } currentField := valSlice.Index(i) if err := d.decode(fieldName, currentData, currentField); err != nil { return err } } val.Set(valSlice) return nil } func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error { valType := val.Type() valKeyType := valType.Key() valElemType := valType.Elem() valMap := val if valMap.IsNil() { mapType := reflect.MapOf(valKeyType, valElemType) valMap = reflect.MakeMap(mapType) } dataVal := reflect.Indirect(reflect.ValueOf(data)) if dataVal.Kind() != reflect.Map { return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) } return d.decodeMapFromMap(name, dataVal, val, valMap) } func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { valType := val.Type() valKeyType := valType.Key() valElemType := valType.Elem() errors := make([]string, 0) if dataVal.Len() == 0 { if dataVal.IsNil() { if !val.IsNil() { val.Set(dataVal) } } else { val.Set(valMap) } return nil } for _, k := range dataVal.MapKeys() { fieldName := fmt.Sprintf("%s[%s]", name, k) currentKey := reflect.Indirect(reflect.New(valKeyType)) if err := d.decode(fieldName, k.Interface(), currentKey); err != nil { errors = append(errors, err.Error()) continue } v := dataVal.MapIndex(k).Interface() if v == nil { errors = append(errors, fmt.Sprintf("filed %s invalid", fieldName)) continue } currentVal := reflect.Indirect(reflect.New(valElemType)) if err := d.decode(fieldName, v, currentVal); err != nil { errors = append(errors, err.Error()) continue } valMap.SetMapIndex(currentKey, currentVal) } val.Set(valMap) if len(errors) > 0 { return fmt.Errorf(strings.Join(errors, ",")) } return nil } func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) // If the type of the value to write to and the data match directly, // then we just set it directly instead of recursing into the structure. if dataVal.Type() == val.Type() { val.Set(dataVal) return nil } dataValKind := dataVal.Kind() switch dataValKind { case reflect.Map: return d.decodeStructFromMap(name, dataVal, val) default: return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) } } func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { dataValType := dataVal.Type() if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { return fmt.Errorf( "'%s' needs a map with string keys, has '%s' keys", name, dataValType.Key().Kind()) } dataValKeys := make(map[reflect.Value]struct{}) dataValKeysUnused := make(map[any]struct{}) for _, dataValKey := range dataVal.MapKeys() { dataValKeys[dataValKey] = struct{}{} dataValKeysUnused[dataValKey.Interface()] = struct{}{} } targetValKeysUnused := make(map[any]struct{}) errors := make([]string, 0) // This slice will keep track of all the structs we'll be decoding. // There can be more than one struct if there are embedded structs // that are squashed. structs := make([]reflect.Value, 1, 5) structs[0] = val // Compile the list of all the fields that we're going to be decoding // from all the structs. type field struct { field reflect.StructField val reflect.Value } // remainField is set to a valid field set with the "remain" tag if // we are keeping track of remaining values. var remainField *field var fields []field for len(structs) > 0 { structVal := structs[0] structs = structs[1:] structType := structVal.Type() for i := 0; i < structType.NumField(); i++ { fieldType := structType.Field(i) fieldVal := structVal.Field(i) if fieldVal.Kind() == reflect.Ptr && fieldVal.Elem().Kind() == reflect.Struct { // Handle embedded struct pointers as embedded structs. fieldVal = fieldVal.Elem() } // If "squash" is specified in the tag, we squash the field down. squash := fieldVal.Kind() == reflect.Struct && fieldType.Anonymous remain := false // We always parse the tags cause we're looking for other tags too tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",") for _, tag := range tagParts[1:] { if tag == "squash" { squash = true break } if tag == "remain" { remain = true break } } if squash { if fieldVal.Kind() != reflect.Struct { errors = append(errors, fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldVal.Kind()).Error()) } else { structs = append(structs, fieldVal) } continue } // Build our field if remain { remainField = &field{fieldType, fieldVal} } else { // Normal struct field, store it away fields = append(fields, field{fieldType, fieldVal}) } } } // for fieldType, field := range fields { for _, f := range fields { field, fieldValue := f.field, f.val fieldName := field.Name tagParts := strings.Split(field.Tag.Get(d.option.TagName), ",") tagValue := tagParts[0] if tagValue != "" { fieldName = tagValue } if tagValue == "-" { continue } omitempty := false for _, tag := range tagParts[1:] { if tag == "omitempty" { omitempty = true } } rawMapKey := reflect.ValueOf(fieldName) rawMapVal := dataVal.MapIndex(rawMapKey) if !rawMapVal.IsValid() { // Do a slower search by iterating over each key and // doing case-insensitive search. if d.option.KeyReplacer != nil { fieldName = d.option.KeyReplacer.Replace(fieldName) } for dataValKey := range dataValKeys { mK, ok := dataValKey.Interface().(string) if !ok { // Not a string key continue } if d.option.KeyReplacer != nil { mK = d.option.KeyReplacer.Replace(mK) } if strings.EqualFold(mK, fieldName) { rawMapKey = dataValKey rawMapVal = dataVal.MapIndex(dataValKey) break } } if !rawMapVal.IsValid() { // There was no matching key in the map for the value in // the struct. Remember it for potential errors and metadata. if !omitempty { targetValKeysUnused[fieldName] = struct{}{} } continue } } // Delete the key we're using from the unused map so we stop tracking delete(dataValKeysUnused, rawMapKey.Interface()) if !fieldValue.IsValid() { // This should never happen panic("field is not valid") } // If we can't set the field, then it is unexported or something, // and we just continue onwards. if !fieldValue.CanSet() { continue } // If the name is empty string, then we're at the root, and we // don't dot-join the fields. if name != "" { fieldName = name + "." + fieldName } if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil { errors = append(errors, err.Error()) } } // If we have a "remain"-tagged field and we have unused keys then // we put the unused keys directly into the remain field. if remainField != nil && len(dataValKeysUnused) > 0 { // Build a map of only the unused values remain := map[interface{}]interface{}{} for key := range dataValKeysUnused { remain[key] = dataVal.MapIndex(reflect.ValueOf(key)).Interface() } // Decode it as-if we were just decoding this map onto our map. if err := d.decodeMap(name, remain, remainField.val); err != nil { errors = append(errors, err.Error()) } // Set the map to nil so we have none so that the next check will // not error (ErrorUnused) dataValKeysUnused = nil } if len(targetValKeysUnused) > 0 { keys := make([]string, 0, len(targetValKeysUnused)) for rawKey := range targetValKeysUnused { keys = append(keys, rawKey.(string)) } sort.Strings(keys) err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", ")) errors = append(errors, err.Error()) } if len(errors) > 0 { return fmt.Errorf(strings.Join(errors, ",")) } return nil } func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) val.Set(dataVal) return nil } func (d *Decoder) decodeTextUnmarshaller(name string, data any, val reflect.Value) (bool, error) { if !val.CanAddr() { return false, nil } valAddr := val.Addr() if !valAddr.CanInterface() { return false, nil } unmarshaller, ok := valAddr.Interface().(encoding.TextUnmarshaler) if !ok { return false, nil } var str string if err := d.decodeString(name, data, reflect.Indirect(reflect.ValueOf(&str))); err != nil { return false, err } if err := unmarshaller.UnmarshalText([]byte(str)); err != nil { return true, fmt.Errorf("cannot parse '%s' as %s: %s", name, val.Type(), err) } return true, nil } ================================================ FILE: core/Clash.Meta/common/structure/structure_test.go ================================================ package structure import ( "strconv" "testing" "github.com/stretchr/testify/assert" ) var ( decoder = NewDecoder(Option{TagName: "test"}) weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true}) ) type Baz struct { Foo int `test:"foo"` Bar string `test:"bar"` } type BazSlice struct { Foo int `test:"foo"` Bar []string `test:"bar"` } type BazOptional struct { Foo int `test:"foo,omitempty"` Bar string `test:"bar,omitempty"` } func TestStructure_Basic(t *testing.T) { rawMap := map[string]any{ "foo": 1, "bar": "test", "extra": false, } goal := &Baz{ Foo: 1, Bar: "test", } s := &Baz{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, goal, s) } func TestStructure_Slice(t *testing.T) { rawMap := map[string]any{ "foo": 1, "bar": []string{"one", "two"}, } goal := &BazSlice{ Foo: 1, Bar: []string{"one", "two"}, } s := &BazSlice{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, goal, s) } func TestStructure_Optional(t *testing.T) { rawMap := map[string]any{ "foo": 1, } goal := &BazOptional{ Foo: 1, } s := &BazOptional{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, goal, s) } func TestStructure_MissingKey(t *testing.T) { rawMap := map[string]any{ "foo": 1, } s := &Baz{} err := decoder.Decode(rawMap, s) assert.NotNilf(t, err, "should throw error: %#v", s) } func TestStructure_ParamError(t *testing.T) { rawMap := map[string]any{} s := Baz{} err := decoder.Decode(rawMap, s) assert.NotNilf(t, err, "should throw error: %#v", s) } func TestStructure_SliceTypeError(t *testing.T) { rawMap := map[string]any{ "foo": 1, "bar": []int{1, 2}, } s := &BazSlice{} err := decoder.Decode(rawMap, s) assert.NotNilf(t, err, "should throw error: %#v", s) } func TestStructure_WeakType(t *testing.T) { rawMap := map[string]any{ "foo": "1", "bar": []int{1}, } goal := &BazSlice{ Foo: 1, Bar: []string{"1"}, } s := &BazSlice{} err := weakTypeDecoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, goal, s) } func TestStructure_Nest(t *testing.T) { rawMap := map[string]any{ "foo": 1, } goal := BazOptional{ Foo: 1, } s := &struct { BazOptional }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, s.BazOptional, goal) } func TestStructure_DoubleNest(t *testing.T) { rawMap := map[string]any{ "bar": map[string]any{ "foo": 1, }, } goal := BazOptional{ Foo: 1, } s := &struct { Bar struct { BazOptional } `test:"bar"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, s.Bar.BazOptional, goal) } func TestStructure_Remain(t *testing.T) { rawMap := map[string]any{ "foo": 1, "bar": "test", "extra": false, } goal := &Baz{ Foo: 1, Bar: "test", } s := &struct { Baz Remain map[string]any `test:",remain"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, *goal, s.Baz) assert.Equal(t, map[string]any{"extra": false}, s.Remain) } func TestStructure_SliceNilValue(t *testing.T) { rawMap := map[string]any{ "foo": 1, "bar": []any{"bar", nil}, } goal := &BazSlice{ Foo: 1, Bar: []string{"bar", ""}, } s := &BazSlice{} err := weakTypeDecoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, goal.Bar, s.Bar) s = &BazSlice{} err = decoder.Decode(rawMap, s) assert.NotNil(t, err) } func TestStructure_SliceNilValueComplex(t *testing.T) { rawMap := map[string]any{ "bar": []any{map[string]any{"bar": "foo"}, nil}, } s := &struct { Bar []map[string]any `test:"bar"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Nil(t, s.Bar[1]) ss := &struct { Bar []Baz `test:"bar"` }{} err = decoder.Decode(rawMap, ss) assert.NotNil(t, err) } func TestStructure_SliceCap(t *testing.T) { rawMap := map[string]any{ "foo": []string{}, } s := &struct { Foo []string `test:"foo,omitempty"` Bar []string `test:"bar,omitempty"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.NotNil(t, s.Foo) // structure's Decode will ensure value not nil when input has value even it was set an empty array assert.Nil(t, s.Bar) } func TestStructure_Base64(t *testing.T) { rawMap := map[string]any{ "foo": "AQID", } s := &struct { Foo []byte `test:"foo"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, []byte{1, 2, 3}, s.Foo) } func TestStructure_Pointer(t *testing.T) { rawMap := map[string]any{ "foo": "foo", } s := &struct { Foo *string `test:"foo,omitempty"` Bar *string `test:"bar,omitempty"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.NotNil(t, s.Foo) assert.Equal(t, "foo", *s.Foo) assert.Nil(t, s.Bar) } func TestStructure_PointerStruct(t *testing.T) { rawMap := map[string]any{ "foo": "foo", } s := &struct { Foo *string `test:"foo,omitempty"` Bar *Baz `test:"bar,omitempty"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.NotNil(t, s.Foo) assert.Equal(t, "foo", *s.Foo) assert.Nil(t, s.Bar) } type num struct { a int } func (n *num) UnmarshalText(text []byte) (err error) { n.a, err = strconv.Atoi(string(text)) return } func TestStructure_TextUnmarshaller(t *testing.T) { rawMap := map[string]any{ "num": "255", "num_p": "127", "num_arr": []string{"1", "2", "3"}, } s := &struct { Num num `test:"num"` NumP *num `test:"num_p"` NumArr []num `test:"num_arr"` }{} err := decoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, 255, s.Num.a) assert.NotNil(t, s.NumP) assert.Equal(t, s.NumP.a, 127) assert.Equal(t, s.NumArr, []num{{1}, {2}, {3}}) // test WeaklyTypedInput rawMap["num"] = 256 err = decoder.Decode(rawMap, s) assert.NotNilf(t, err, "should throw error: %#v", s) err = weakTypeDecoder.Decode(rawMap, s) assert.Nil(t, err) assert.Equal(t, 256, s.Num.a) // test invalid input rawMap["num_p"] = "abc" err = decoder.Decode(rawMap, s) assert.NotNilf(t, err, "should throw error: %#v", s) } func TestStructure_Null(t *testing.T) { rawMap := map[string]any{ "opt": map[string]any{ "bar": nil, }, } s := struct { Opt struct { Bar string `test:"bar,optional"` } `test:"opt,optional"` }{} err := decoder.Decode(rawMap, &s) assert.Nil(t, err) assert.Equal(t, s.Opt.Bar, "") } func TestStructure_Ignore(t *testing.T) { rawMap := map[string]any{ "-": "newData", } s := struct { MustIgnore string `test:"-"` }{MustIgnore: "oldData"} err := decoder.Decode(rawMap, &s) assert.Nil(t, err) assert.Equal(t, s.MustIgnore, "oldData") // test omitempty delete(rawMap, "-") err = decoder.Decode(rawMap, &s) assert.Nil(t, err) assert.Equal(t, s.MustIgnore, "oldData") } func TestStructure_IgnoreInNest(t *testing.T) { rawMap := map[string]any{ "-": "newData", } type TP struct { MustIgnore string `test:"-"` } s := struct { TP }{TP{MustIgnore: "oldData"}} err := decoder.Decode(rawMap, &s) assert.Nil(t, err) assert.Equal(t, s.MustIgnore, "oldData") // test omitempty delete(rawMap, "-") err = decoder.Decode(rawMap, &s) assert.Nil(t, err) assert.Equal(t, s.MustIgnore, "oldData") } ================================================ FILE: core/Clash.Meta/common/utils/callback.go ================================================ package utils import ( "io" "sync" list "github.com/bahlo/generic-list-go" ) type Callback[T any] struct { list list.List[func(T)] mutex sync.RWMutex } func NewCallback[T any]() *Callback[T] { return &Callback[T]{} } func (c *Callback[T]) Register(item func(T)) io.Closer { c.mutex.Lock() defer c.mutex.Unlock() element := c.list.PushBack(item) return &callbackCloser[T]{ element: element, callback: c, } } func (c *Callback[T]) Emit(item T) { c.mutex.RLock() defer c.mutex.RUnlock() for element := c.list.Front(); element != nil; element = element.Next() { go element.Value(item) } } type callbackCloser[T any] struct { element *list.Element[func(T)] callback *Callback[T] once sync.Once } func (c *callbackCloser[T]) Close() error { c.once.Do(func() { c.callback.mutex.Lock() defer c.callback.mutex.Unlock() c.callback.list.Remove(c.element) }) return nil } ================================================ FILE: core/Clash.Meta/common/utils/global_id.go ================================================ package utils import ( "hash/maphash" "unsafe" ) var globalSeed = maphash.MakeSeed() func GlobalID(material string) (id [8]byte) { *(*uint64)(unsafe.Pointer(&id[0])) = maphash.String(globalSeed, material) return } func MapHash(material string) uint64 { return maphash.String(globalSeed, material) } ================================================ FILE: core/Clash.Meta/common/utils/hash.go ================================================ package utils import ( "crypto/md5" "encoding/hex" "errors" ) // HashType warps hash array inside struct // someday can change to other hash algorithm simply type HashType struct { md5 [md5.Size]byte // MD5 } func MakeHash(data []byte) HashType { return HashType{md5.Sum(data)} } func (h HashType) Equal(hash HashType) bool { return h.md5 == hash.md5 } func (h HashType) Bytes() []byte { return h.md5[:] } func (h HashType) String() string { return hex.EncodeToString(h.Bytes()) } func (h HashType) MarshalText() ([]byte, error) { return []byte(h.String()), nil } func (h *HashType) UnmarshalText(data []byte) error { if hex.DecodedLen(len(data)) != md5.Size { return errors.New("invalid hash length") } _, err := hex.Decode(h.md5[:], data) return err } func (h HashType) MarshalBinary() ([]byte, error) { return h.md5[:], nil } func (h *HashType) UnmarshalBinary(data []byte) error { if len(data) != md5.Size { return errors.New("invalid hash length") } copy(h.md5[:], data) return nil } func (h HashType) Len() int { return len(h.md5) } func (h HashType) IsValid() bool { var zero HashType return h != zero } ================================================ FILE: core/Clash.Meta/common/utils/manipulation.go ================================================ package utils import "github.com/samber/lo" func EmptyOr[T comparable](v T, def T) T { ret, _ := lo.Coalesce(v, def) return ret } ================================================ FILE: core/Clash.Meta/common/utils/must.go ================================================ package utils func MustOK[T any](result T, ok bool) T { if ok { return result } panic("operation failed") } ================================================ FILE: core/Clash.Meta/common/utils/range.go ================================================ package utils import ( "fmt" "strconv" "strings" "golang.org/x/exp/constraints" ) type Range[T constraints.Ordered] struct { start T end T } func NewRange[T constraints.Ordered](start, end T) Range[T] { if start > end { return Range[T]{ start: end, end: start, } } return Range[T]{ start: start, end: end, } } func (r Range[T]) Contains(t T) bool { return t >= r.start && t <= r.end } func (r Range[T]) LeftContains(t T) bool { return t >= r.start && t < r.end } func (r Range[T]) RightContains(t T) bool { return t > r.start && t <= r.end } func (r Range[T]) Start() T { return r.start } func (r Range[T]) End() T { return r.end } func (r Range[T]) String() string { if r.start == r.end { return fmt.Sprintf("%v", r.start) } return fmt.Sprintf("%v-%v", r.start, r.end) } func NewUnsignedRange[T constraints.Unsigned](expected string) (Range[T], error) { return newIntRange(expected, parseUnsigned[T]) } func NewSignedRange[T constraints.Signed](expected string) (Range[T], error) { return newIntRange(expected, parseSigned[T]) } func newIntRange[T constraints.Integer](s string, parseFn func(string) (T, error)) (Range[T], error) { s = strings.TrimSpace(s) if len(s) == 0 { return NewRange[T](0, 0), nil } status := strings.Split(s, "-") start, err := parseFn(strings.Trim(status[0], "[ ]")) if err != nil { return Range[T]{}, fmt.Errorf("invalid range: %s", s) } switch len(status) { case 1: // Port range return NewRange(start, start), nil case 2: // Single port end, err := parseFn(strings.Trim(status[1], "[ ]")) if err != nil { return Range[T]{}, fmt.Errorf("invalid range: %s", s) } return NewRange(start, end), nil default: return Range[T]{}, fmt.Errorf("invalid range: %s", s) } } func parseUnsigned[T constraints.Unsigned](s string) (T, error) { if val, err := strconv.ParseUint(s, 10, 64); err == nil { return T(val), nil } else { return 0, err } } func parseSigned[T constraints.Signed](s string) (T, error) { if val, err := strconv.ParseInt(s, 10, 64); err == nil { return T(val), nil } else { return 0, err } } ================================================ FILE: core/Clash.Meta/common/utils/ranges.go ================================================ package utils import ( "errors" "fmt" "sort" "strings" "golang.org/x/exp/constraints" ) type IntRanges[T constraints.Integer] []Range[T] var errIntRanges = errors.New("intRanges error") func newIntRanges[T constraints.Integer](expected string, parseFn func(string) (T, error)) (IntRanges[T], error) { // example: 200 or 200/302 or 200-400 or 200/204/401-429/501-503 expected = strings.TrimSpace(expected) if len(expected) == 0 || expected == "*" { return nil, nil } // support: 200,302 or 200,204,401-429,501-503 expected = strings.ReplaceAll(expected, ",", "/") list := strings.Split(expected, "/") if len(list) > 28 { return nil, fmt.Errorf("%w, too many ranges to use, maximum support 28 ranges", errIntRanges) } return newIntRangesFromList[T](list, parseFn) } func newIntRangesFromList[T constraints.Integer](list []string, parseFn func(string) (T, error)) (IntRanges[T], error) { var ranges IntRanges[T] for _, s := range list { if s == "" { continue } r, err := newIntRange[T](s, parseFn) if err != nil { return nil, err } ranges = append(ranges, r) } return ranges, nil } func NewUnsignedRanges[T constraints.Unsigned](expected string) (IntRanges[T], error) { return newIntRanges(expected, parseUnsigned[T]) } func NewUnsignedRangesFromList[T constraints.Unsigned](list []string) (IntRanges[T], error) { return newIntRangesFromList(list, parseUnsigned[T]) } func NewSignedRanges[T constraints.Signed](expected string) (IntRanges[T], error) { return newIntRanges(expected, parseSigned[T]) } func NewSignedRangesFromList[T constraints.Signed](list []string) (IntRanges[T], error) { return newIntRangesFromList(list, parseSigned[T]) } func (ranges IntRanges[T]) Check(status T) bool { if len(ranges) == 0 { return true } for _, segment := range ranges { if segment.Contains(status) { return true } } return false } func (ranges IntRanges[T]) String() string { if len(ranges) == 0 { return "*" } terms := make([]string, len(ranges)) for i, r := range ranges { terms[i] = r.String() } return strings.Join(terms, "/") } func (ranges IntRanges[T]) Range(f func(t T) bool) { if len(ranges) == 0 { return } for _, r := range ranges { for i := r.Start(); i <= r.End() && i >= r.Start(); i++ { if !f(i) { return } if i+1 < i { // integer overflow break } } } } func (ranges IntRanges[T]) Merge() (mergedRanges IntRanges[T]) { if len(ranges) == 0 { return } sort.Slice(ranges, func(i, j int) bool { return ranges[i].Start() < ranges[j].Start() }) mergedRanges = ranges[:1] var rangeIndex int for _, r := range ranges[1:] { if mergedRanges[rangeIndex].End()+1 > mergedRanges[rangeIndex].End() && // integer overflow r.Start() > mergedRanges[rangeIndex].End()+1 { mergedRanges = append(mergedRanges, r) rangeIndex++ } else if r.End() > mergedRanges[rangeIndex].End() { mergedRanges[rangeIndex].end = r.End() } } return } ================================================ FILE: core/Clash.Meta/common/utils/ranges_test.go ================================================ package utils import ( "github.com/stretchr/testify/assert" "testing" ) func TestMergeRanges(t *testing.T) { t.Parallel() for _, testRange := range []struct { ranges IntRanges[uint16] expected IntRanges[uint16] }{ { ranges: IntRanges[uint16]{ NewRange[uint16](0, 1), NewRange[uint16](1, 2), }, expected: IntRanges[uint16]{ NewRange[uint16](0, 2), }, }, { ranges: IntRanges[uint16]{ NewRange[uint16](0, 3), NewRange[uint16](5, 7), NewRange[uint16](8, 9), NewRange[uint16](10, 10), }, expected: IntRanges[uint16]{ NewRange[uint16](0, 3), NewRange[uint16](5, 10), }, }, { ranges: IntRanges[uint16]{ NewRange[uint16](1, 3), NewRange[uint16](2, 6), NewRange[uint16](8, 10), NewRange[uint16](15, 18), }, expected: IntRanges[uint16]{ NewRange[uint16](1, 6), NewRange[uint16](8, 10), NewRange[uint16](15, 18), }, }, { ranges: IntRanges[uint16]{ NewRange[uint16](1, 3), NewRange[uint16](2, 7), NewRange[uint16](2, 6), }, expected: IntRanges[uint16]{ NewRange[uint16](1, 7), }, }, { ranges: IntRanges[uint16]{ NewRange[uint16](1, 3), NewRange[uint16](2, 6), NewRange[uint16](2, 7), }, expected: IntRanges[uint16]{ NewRange[uint16](1, 7), }, }, { ranges: IntRanges[uint16]{ NewRange[uint16](1, 3), NewRange[uint16](2, 65535), NewRange[uint16](2, 7), NewRange[uint16](3, 16), }, expected: IntRanges[uint16]{ NewRange[uint16](1, 65535), }, }, } { assert.Equal(t, testRange.expected, testRange.ranges.Merge()) } } ================================================ FILE: core/Clash.Meta/common/utils/slice.go ================================================ package utils import ( "errors" "fmt" "reflect" ) func Filter[T comparable](tSlice []T, filter func(t T) bool) []T { result := make([]T, 0) for _, t := range tSlice { if filter(t) { result = append(result, t) } } return result } func Map[T any, N any](arr []T, block func(it T) N) []N { if arr == nil { // keep nil return nil } retArr := make([]N, 0, len(arr)) for index := range arr { retArr = append(retArr, block(arr[index])) } return retArr } func ToStringSlice(value any) ([]string, error) { strArr := make([]string, 0) switch reflect.TypeOf(value).Kind() { case reflect.Slice, reflect.Array: origin := reflect.ValueOf(value) for i := 0; i < origin.Len(); i++ { item := fmt.Sprintf("%v", origin.Index(i)) strArr = append(strArr, item) } case reflect.String: strArr = append(strArr, fmt.Sprintf("%v", value)) default: return nil, errors.New("value format error, must be string or array") } return strArr, nil } ================================================ FILE: core/Clash.Meta/common/utils/string_unsafe.go ================================================ package utils import "unsafe" // ImmutableBytesFromString is equivalent to []byte(s), except that it uses the // same memory backing s instead of making a heap-allocated copy. This is only // valid if the returned slice is never mutated. func ImmutableBytesFromString(s string) []byte { b := unsafe.StringData(s) return unsafe.Slice(b, len(s)) } // StringFromImmutableBytes is equivalent to string(bs), except that it uses // the same memory backing bs instead of making a heap-allocated copy. This is // only valid if bs is never mutated after StringFromImmutableBytes returns. func StringFromImmutableBytes(bs []byte) string { if len(bs) == 0 { return "" } return unsafe.String(&bs[0], len(bs)) } ================================================ FILE: core/Clash.Meta/common/utils/strings.go ================================================ package utils func Reverse(s string) string { a := []rune(s) for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] } return string(a) } ================================================ FILE: core/Clash.Meta/common/utils/uuid.go ================================================ package utils import ( "crypto/md5" "crypto/rand" "crypto/sha1" "github.com/gofrs/uuid/v5" ) // NewUUIDV3 returns a UUID based on the MD5 hash of the namespace UUID and name. func NewUUIDV3(ns uuid.UUID, name string) (u uuid.UUID) { h := md5.New() h.Write(ns[:]) h.Write([]byte(name)) copy(u[:], h.Sum(make([]byte, 0, md5.Size))) u.SetVersion(uuid.V3) u.SetVariant(uuid.VariantRFC9562) return u } // NewUUIDV4 returns a new version 4 UUID. // // Version 4 UUIDs contain 122 bits of random data. func NewUUIDV4() (u uuid.UUID) { rand.Read(u[:]) u.SetVersion(uuid.V4) u.SetVariant(uuid.VariantRFC9562) return u } // NewUUIDV5 returns a UUID based on SHA-1 hash of the namespace UUID and name. func NewUUIDV5(ns uuid.UUID, name string) (u uuid.UUID) { h := sha1.New() h.Write(ns[:]) h.Write([]byte(name)) copy(u[:], h.Sum(make([]byte, 0, sha1.Size))) u.SetVersion(uuid.V5) u.SetVariant(uuid.VariantRFC9562) return u } // UUIDMap https://github.com/XTLS/Xray-core/issues/158#issue-783294090 func UUIDMap(str string) uuid.UUID { u, err := uuid.FromString(str) if err != nil { return NewUUIDV5(uuid.Nil, str) } return u } ================================================ FILE: core/Clash.Meta/common/utils/uuid_test.go ================================================ package utils import ( "reflect" "testing" "github.com/gofrs/uuid/v5" ) func TestUUIDMap(t *testing.T) { type args struct { str string } tests := []struct { name string args args want uuid.UUID wantErr bool }{ { name: "uuid-test-1", args: args{ str: "82410302-039e-41b6-98b0-d964084b4170", }, want: uuid.FromStringOrNil("82410302-039e-41b6-98b0-d964084b4170"), }, { name: "uuid-test-2", args: args{ str: "88c502e6-d7eb-4c8e-8259-94cb13d83c77", }, want: uuid.FromStringOrNil("88c502e6-d7eb-4c8e-8259-94cb13d83c77"), }, { name: "uuid-map-1", args: args{ str: "123456", }, want: uuid.FromStringOrNil("f8598425-92f2-5508-a071-4fc67f9040ac"), }, // GENERATED BY 'xray uuid -i' { name: "uuid-map-2", args: args{ str: "a9dk23bz0", }, want: uuid.FromStringOrNil("c91481b6-fc0f-5d9e-b166-5ddf07b9c3c5"), }, { name: "uuid-map-2", args: args{ str: "中文123", }, want: uuid.FromStringOrNil("145c544c-2229-59e5-8dbb-3f33b7610d26"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := UUIDMap(tt.args.str) if !reflect.DeepEqual(got, tt.want) { t.Errorf("UUIDMap() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: core/Clash.Meta/common/xsync/map.go ================================================ package xsync // copy and modified from https://github.com/puzpuzpuz/xsync/blob/v4.2.0/map.go // which is licensed under Apache v2. // // mihomo modified: // 1. restore xsync/v3's LoadOrCompute api and rename to LoadOrStoreFn. // 2. the zero Map is ready for use. import ( "fmt" "math" "math/bits" "runtime" "strings" "sync" "sync/atomic" "unsafe" "github.com/metacubex/mihomo/common/maphash" ) const ( // number of Map entries per bucket; 5 entries lead to size of 64B // (one cache line) on 64-bit machines entriesPerMapBucket = 5 // threshold fraction of table occupation to start a table shrinking // when deleting the last entry in a bucket chain mapShrinkFraction = 128 // map load factor to trigger a table resize during insertion; // a map holds up to mapLoadFactor*entriesPerMapBucket*mapTableLen // key-value pairs (this is a soft limit) mapLoadFactor = 0.75 // minimal table size, i.e. number of buckets; thus, minimal map // capacity can be calculated as entriesPerMapBucket*defaultMinMapTableLen defaultMinMapTableLen = 32 // minimum counter stripes to use minMapCounterLen = 8 // maximum counter stripes to use; stands for around 4KB of memory maxMapCounterLen = 32 defaultMeta uint64 = 0x8080808080808080 metaMask uint64 = 0xffffffffff defaultMetaMasked uint64 = defaultMeta & metaMask emptyMetaSlot uint8 = 0x80 // minimal number of buckets to transfer when participating in cooperative // resize; should be at least defaultMinMapTableLen minResizeTransferStride = 64 // upper limit for max number of additional goroutines that participate // in cooperative resize; must be changed simultaneously with resizeCtl // and the related code maxResizeHelpersLimit = (1 << 5) - 1 ) // max number of additional goroutines that participate in cooperative resize; // "resize owner" goroutine isn't counted var maxResizeHelpers = func() int32 { v := int32(parallelism() - 1) if v < 1 { v = 1 } if v > maxResizeHelpersLimit { v = maxResizeHelpersLimit } return v }() type mapResizeHint int const ( mapGrowHint mapResizeHint = 0 mapShrinkHint mapResizeHint = 1 mapClearHint mapResizeHint = 2 ) type ComputeOp int const ( // CancelOp signals to Compute to not do anything as a result // of executing the lambda. If the entry was not present in // the map, nothing happens, and if it was present, the // returned value is ignored. CancelOp ComputeOp = iota // UpdateOp signals to Compute to update the entry to the // value returned by the lambda, creating it if necessary. UpdateOp // DeleteOp signals to Compute to always delete the entry // from the map. DeleteOp ) type loadOp int const ( noLoadOp loadOp = iota loadOrComputeOp loadAndDeleteOp ) // Map is like a Go map[K]V but is safe for concurrent // use by multiple goroutines without additional locking or // coordination. It follows the interface of sync.Map with // a number of valuable extensions like Compute or Size. // // A Map must not be copied after first use. // // Map uses a modified version of Cache-Line Hash Table (CLHT) // data structure: https://github.com/LPD-EPFL/CLHT // // CLHT is built around idea to organize the hash table in // cache-line-sized buckets, so that on all modern CPUs update // operations complete with at most one cache-line transfer. // Also, Get operations involve no write to memory, as well as no // mutexes or any other sort of locks. Due to this design, in all // considered scenarios Map outperforms sync.Map. // // Map also borrows ideas from Java's j.u.c.ConcurrentHashMap // (immutable K/V pair structs instead of atomic snapshots) // and C++'s absl::flat_hash_map (meta memory and SWAR-based // lookups). type Map[K comparable, V any] struct { initOnce sync.Once totalGrowths atomic.Int64 totalShrinks atomic.Int64 table atomic.Pointer[mapTable[K, V]] // table being transferred to nextTable atomic.Pointer[mapTable[K, V]] // resize control state: combines resize sequence number (upper 59 bits) and // the current number of resize helpers (lower 5 bits); // odd values of resize sequence mean in-progress resize resizeCtl atomic.Uint64 // only used along with resizeCond resizeMu sync.Mutex // used to wake up resize waiters (concurrent writes) resizeCond sync.Cond // transfer progress index for resize resizeIdx atomic.Int64 minTableLen int growOnly bool } type mapTable[K comparable, V any] struct { buckets []bucketPadded // striped counter for number of table entries; // used to determine if a table shrinking is needed // occupies min(buckets_memory/1024, 64KB) of memory size []counterStripe seed maphash.Seed } type counterStripe struct { c int64 // Padding to prevent false sharing. _ [cacheLineSize - 8]byte } // bucketPadded is a CL-sized map bucket holding up to // entriesPerMapBucket entries. type bucketPadded struct { //lint:ignore U1000 ensure each bucket takes two cache lines on both 32 and 64-bit archs pad [cacheLineSize - unsafe.Sizeof(bucket{})]byte bucket } type bucket struct { meta uint64 entries [entriesPerMapBucket]unsafe.Pointer // *entry next unsafe.Pointer // *bucketPadded mu sync.Mutex } // entry is an immutable map entry. type entry[K comparable, V any] struct { key K value V } // MapConfig defines configurable Map options. type MapConfig struct { sizeHint int growOnly bool } // WithPresize configures new Map instance with capacity enough // to hold sizeHint entries. The capacity is treated as the minimal // capacity meaning that the underlying hash table will never shrink // to a smaller capacity. If sizeHint is zero or negative, the value // is ignored. func WithPresize(sizeHint int) func(*MapConfig) { return func(c *MapConfig) { c.sizeHint = sizeHint } } // WithGrowOnly configures new Map instance to be grow-only. // This means that the underlying hash table grows in capacity when // new keys are added, but does not shrink when keys are deleted. // The only exception to this rule is the Clear method which // shrinks the hash table back to the initial capacity. func WithGrowOnly() func(*MapConfig) { return func(c *MapConfig) { c.growOnly = true } } // NewMap creates a new Map instance configured with the given // options. func NewMap[K comparable, V any](options ...func(*MapConfig)) *Map[K, V] { c := &MapConfig{} for _, o := range options { o(c) } m := &Map[K, V]{} if c.sizeHint > defaultMinMapTableLen*entriesPerMapBucket { tableLen := nextPowOf2(uint32((float64(c.sizeHint) / entriesPerMapBucket) / mapLoadFactor)) m.minTableLen = int(tableLen) } m.growOnly = c.growOnly return m } func (m *Map[K, V]) init() { if m.minTableLen == 0 { m.minTableLen = defaultMinMapTableLen } m.resizeCond = *sync.NewCond(&m.resizeMu) table := newMapTable[K, V](m.minTableLen, maphash.MakeSeed()) m.minTableLen = len(table.buckets) m.table.Store(table) } func newMapTable[K comparable, V any](minTableLen int, seed maphash.Seed) *mapTable[K, V] { buckets := make([]bucketPadded, minTableLen) for i := range buckets { buckets[i].meta = defaultMeta } counterLen := minTableLen >> 10 if counterLen < minMapCounterLen { counterLen = minMapCounterLen } else if counterLen > maxMapCounterLen { counterLen = maxMapCounterLen } counter := make([]counterStripe, counterLen) t := &mapTable[K, V]{ buckets: buckets, size: counter, seed: seed, } return t } // ToPlainMap returns a native map with a copy of xsync Map's // contents. The copied xsync Map should not be modified while // this call is made. If the copied Map is modified, the copying // behavior is the same as in the Range method. func ToPlainMap[K comparable, V any](m *Map[K, V]) map[K]V { pm := make(map[K]V) if m != nil { m.Range(func(key K, value V) bool { pm[key] = value return true }) } return pm } // Load returns the value stored in the map for a key, or zero value // of type V if no value is present. // The ok result indicates whether value was found in the map. func (m *Map[K, V]) Load(key K) (value V, ok bool) { m.initOnce.Do(m.init) table := m.table.Load() hash := maphash.Comparable(table.seed, key) h1 := h1(hash) h2w := broadcast(h2(hash)) bidx := uint64(len(table.buckets)-1) & h1 b := &table.buckets[bidx] for { metaw := atomic.LoadUint64(&b.meta) markedw := markZeroBytes(metaw^h2w) & metaMask for markedw != 0 { idx := firstMarkedByteIndex(markedw) eptr := atomic.LoadPointer(&b.entries[idx]) if eptr != nil { e := (*entry[K, V])(eptr) if e.key == key { return e.value, true } } markedw &= markedw - 1 } bptr := atomic.LoadPointer(&b.next) if bptr == nil { return } b = (*bucketPadded)(bptr) } } // Store sets the value for a key. func (m *Map[K, V]) Store(key K, value V) { m.doCompute( key, func(V, bool) (V, ComputeOp) { return value, UpdateOp }, noLoadOp, false, ) } // LoadOrStore returns the existing value for the key if present. // Otherwise, it stores and returns the given value. // The loaded result is true if the value was loaded, false if stored. func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { return m.doCompute( key, func(oldValue V, loaded bool) (V, ComputeOp) { if loaded { return oldValue, CancelOp } return value, UpdateOp }, loadOrComputeOp, false, ) } // LoadAndStore returns the existing value for the key if present, // while setting the new value for the key. // It stores the new value and returns the existing one, if present. // The loaded result is true if the existing value was loaded, // false otherwise. func (m *Map[K, V]) LoadAndStore(key K, value V) (actual V, loaded bool) { return m.doCompute( key, func(V, bool) (V, ComputeOp) { return value, UpdateOp }, noLoadOp, false, ) } // LoadOrCompute returns the existing value for the key if // present. Otherwise, it tries to compute the value using the // provided function and, if successful, stores and returns // the computed value. The loaded result is true if the value was // loaded, or false if computed. If valueFn returns true as the // cancel value, the computation is cancelled and the zero value // for type V is returned. // // This call locks a hash table bucket while the compute function // is executed. It means that modifications on other entries in // the bucket will be blocked until the valueFn executes. Consider // this when the function includes long-running operations. func (m *Map[K, V]) LoadOrCompute( key K, valueFn func() (newValue V, cancel bool), ) (value V, loaded bool) { return m.doCompute( key, func(oldValue V, loaded bool) (V, ComputeOp) { if loaded { return oldValue, CancelOp } newValue, c := valueFn() if !c { return newValue, UpdateOp } return oldValue, CancelOp }, loadOrComputeOp, false, ) } // Compute either sets the computed new value for the key, // deletes the value for the key, or does nothing, based on // the returned [ComputeOp]. When the op returned by valueFn // is [UpdateOp], the value is updated to the new value. If // it is [DeleteOp], the entry is removed from the map // altogether. And finally, if the op is [CancelOp] then the // entry is left as-is. In other words, if it did not already // exist, it is not created, and if it did exist, it is not // updated. This is useful to synchronously execute some // operation on the value without incurring the cost of // updating the map every time. The ok result indicates // whether the entry is present in the map after the compute // operation. The actual result contains the value of the map // if a corresponding entry is present, or the zero value // otherwise. See the example for a few use cases. // // This call locks a hash table bucket while the compute function // is executed. It means that modifications on other entries in // the bucket will be blocked until the valueFn executes. Consider // this when the function includes long-running operations. func (m *Map[K, V]) Compute( key K, valueFn func(oldValue V, loaded bool) (newValue V, op ComputeOp), ) (actual V, ok bool) { return m.doCompute(key, valueFn, noLoadOp, true) } // LoadAndDelete deletes the value for a key, returning the previous // value if any. The loaded result reports whether the key was // present. func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) { return m.doCompute( key, func(value V, loaded bool) (V, ComputeOp) { return value, DeleteOp }, loadAndDeleteOp, false, ) } // Delete deletes the value for a key. func (m *Map[K, V]) Delete(key K) { m.LoadAndDelete(key) } func (m *Map[K, V]) doCompute( key K, valueFn func(oldValue V, loaded bool) (V, ComputeOp), loadOp loadOp, computeOnly bool, ) (V, bool) { m.initOnce.Do(m.init) for { compute_attempt: var ( emptyb *bucketPadded emptyidx int ) table := m.table.Load() tableLen := len(table.buckets) hash := maphash.Comparable(table.seed, key) h1 := h1(hash) h2 := h2(hash) h2w := broadcast(h2) bidx := uint64(len(table.buckets)-1) & h1 rootb := &table.buckets[bidx] if loadOp != noLoadOp { b := rootb load: for { metaw := atomic.LoadUint64(&b.meta) markedw := markZeroBytes(metaw^h2w) & metaMask for markedw != 0 { idx := firstMarkedByteIndex(markedw) eptr := atomic.LoadPointer(&b.entries[idx]) if eptr != nil { e := (*entry[K, V])(eptr) if e.key == key { if loadOp == loadOrComputeOp { return e.value, true } break load } } markedw &= markedw - 1 } bptr := atomic.LoadPointer(&b.next) if bptr == nil { if loadOp == loadAndDeleteOp { return *new(V), false } break load } b = (*bucketPadded)(bptr) } } rootb.mu.Lock() // The following two checks must go in reverse to what's // in the resize method. if seq := resizeSeq(m.resizeCtl.Load()); seq&1 == 1 { // Resize is in progress. Help with the transfer, then go for another attempt. rootb.mu.Unlock() m.helpResize(seq) goto compute_attempt } if m.newerTableExists(table) { // Someone resized the table. Go for another attempt. rootb.mu.Unlock() goto compute_attempt } b := rootb for { metaw := b.meta markedw := markZeroBytes(metaw^h2w) & metaMask for markedw != 0 { idx := firstMarkedByteIndex(markedw) eptr := b.entries[idx] if eptr != nil { e := (*entry[K, V])(eptr) if e.key == key { // In-place update/delete. // We get a copy of the value via an interface{} on each call, // thus the live value pointers are unique. Otherwise atomic // snapshot won't be correct in case of multiple Store calls // using the same value. oldv := e.value newv, op := valueFn(oldv, true) switch op { case DeleteOp: // Deletion. // First we update the hash, then the entry. newmetaw := setByte(metaw, emptyMetaSlot, idx) atomic.StoreUint64(&b.meta, newmetaw) atomic.StorePointer(&b.entries[idx], nil) rootb.mu.Unlock() table.addSize(bidx, -1) // Might need to shrink the table if we left bucket empty. if newmetaw == defaultMeta { m.resize(table, mapShrinkHint) } return oldv, !computeOnly case UpdateOp: newe := new(entry[K, V]) newe.key = key newe.value = newv atomic.StorePointer(&b.entries[idx], unsafe.Pointer(newe)) case CancelOp: newv = oldv } rootb.mu.Unlock() if computeOnly { // Compute expects the new value to be returned. return newv, true } // LoadAndStore expects the old value to be returned. return oldv, true } } markedw &= markedw - 1 } if emptyb == nil { // Search for empty entries (up to 5 per bucket). emptyw := metaw & defaultMetaMasked if emptyw != 0 { idx := firstMarkedByteIndex(emptyw) emptyb = b emptyidx = idx } } if b.next == nil { if emptyb != nil { // Insertion into an existing bucket. var zeroV V newValue, op := valueFn(zeroV, false) switch op { case DeleteOp, CancelOp: rootb.mu.Unlock() return zeroV, false default: newe := new(entry[K, V]) newe.key = key newe.value = newValue // First we update meta, then the entry. atomic.StoreUint64(&emptyb.meta, setByte(emptyb.meta, h2, emptyidx)) atomic.StorePointer(&emptyb.entries[emptyidx], unsafe.Pointer(newe)) rootb.mu.Unlock() table.addSize(bidx, 1) return newValue, computeOnly } } growThreshold := float64(tableLen) * entriesPerMapBucket * mapLoadFactor if table.sumSize() > int64(growThreshold) { // Need to grow the table. Then go for another attempt. rootb.mu.Unlock() m.resize(table, mapGrowHint) goto compute_attempt } // Insertion into a new bucket. var zeroV V newValue, op := valueFn(zeroV, false) switch op { case DeleteOp, CancelOp: rootb.mu.Unlock() return newValue, false default: // Create and append a bucket. newb := new(bucketPadded) newb.meta = setByte(defaultMeta, h2, 0) newe := new(entry[K, V]) newe.key = key newe.value = newValue newb.entries[0] = unsafe.Pointer(newe) atomic.StorePointer(&b.next, unsafe.Pointer(newb)) rootb.mu.Unlock() table.addSize(bidx, 1) return newValue, computeOnly } } b = (*bucketPadded)(b.next) } } } func (m *Map[K, V]) newerTableExists(table *mapTable[K, V]) bool { return table != m.table.Load() } func resizeSeq(ctl uint64) uint64 { return ctl >> 5 } func resizeHelpers(ctl uint64) uint64 { return ctl & maxResizeHelpersLimit } func resizeCtl(seq uint64, helpers uint64) uint64 { return (seq << 5) | (helpers & maxResizeHelpersLimit) } func (m *Map[K, V]) waitForResize() { m.resizeMu.Lock() for resizeSeq(m.resizeCtl.Load())&1 == 1 { m.resizeCond.Wait() } m.resizeMu.Unlock() } func (m *Map[K, V]) resize(knownTable *mapTable[K, V], hint mapResizeHint) { knownTableLen := len(knownTable.buckets) // Fast path for shrink attempts. if hint == mapShrinkHint { if m.growOnly || m.minTableLen == knownTableLen || knownTable.sumSize() > int64((knownTableLen*entriesPerMapBucket)/mapShrinkFraction) { return } } // Slow path. seq := resizeSeq(m.resizeCtl.Load()) if seq&1 == 1 || !m.resizeCtl.CompareAndSwap(resizeCtl(seq, 0), resizeCtl(seq+1, 0)) { m.helpResize(seq) return } var newTable *mapTable[K, V] table := m.table.Load() tableLen := len(table.buckets) switch hint { case mapGrowHint: // Grow the table with factor of 2. // We must keep the same table seed here to keep the same hash codes // allowing us to avoid locking destination buckets when resizing. m.totalGrowths.Add(1) newTable = newMapTable[K, V](tableLen<<1, table.seed) case mapShrinkHint: shrinkThreshold := int64((tableLen * entriesPerMapBucket) / mapShrinkFraction) if tableLen > m.minTableLen && table.sumSize() <= shrinkThreshold { // Shrink the table with factor of 2. // It's fine to generate a new seed since full locking // is required anyway. m.totalShrinks.Add(1) newTable = newMapTable[K, V](tableLen>>1, maphash.MakeSeed()) } else { // No need to shrink. Wake up all waiters and give up. m.resizeMu.Lock() m.resizeCtl.Store(resizeCtl(seq+2, 0)) m.resizeCond.Broadcast() m.resizeMu.Unlock() return } case mapClearHint: newTable = newMapTable[K, V](m.minTableLen, maphash.MakeSeed()) default: panic(fmt.Sprintf("unexpected resize hint: %d", hint)) } // Copy the data only if we're not clearing the map. if hint != mapClearHint { // Set up cooperative transfer state. // Next table must be published as the last step. m.resizeIdx.Store(0) m.nextTable.Store(newTable) // Copy the buckets. m.transfer(table, newTable) } // We're about to publish the new table, but before that // we must wait for all helpers to finish. for resizeHelpers(m.resizeCtl.Load()) != 0 { runtime.Gosched() } m.table.Store(newTable) m.nextTable.Store(nil) ctl := resizeCtl(seq+1, 0) newCtl := resizeCtl(seq+2, 0) // Increment the sequence number and wake up all waiters. m.resizeMu.Lock() // There may be slowpoke helpers who have just incremented // the helper counter. This CAS loop makes sure to wait // for them to back off. for !m.resizeCtl.CompareAndSwap(ctl, newCtl) { runtime.Gosched() } m.resizeCond.Broadcast() m.resizeMu.Unlock() } func (m *Map[K, V]) helpResize(seq uint64) { for { table := m.table.Load() nextTable := m.nextTable.Load() if resizeSeq(m.resizeCtl.Load()) == seq { if nextTable == nil || nextTable == table { // Carry on until the next table is set by the main // resize goroutine or until the resize finishes. runtime.Gosched() continue } // The resize is still in-progress, so let's try registering // as a helper. for { ctl := m.resizeCtl.Load() if resizeSeq(ctl) != seq || resizeHelpers(ctl) >= uint64(maxResizeHelpers) { // The resize has ended or there are too many helpers. break } if m.resizeCtl.CompareAndSwap(ctl, ctl+1) { // Yay, we're a resize helper! m.transfer(table, nextTable) // Don't forget to unregister as a helper. m.resizeCtl.Add(^uint64(0)) break } } m.waitForResize() } break } } func (m *Map[K, V]) transfer(table, newTable *mapTable[K, V]) { tableLen := len(table.buckets) newTableLen := len(newTable.buckets) stride := (tableLen >> 3) / int(maxResizeHelpers) if stride < minResizeTransferStride { stride = minResizeTransferStride } for { // Claim work by incrementing resizeIdx. nextIdx := m.resizeIdx.Add(int64(stride)) start := int(nextIdx) - stride if start < 0 { start = 0 } if start > tableLen { break } end := int(nextIdx) if end > tableLen { end = tableLen } // Transfer buckets in this range. total := 0 if newTableLen > tableLen { // We're growing the table with 2x multiplier, so entries from a N bucket can // only be transferred to N and 2*N buckets in the new table. Thus, destination // buckets written by the resize helpers don't intersect, so we don't need to // acquire locks in the destination buckets. for i := start; i < end; i++ { total += transferBucketUnsafe(&table.buckets[i], newTable) } } else { // We're shrinking the table, so all locks must be acquired. for i := start; i < end; i++ { total += transferBucket(&table.buckets[i], newTable) } } // The exact counter stripe doesn't matter here, so pick up the one // that corresponds to the start value to avoid contention. newTable.addSize(uint64(start), total) } } // Doesn't acquire dest bucket lock. func transferBucketUnsafe[K comparable, V any]( b *bucketPadded, destTable *mapTable[K, V], ) (copied int) { rootb := b rootb.mu.Lock() for { for i := 0; i < entriesPerMapBucket; i++ { if eptr := b.entries[i]; eptr != nil { e := (*entry[K, V])(eptr) hash := maphash.Comparable(destTable.seed, e.key) bidx := uint64(len(destTable.buckets)-1) & h1(hash) destb := &destTable.buckets[bidx] appendToBucket(h2(hash), e, destb) copied++ } } if b.next == nil { rootb.mu.Unlock() return } b = (*bucketPadded)(b.next) } } func transferBucket[K comparable, V any]( b *bucketPadded, destTable *mapTable[K, V], ) (copied int) { rootb := b rootb.mu.Lock() for { for i := 0; i < entriesPerMapBucket; i++ { if eptr := b.entries[i]; eptr != nil { e := (*entry[K, V])(eptr) hash := maphash.Comparable(destTable.seed, e.key) bidx := uint64(len(destTable.buckets)-1) & h1(hash) destb := &destTable.buckets[bidx] destb.mu.Lock() appendToBucket(h2(hash), e, destb) destb.mu.Unlock() copied++ } } if b.next == nil { rootb.mu.Unlock() return } b = (*bucketPadded)(b.next) } } // Range calls f sequentially for each key and value present in the // map. If f returns false, range stops the iteration. // // Range does not necessarily correspond to any consistent snapshot // of the Map's contents: no key will be visited more than once, but // if the value for any key is stored or deleted concurrently, Range // may reflect any mapping for that key from any point during the // Range call. // // It is safe to modify the map while iterating it, including entry // creation, modification and deletion. However, the concurrent // modification rule apply, i.e. the changes may be not reflected // in the subsequently iterated entries. func (m *Map[K, V]) Range(f func(key K, value V) bool) { m.initOnce.Do(m.init) // Pre-allocate array big enough to fit entries for most hash tables. bentries := make([]*entry[K, V], 0, 16*entriesPerMapBucket) table := m.table.Load() for i := range table.buckets { rootb := &table.buckets[i] b := rootb // Prevent concurrent modifications and copy all entries into // the intermediate slice. rootb.mu.Lock() for { for i := 0; i < entriesPerMapBucket; i++ { if b.entries[i] != nil { bentries = append(bentries, (*entry[K, V])(b.entries[i])) } } if b.next == nil { rootb.mu.Unlock() break } b = (*bucketPadded)(b.next) } // Call the function for all copied entries. for j, e := range bentries { if !f(e.key, e.value) { return } // Remove the reference to avoid preventing the copied // entries from being GCed until this method finishes. bentries[j] = nil } bentries = bentries[:0] } } // Clear deletes all keys and values currently stored in the map. func (m *Map[K, V]) Clear() { m.initOnce.Do(m.init) m.resize(m.table.Load(), mapClearHint) } // Size returns current size of the map. func (m *Map[K, V]) Size() int { m.initOnce.Do(m.init) return int(m.table.Load().sumSize()) } // It is safe to use plain stores here because the destination bucket must be // either locked or exclusively written to by the helper during resize. func appendToBucket[K comparable, V any](h2 uint8, e *entry[K, V], b *bucketPadded) { for { for i := 0; i < entriesPerMapBucket; i++ { if b.entries[i] == nil { b.meta = setByte(b.meta, h2, i) b.entries[i] = unsafe.Pointer(e) return } } if b.next == nil { newb := new(bucketPadded) newb.meta = setByte(defaultMeta, h2, 0) newb.entries[0] = unsafe.Pointer(e) b.next = unsafe.Pointer(newb) return } b = (*bucketPadded)(b.next) } } func (table *mapTable[K, V]) addSize(bucketIdx uint64, delta int) { cidx := uint64(len(table.size)-1) & bucketIdx atomic.AddInt64(&table.size[cidx].c, int64(delta)) } func (table *mapTable[K, V]) sumSize() int64 { sum := int64(0) for i := range table.size { sum += atomic.LoadInt64(&table.size[i].c) } return sum } func h1(h uint64) uint64 { return h >> 7 } func h2(h uint64) uint8 { return uint8(h & 0x7f) } // MapStats is Map statistics. // // Warning: map statistics are intented to be used for diagnostic // purposes, not for production code. This means that breaking changes // may be introduced into this struct even between minor releases. type MapStats struct { // RootBuckets is the number of root buckets in the hash table. // Each bucket holds a few entries. RootBuckets int // TotalBuckets is the total number of buckets in the hash table, // including root and their chained buckets. Each bucket holds // a few entries. TotalBuckets int // EmptyBuckets is the number of buckets that hold no entries. EmptyBuckets int // Capacity is the Map capacity, i.e. the total number of // entries that all buckets can physically hold. This number // does not consider the load factor. Capacity int // Size is the exact number of entries stored in the map. Size int // Counter is the number of entries stored in the map according // to the internal atomic counter. In case of concurrent map // modifications this number may be different from Size. Counter int // CounterLen is the number of internal atomic counter stripes. // This number may grow with the map capacity to improve // multithreaded scalability. CounterLen int // MinEntries is the minimum number of entries per a chain of // buckets, i.e. a root bucket and its chained buckets. MinEntries int // MinEntries is the maximum number of entries per a chain of // buckets, i.e. a root bucket and its chained buckets. MaxEntries int // TotalGrowths is the number of times the hash table grew. TotalGrowths int64 // TotalGrowths is the number of times the hash table shrinked. TotalShrinks int64 } // ToString returns string representation of map stats. func (s *MapStats) ToString() string { var sb strings.Builder sb.WriteString("MapStats{\n") sb.WriteString(fmt.Sprintf("RootBuckets: %d\n", s.RootBuckets)) sb.WriteString(fmt.Sprintf("TotalBuckets: %d\n", s.TotalBuckets)) sb.WriteString(fmt.Sprintf("EmptyBuckets: %d\n", s.EmptyBuckets)) sb.WriteString(fmt.Sprintf("Capacity: %d\n", s.Capacity)) sb.WriteString(fmt.Sprintf("Size: %d\n", s.Size)) sb.WriteString(fmt.Sprintf("Counter: %d\n", s.Counter)) sb.WriteString(fmt.Sprintf("CounterLen: %d\n", s.CounterLen)) sb.WriteString(fmt.Sprintf("MinEntries: %d\n", s.MinEntries)) sb.WriteString(fmt.Sprintf("MaxEntries: %d\n", s.MaxEntries)) sb.WriteString(fmt.Sprintf("TotalGrowths: %d\n", s.TotalGrowths)) sb.WriteString(fmt.Sprintf("TotalShrinks: %d\n", s.TotalShrinks)) sb.WriteString("}\n") return sb.String() } // Stats returns statistics for the Map. Just like other map // methods, this one is thread-safe. Yet it's an O(N) operation, // so it should be used only for diagnostics or debugging purposes. func (m *Map[K, V]) Stats() MapStats { m.initOnce.Do(m.init) stats := MapStats{ TotalGrowths: m.totalGrowths.Load(), TotalShrinks: m.totalShrinks.Load(), MinEntries: math.MaxInt32, } table := m.table.Load() stats.RootBuckets = len(table.buckets) stats.Counter = int(table.sumSize()) stats.CounterLen = len(table.size) for i := range table.buckets { nentries := 0 b := &table.buckets[i] stats.TotalBuckets++ for { nentriesLocal := 0 stats.Capacity += entriesPerMapBucket for i := 0; i < entriesPerMapBucket; i++ { if atomic.LoadPointer(&b.entries[i]) != nil { stats.Size++ nentriesLocal++ } } nentries += nentriesLocal if nentriesLocal == 0 { stats.EmptyBuckets++ } if b.next == nil { break } b = (*bucketPadded)(atomic.LoadPointer(&b.next)) stats.TotalBuckets++ } if nentries < stats.MinEntries { stats.MinEntries = nentries } if nentries > stats.MaxEntries { stats.MaxEntries = nentries } } return stats } const ( // cacheLineSize is used in paddings to prevent false sharing; // 64B are used instead of 128B as a compromise between // memory footprint and performance; 128B usage may give ~30% // improvement on NUMA machines. cacheLineSize = 64 ) // nextPowOf2 computes the next highest power of 2 of 32-bit v. // Source: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 func nextPowOf2(v uint32) uint32 { if v == 0 { return 1 } v-- v |= v >> 1 v |= v >> 2 v |= v >> 4 v |= v >> 8 v |= v >> 16 v++ return v } func parallelism() uint32 { maxProcs := uint32(runtime.GOMAXPROCS(0)) numCores := uint32(runtime.NumCPU()) if maxProcs < numCores { return maxProcs } return numCores } func broadcast(b uint8) uint64 { return 0x101010101010101 * uint64(b) } func firstMarkedByteIndex(w uint64) int { return bits.TrailingZeros64(w) >> 3 } // SWAR byte search: may produce false positives, e.g. for 0x0100, // so make sure to double-check bytes found by this function. func markZeroBytes(w uint64) uint64 { return ((w - 0x0101010101010101) & (^w) & 0x8080808080808080) } // Sets byte of the input word at the specified index to the given value. func setByte(w uint64, b uint8, idx int) uint64 { shift := idx << 3 return (w &^ (0xff << shift)) | (uint64(b) << shift) } ================================================ FILE: core/Clash.Meta/common/xsync/map_extra.go ================================================ package xsync // LoadOrStoreFn returns the existing value for the key if // present. Otherwise, it tries to compute the value using the // provided function and, if successful, stores and returns // the computed value. The loaded result is true if the value was // loaded, or false if computed. // // This call locks a hash table bucket while the compute function // is executed. It means that modifications on other entries in // the bucket will be blocked until the valueFn executes. Consider // this when the function includes long-running operations. // // Recovery this API and renamed from xsync/v3's LoadOrCompute. // We unneeded support no-op (cancel) compute operation, it will only add complexity to existing code. func (m *Map[K, V]) LoadOrStoreFn(key K, valueFn func() V) (actual V, loaded bool) { return m.doCompute( key, func(oldValue V, loaded bool) (V, ComputeOp) { if loaded { return oldValue, CancelOp } return valueFn(), UpdateOp }, loadOrComputeOp, false, ) } ================================================ FILE: core/Clash.Meta/common/xsync/map_extra_test.go ================================================ package xsync import ( "strconv" "testing" ) func TestMapOfLoadOrStoreFn(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { v, loaded := m.LoadOrStoreFn(strconv.Itoa(i), func() int { return i }) if loaded { t.Fatalf("value not computed for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } for i := 0; i < numEntries; i++ { v, loaded := m.LoadOrStoreFn(strconv.Itoa(i), func() int { return i }) if !loaded { t.Fatalf("value not loaded for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } } func TestMapOfLoadOrStoreFn_FunctionCalledOnce(t *testing.T) { m := NewMap[int, int]() for i := 0; i < 100; { m.LoadOrStoreFn(i, func() (v int) { v, i = i, i+1 return v }) } m.Range(func(k, v int) bool { if k != v { t.Fatalf("%dth key is not equal to value %d", k, v) } return true }) } ================================================ FILE: core/Clash.Meta/common/xsync/map_test.go ================================================ package xsync import ( "math" "math/rand" "runtime" "strconv" "sync" "sync/atomic" "testing" "time" "unsafe" "github.com/metacubex/randv2" ) const ( // number of entries to use in benchmarks benchmarkNumEntries = 1_000 // key prefix used in benchmarks benchmarkKeyPrefix = "what_a_looooooooooooooooooooooong_key_prefix_" ) type point struct { x int32 y int32 } var benchmarkCases = []struct { name string readPercentage int }{ {"reads=100%", 100}, // 100% loads, 0% stores, 0% deletes {"reads=99%", 99}, // 99% loads, 0.5% stores, 0.5% deletes {"reads=90%", 90}, // 90% loads, 5% stores, 5% deletes {"reads=75%", 75}, // 75% loads, 12.5% stores, 12.5% deletes } var benchmarkKeys []string func init() { benchmarkKeys = make([]string, benchmarkNumEntries) for i := 0; i < benchmarkNumEntries; i++ { benchmarkKeys[i] = benchmarkKeyPrefix + strconv.Itoa(i) } } func runParallel(b *testing.B, benchFn func(pb *testing.PB)) { b.ResetTimer() start := time.Now() b.RunParallel(benchFn) opsPerSec := float64(b.N) / float64(time.Since(start).Seconds()) b.ReportMetric(opsPerSec, "ops/s") } func TestMap_BucketStructSize(t *testing.T) { size := unsafe.Sizeof(bucketPadded{}) if size != 64 { t.Fatalf("size of 64B (one cache line) is expected, got: %d", size) } size = unsafe.Sizeof(bucketPadded{}) if size != 64 { t.Fatalf("size of 64B (one cache line) is expected, got: %d", size) } } func TestMap_MissingEntry(t *testing.T) { m := NewMap[string, string]() v, ok := m.Load("foo") if ok { t.Fatalf("value was not expected: %v", v) } if deleted, loaded := m.LoadAndDelete("foo"); loaded { t.Fatalf("value was not expected %v", deleted) } if actual, loaded := m.LoadOrStore("foo", "bar"); loaded { t.Fatalf("value was not expected %v", actual) } } func TestMap_EmptyStringKey(t *testing.T) { m := NewMap[string, string]() m.Store("", "foobar") v, ok := m.Load("") if !ok { t.Fatal("value was expected") } if v != "foobar" { t.Fatalf("value does not match: %v", v) } } func TestMapStore_NilValue(t *testing.T) { m := NewMap[string, *struct{}]() m.Store("foo", nil) v, ok := m.Load("foo") if !ok { t.Fatal("nil value was expected") } if v != nil { t.Fatalf("value was not nil: %v", v) } } func TestMapLoadOrStore_NilValue(t *testing.T) { m := NewMap[string, *struct{}]() m.LoadOrStore("foo", nil) v, loaded := m.LoadOrStore("foo", nil) if !loaded { t.Fatal("nil value was expected") } if v != nil { t.Fatalf("value was not nil: %v", v) } } func TestMapLoadOrStore_NonNilValue(t *testing.T) { type foo struct{} m := NewMap[string, *foo]() newv := &foo{} v, loaded := m.LoadOrStore("foo", newv) if loaded { t.Fatal("no value was expected") } if v != newv { t.Fatalf("value does not match: %v", v) } newv2 := &foo{} v, loaded = m.LoadOrStore("foo", newv2) if !loaded { t.Fatal("value was expected") } if v != newv { t.Fatalf("value does not match: %v", v) } } func TestMapLoadAndStore_NilValue(t *testing.T) { m := NewMap[string, *struct{}]() m.LoadAndStore("foo", nil) v, loaded := m.LoadAndStore("foo", nil) if !loaded { t.Fatal("nil value was expected") } if v != nil { t.Fatalf("value was not nil: %v", v) } v, loaded = m.Load("foo") if !loaded { t.Fatal("nil value was expected") } if v != nil { t.Fatalf("value was not nil: %v", v) } } func TestMapLoadAndStore_NonNilValue(t *testing.T) { m := NewMap[string, int]() v1 := 1 v, loaded := m.LoadAndStore("foo", v1) if loaded { t.Fatal("no value was expected") } if v != v1 { t.Fatalf("value does not match: %v", v) } v2 := 2 v, loaded = m.LoadAndStore("foo", v2) if !loaded { t.Fatal("value was expected") } if v != v1 { t.Fatalf("value does not match: %v", v) } v, loaded = m.Load("foo") if !loaded { t.Fatal("value was expected") } if v != v2 { t.Fatalf("value does not match: %v", v) } } func TestMapRange(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } iters := 0 met := make(map[string]int) m.Range(func(key string, value int) bool { if key != strconv.Itoa(value) { t.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, key, value) return false } met[key] += 1 iters++ return true }) if iters != numEntries { t.Fatalf("got unexpected number of iterations: %d", iters) } for i := 0; i < numEntries; i++ { if c := met[strconv.Itoa(i)]; c != 1 { t.Fatalf("range did not iterate correctly over %d: %d", i, c) } } } func TestMapRange_FalseReturned(t *testing.T) { m := NewMap[string, int]() for i := 0; i < 100; i++ { m.Store(strconv.Itoa(i), i) } iters := 0 m.Range(func(key string, value int) bool { iters++ return iters != 13 }) if iters != 13 { t.Fatalf("got unexpected number of iterations: %d", iters) } } func TestMapRange_NestedDelete(t *testing.T) { const numEntries = 256 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } m.Range(func(key string, value int) bool { m.Delete(key) return true }) for i := 0; i < numEntries; i++ { if _, ok := m.Load(strconv.Itoa(i)); ok { t.Fatalf("value found for %d", i) } } } func TestMapStringStore(t *testing.T) { const numEntries = 128 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } for i := 0; i < numEntries; i++ { v, ok := m.Load(strconv.Itoa(i)) if !ok { t.Fatalf("value not found for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } } func TestMapIntStore(t *testing.T) { const numEntries = 128 m := NewMap[int, int]() for i := 0; i < numEntries; i++ { m.Store(i, i) } for i := 0; i < numEntries; i++ { v, ok := m.Load(i) if !ok { t.Fatalf("value not found for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } } func TestMapStore_StructKeys_IntValues(t *testing.T) { const numEntries = 128 m := NewMap[point, int]() for i := 0; i < numEntries; i++ { m.Store(point{int32(i), -int32(i)}, i) } for i := 0; i < numEntries; i++ { v, ok := m.Load(point{int32(i), -int32(i)}) if !ok { t.Fatalf("value not found for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } } func TestMapStore_StructKeys_StructValues(t *testing.T) { const numEntries = 128 m := NewMap[point, point]() for i := 0; i < numEntries; i++ { m.Store(point{int32(i), -int32(i)}, point{-int32(i), int32(i)}) } for i := 0; i < numEntries; i++ { v, ok := m.Load(point{int32(i), -int32(i)}) if !ok { t.Fatalf("value not found for %d", i) } if v.x != -int32(i) { t.Fatalf("x value does not match for %d: %v", i, v) } if v.y != int32(i) { t.Fatalf("y value does not match for %d: %v", i, v) } } } func TestMapLoadOrStore(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } for i := 0; i < numEntries; i++ { if _, loaded := m.LoadOrStore(strconv.Itoa(i), i); !loaded { t.Fatalf("value not found for %d", i) } } } func TestMapLoadOrCompute(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() (newValue int, cancel bool) { return i, true }) if loaded { t.Fatalf("value not computed for %d", i) } if v != 0 { t.Fatalf("values do not match for %d: %v", i, v) } } if m.Size() != 0 { t.Fatalf("zero map size expected: %d", m.Size()) } for i := 0; i < numEntries; i++ { v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() (newValue int, cancel bool) { return i, false }) if loaded { t.Fatalf("value not computed for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } for i := 0; i < numEntries; i++ { v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() (newValue int, cancel bool) { t.Fatalf("value func invoked") return newValue, false }) if !loaded { t.Fatalf("value not loaded for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } } func TestMapLoadOrCompute_FunctionCalledOnce(t *testing.T) { m := NewMap[int, int]() for i := 0; i < 100; { m.LoadOrCompute(i, func() (newValue int, cancel bool) { newValue, i = i, i+1 return newValue, false }) } m.Range(func(k, v int) bool { if k != v { t.Fatalf("%dth key is not equal to value %d", k, v) } return true }) } func TestMapOfCompute(t *testing.T) { m := NewMap[string, int]() // Store a new value. v, ok := m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) { if oldValue != 0 { t.Fatalf("oldValue should be 0 when computing a new value: %d", oldValue) } if loaded { t.Fatal("loaded should be false when computing a new value") } newValue = 42 op = UpdateOp return }) if v != 42 { t.Fatalf("v should be 42 when computing a new value: %d", v) } if !ok { t.Fatal("ok should be true when computing a new value") } // Update an existing value. v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) { if oldValue != 42 { t.Fatalf("oldValue should be 42 when updating the value: %d", oldValue) } if !loaded { t.Fatal("loaded should be true when updating the value") } newValue = oldValue + 42 op = UpdateOp return }) if v != 84 { t.Fatalf("v should be 84 when updating the value: %d", v) } if !ok { t.Fatal("ok should be true when updating the value") } // Check that NoOp doesn't update the value v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) { return 0, CancelOp }) if v != 84 { t.Fatalf("v should be 84 after using NoOp: %d", v) } if !ok { t.Fatal("ok should be true when updating the value") } // Delete an existing value. v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) { if oldValue != 84 { t.Fatalf("oldValue should be 84 when deleting the value: %d", oldValue) } if !loaded { t.Fatal("loaded should be true when deleting the value") } op = DeleteOp return }) if v != 84 { t.Fatalf("v should be 84 when deleting the value: %d", v) } if ok { t.Fatal("ok should be false when deleting the value") } // Try to delete a non-existing value. Notice different key. v, ok = m.Compute("barbaz", func(oldValue int, loaded bool) (newValue int, op ComputeOp) { if oldValue != 0 { t.Fatalf("oldValue should be 0 when trying to delete a non-existing value: %d", oldValue) } if loaded { t.Fatal("loaded should be false when trying to delete a non-existing value") } // We're returning a non-zero value, but the map should ignore it. newValue = 42 op = DeleteOp return }) if v != 0 { t.Fatalf("v should be 0 when trying to delete a non-existing value: %d", v) } if ok { t.Fatal("ok should be false when trying to delete a non-existing value") } // Try NoOp on a non-existing value v, ok = m.Compute("barbaz", func(oldValue int, loaded bool) (newValue int, op ComputeOp) { if oldValue != 0 { t.Fatalf("oldValue should be 0 when trying to delete a non-existing value: %d", oldValue) } if loaded { t.Fatal("loaded should be false when trying to delete a non-existing value") } // We're returning a non-zero value, but the map should ignore it. newValue = 42 op = CancelOp return }) if v != 0 { t.Fatalf("v should be 0 when trying to delete a non-existing value: %d", v) } if ok { t.Fatal("ok should be false when trying to delete a non-existing value") } } func TestMapStringStoreThenDelete(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } for i := 0; i < numEntries; i++ { m.Delete(strconv.Itoa(i)) if _, ok := m.Load(strconv.Itoa(i)); ok { t.Fatalf("value was not expected for %d", i) } } } func TestMapIntStoreThenDelete(t *testing.T) { const numEntries = 1000 m := NewMap[int32, int32]() for i := 0; i < numEntries; i++ { m.Store(int32(i), int32(i)) } for i := 0; i < numEntries; i++ { m.Delete(int32(i)) if _, ok := m.Load(int32(i)); ok { t.Fatalf("value was not expected for %d", i) } } } func TestMapStructStoreThenDelete(t *testing.T) { const numEntries = 1000 m := NewMap[point, string]() for i := 0; i < numEntries; i++ { m.Store(point{int32(i), 42}, strconv.Itoa(i)) } for i := 0; i < numEntries; i++ { m.Delete(point{int32(i), 42}) if _, ok := m.Load(point{int32(i), 42}); ok { t.Fatalf("value was not expected for %d", i) } } } func TestMapStringStoreThenLoadAndDelete(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } for i := 0; i < numEntries; i++ { if v, loaded := m.LoadAndDelete(strconv.Itoa(i)); !loaded || v != i { t.Fatalf("value was not found or different for %d: %v", i, v) } if _, ok := m.Load(strconv.Itoa(i)); ok { t.Fatalf("value was not expected for %d", i) } } } func TestMapIntStoreThenLoadAndDelete(t *testing.T) { const numEntries = 1000 m := NewMap[int, int]() for i := 0; i < numEntries; i++ { m.Store(i, i) } for i := 0; i < numEntries; i++ { if _, loaded := m.LoadAndDelete(i); !loaded { t.Fatalf("value was not found for %d", i) } if _, ok := m.Load(i); ok { t.Fatalf("value was not expected for %d", i) } } } func TestMapStructStoreThenLoadAndDelete(t *testing.T) { const numEntries = 1000 m := NewMap[point, int]() for i := 0; i < numEntries; i++ { m.Store(point{42, int32(i)}, i) } for i := 0; i < numEntries; i++ { if _, loaded := m.LoadAndDelete(point{42, int32(i)}); !loaded { t.Fatalf("value was not found for %d", i) } if _, ok := m.Load(point{42, int32(i)}); ok { t.Fatalf("value was not expected for %d", i) } } } func TestMapStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T) { const numEntries = 1000 m := NewMap[int, int]() for i := 0; i < numEntries; i++ { m.Store(i, i) } cdone := make(chan bool) go func() { for i := 0; i < numEntries; i++ { m.Delete(i) } cdone <- true }() go func() { for i := 0; i < numEntries; i++ { m.Delete(i) } cdone <- true }() // Wait for the goroutines to finish. <-cdone <-cdone stats := m.Stats() if stats.RootBuckets != defaultMinMapTableLen { t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets) } } func sizeBasedOnTypedRange(m *Map[string, int]) int { size := 0 m.Range(func(key string, value int) bool { size++ return true }) return size } func TestMapSize(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() size := m.Size() if size != 0 { t.Fatalf("zero size expected: %d", size) } expectedSize := 0 for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) expectedSize++ size := m.Size() if size != expectedSize { t.Fatalf("size of %d was expected, got: %d", expectedSize, size) } rsize := sizeBasedOnTypedRange(m) if size != rsize { t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize) } } for i := 0; i < numEntries; i++ { m.Delete(strconv.Itoa(i)) expectedSize-- size := m.Size() if size != expectedSize { t.Fatalf("size of %d was expected, got: %d", expectedSize, size) } rsize := sizeBasedOnTypedRange(m) if size != rsize { t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize) } } } func TestMapClear(t *testing.T) { const numEntries = 1000 m := NewMap[string, int]() for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } size := m.Size() if size != numEntries { t.Fatalf("size of %d was expected, got: %d", numEntries, size) } m.Clear() size = m.Size() if size != 0 { t.Fatalf("zero size was expected, got: %d", size) } rsize := sizeBasedOnTypedRange(m) if rsize != 0 { t.Fatalf("zero number of entries in Range was expected, got: %d", rsize) } } func assertMapCapacity[K comparable, V any](t *testing.T, m *Map[K, V], expectedCap int) { stats := m.Stats() if stats.Capacity != expectedCap { t.Fatalf("capacity was different from %d: %d", expectedCap, stats.Capacity) } } func TestNewMapWithPresize(t *testing.T) { assertMapCapacity(t, NewMap[string, string](), defaultMinMapTableLen*entriesPerMapBucket) assertMapCapacity(t, NewMap[string, string](WithPresize(0)), defaultMinMapTableLen*entriesPerMapBucket) assertMapCapacity(t, NewMap[string, string](WithPresize(-100)), defaultMinMapTableLen*entriesPerMapBucket) assertMapCapacity(t, NewMap[string, string](WithPresize(500)), 1280) assertMapCapacity(t, NewMap[int, int](WithPresize(1_000_000)), 2621440) assertMapCapacity(t, NewMap[point, point](WithPresize(100)), 160) } func TestNewMapWithPresize_DoesNotShrinkBelowMinTableLen(t *testing.T) { const minTableLen = 1024 const numEntries = int(minTableLen * entriesPerMapBucket * mapLoadFactor) m := NewMap[int, int](WithPresize(numEntries)) for i := 0; i < 2*numEntries; i++ { m.Store(i, i) } stats := m.Stats() if stats.RootBuckets <= minTableLen { t.Fatalf("table did not grow: %d", stats.RootBuckets) } for i := 0; i < 2*numEntries; i++ { m.Delete(i) } stats = m.Stats() if stats.RootBuckets != minTableLen { t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets) } } func TestNewMapGrowOnly_OnlyShrinksOnClear(t *testing.T) { const minTableLen = 128 const numEntries = minTableLen * entriesPerMapBucket m := NewMap[int, int](WithPresize(numEntries), WithGrowOnly()) stats := m.Stats() initialTableLen := stats.RootBuckets for i := 0; i < 2*numEntries; i++ { m.Store(i, i) } stats = m.Stats() maxTableLen := stats.RootBuckets if maxTableLen <= minTableLen { t.Fatalf("table did not grow: %d", maxTableLen) } for i := 0; i < numEntries; i++ { m.Delete(i) } stats = m.Stats() if stats.RootBuckets != maxTableLen { t.Fatalf("table length was different from the expected: %d", stats.RootBuckets) } m.Clear() stats = m.Stats() if stats.RootBuckets != initialTableLen { t.Fatalf("table length was different from the initial: %d", stats.RootBuckets) } } func TestMapResize(t *testing.T) { m := NewMap[string, int]() const numEntries = 100_000 for i := 0; i < numEntries; i++ { m.Store(strconv.Itoa(i), i) } stats := m.Stats() if stats.Size != numEntries { t.Fatalf("size was too small: %d", stats.Size) } expectedCapacity := int(math.RoundToEven(mapLoadFactor+1)) * stats.RootBuckets * entriesPerMapBucket if stats.Capacity > expectedCapacity { t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity) } if stats.RootBuckets <= defaultMinMapTableLen { t.Fatalf("table was too small: %d", stats.RootBuckets) } if stats.TotalGrowths == 0 { t.Fatalf("non-zero total growths expected: %d", stats.TotalGrowths) } if stats.TotalShrinks > 0 { t.Fatalf("zero total shrinks expected: %d", stats.TotalShrinks) } // This is useful when debugging table resize and occupancy. // Use -v flag to see the output. t.Log(stats.ToString()) for i := 0; i < numEntries; i++ { m.Delete(strconv.Itoa(i)) } stats = m.Stats() if stats.Size > 0 { t.Fatalf("zero size was expected: %d", stats.Size) } expectedCapacity = stats.RootBuckets * entriesPerMapBucket if stats.Capacity != expectedCapacity { t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity) } if stats.RootBuckets != defaultMinMapTableLen { t.Fatalf("table was too large: %d", stats.RootBuckets) } if stats.TotalShrinks == 0 { t.Fatalf("non-zero total shrinks expected: %d", stats.TotalShrinks) } t.Log(stats.ToString()) } func TestMapResize_CounterLenLimit(t *testing.T) { const numEntries = 1_000_000 m := NewMap[string, string]() for i := 0; i < numEntries; i++ { m.Store("foo"+strconv.Itoa(i), "bar"+strconv.Itoa(i)) } stats := m.Stats() if stats.Size != numEntries { t.Fatalf("size was too small: %d", stats.Size) } if stats.CounterLen != maxMapCounterLen { t.Fatalf("number of counter stripes was too large: %d, expected: %d", stats.CounterLen, maxMapCounterLen) } } func testParallelResize(t *testing.T, numGoroutines int) { m := NewMap[int, int]() // Fill the map to trigger resizing const initialEntries = 10000 const newEntries = 5000 for i := 0; i < initialEntries; i++ { m.Store(i, i*2) } // Start concurrent operations that should trigger helping behavior var wg sync.WaitGroup // Launch goroutines that will encounter resize operations for g := 0; g < numGoroutines; g++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() // Perform many operations to trigger resize and helping for i := 0; i < newEntries; i++ { key := goroutineID*newEntries + i + initialEntries m.Store(key, key*2) // Verify the value if val, ok := m.Load(key); !ok || val != key*2 { t.Errorf("Failed to load key %d: got %v, %v", key, val, ok) return } } }(g) } wg.Wait() // Verify all entries are present finalSize := m.Size() expectedSize := initialEntries + numGoroutines*newEntries if finalSize != expectedSize { t.Errorf("Expected size %d, got %d", expectedSize, finalSize) } stats := m.Stats() if stats.TotalGrowths == 0 { t.Error("Expected at least one table growth due to concurrent operations") } } func TestMapParallelResize(t *testing.T) { testParallelResize(t, 1) testParallelResize(t, runtime.GOMAXPROCS(0)) testParallelResize(t, 100) } func testParallelResizeWithSameKeys(t *testing.T, numGoroutines int) { m := NewMap[int, int]() // Fill the map to trigger resizing const entries = 1000 for i := 0; i < entries; i++ { m.Store(2*i, 2*i) } // Start concurrent operations that should trigger helping behavior var wg sync.WaitGroup // Launch goroutines that will encounter resize operations for g := 0; g < numGoroutines; g++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() for i := 0; i < 10*entries; i++ { m.Store(i, i) } }(g) } wg.Wait() // Verify all entries are present finalSize := m.Size() expectedSize := 10 * entries if finalSize != expectedSize { t.Errorf("Expected size %d, got %d", expectedSize, finalSize) } stats := m.Stats() if stats.TotalGrowths == 0 { t.Error("Expected at least one table growth due to concurrent operations") } } func TestMapParallelResize_IntersectingKeys(t *testing.T) { testParallelResizeWithSameKeys(t, 1) testParallelResizeWithSameKeys(t, runtime.GOMAXPROCS(0)) testParallelResizeWithSameKeys(t, 100) } func testParallelShrinking(t *testing.T, numGoroutines int) { m := NewMap[int, int]() // Fill the map to trigger resizing const entries = 100000 for i := 0; i < entries; i++ { m.Store(i, i) } // Start concurrent operations that should trigger helping behavior var wg sync.WaitGroup // Launch goroutines that will encounter resize operations for g := 0; g < numGoroutines; g++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() for i := 0; i < entries; i++ { m.Delete(i) } }(g) } wg.Wait() // Verify all entries are present finalSize := m.Size() if finalSize != 0 { t.Errorf("Expected size 0, got %d", finalSize) } stats := m.Stats() if stats.TotalShrinks == 0 { t.Error("Expected at least one table shrinking due to concurrent operations") } } func TestMapParallelShrinking(t *testing.T) { testParallelShrinking(t, 1) testParallelShrinking(t, runtime.GOMAXPROCS(0)) testParallelShrinking(t, 100) } func parallelSeqMapGrower(m *Map[int, int], numEntries int, positive bool, cdone chan bool) { for i := 0; i < numEntries; i++ { if positive { m.Store(i, i) } else { m.Store(-i, -i) } } cdone <- true } func TestMapParallelGrowth_GrowOnly(t *testing.T) { const numEntries = 100_000 m := NewMap[int, int]() cdone := make(chan bool) go parallelSeqMapGrower(m, numEntries, true, cdone) go parallelSeqMapGrower(m, numEntries, false, cdone) // Wait for the goroutines to finish. <-cdone <-cdone // Verify map contents. for i := -numEntries + 1; i < numEntries; i++ { v, ok := m.Load(i) if !ok { t.Fatalf("value not found for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } if s := m.Size(); s != 2*numEntries-1 { t.Fatalf("unexpected size: %v", s) } } func parallelRandMapResizer(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) { r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < numIters; i++ { coin := r.Int63n(2) for j := 0; j < numEntries; j++ { if coin == 1 { m.Store(strconv.Itoa(j), j) } else { m.Delete(strconv.Itoa(j)) } } } cdone <- true } func TestMapParallelGrowth(t *testing.T) { const numIters = 1_000 const numEntries = 2 * entriesPerMapBucket * defaultMinMapTableLen m := NewMap[string, int]() cdone := make(chan bool) go parallelRandMapResizer(t, m, numIters, numEntries, cdone) go parallelRandMapResizer(t, m, numIters, numEntries, cdone) // Wait for the goroutines to finish. <-cdone <-cdone // Verify map contents. for i := 0; i < numEntries; i++ { v, ok := m.Load(strconv.Itoa(i)) if !ok { // The entry may be deleted and that's ok. continue } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } s := m.Size() if s > numEntries { t.Fatalf("unexpected size: %v", s) } rs := sizeBasedOnTypedRange(m) if s != rs { t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs) } } func parallelRandMapClearer(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) { r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < numIters; i++ { coin := r.Int63n(2) for j := 0; j < numEntries; j++ { if coin == 1 { m.Store(strconv.Itoa(j), j) } else { m.Clear() } } } cdone <- true } func TestMapParallelClear(t *testing.T) { const numIters = 100 const numEntries = 1_000 m := NewMap[string, int]() cdone := make(chan bool) go parallelRandMapClearer(t, m, numIters, numEntries, cdone) go parallelRandMapClearer(t, m, numIters, numEntries, cdone) // Wait for the goroutines to finish. <-cdone <-cdone // Verify map size. s := m.Size() if s > numEntries { t.Fatalf("unexpected size: %v", s) } rs := sizeBasedOnTypedRange(m) if s != rs { t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs) } } func parallelSeqMapStorer(t *testing.T, m *Map[string, int], storeEach, numIters, numEntries int, cdone chan bool) { for i := 0; i < numIters; i++ { for j := 0; j < numEntries; j++ { if storeEach == 0 || j%storeEach == 0 { m.Store(strconv.Itoa(j), j) // Due to atomic snapshots we must see a ""/j pair. v, ok := m.Load(strconv.Itoa(j)) if !ok { t.Errorf("value was not found for %d", j) break } if v != j { t.Errorf("value was not expected for %d: %d", j, v) break } } } } cdone <- true } func TestMapParallelStores(t *testing.T) { const numStorers = 4 const numIters = 10_000 const numEntries = 100 m := NewMap[string, int]() cdone := make(chan bool) for i := 0; i < numStorers; i++ { go parallelSeqMapStorer(t, m, i, numIters, numEntries, cdone) } // Wait for the goroutines to finish. for i := 0; i < numStorers; i++ { <-cdone } // Verify map contents. for i := 0; i < numEntries; i++ { v, ok := m.Load(strconv.Itoa(i)) if !ok { t.Fatalf("value not found for %d", i) } if v != i { t.Fatalf("values do not match for %d: %v", i, v) } } } func parallelRandMapStorer(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) { r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < numIters; i++ { j := r.Intn(numEntries) if v, loaded := m.LoadOrStore(strconv.Itoa(j), j); loaded { if v != j { t.Errorf("value was not expected for %d: %d", j, v) } } } cdone <- true } func parallelRandMapDeleter(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) { r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < numIters; i++ { j := r.Intn(numEntries) if v, loaded := m.LoadAndDelete(strconv.Itoa(j)); loaded { if v != j { t.Errorf("value was not expected for %d: %d", j, v) } } } cdone <- true } func parallelMapLoader(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) { for i := 0; i < numIters; i++ { for j := 0; j < numEntries; j++ { // Due to atomic snapshots we must either see no entry, or a ""/j pair. if v, ok := m.Load(strconv.Itoa(j)); ok { if v != j { t.Errorf("value was not expected for %d: %d", j, v) } } } } cdone <- true } func TestMapAtomicSnapshot(t *testing.T) { const numIters = 100_000 const numEntries = 100 m := NewMap[string, int]() cdone := make(chan bool) // Update or delete random entry in parallel with loads. go parallelRandMapStorer(t, m, numIters, numEntries, cdone) go parallelRandMapDeleter(t, m, numIters, numEntries, cdone) go parallelMapLoader(t, m, numIters, numEntries, cdone) // Wait for the goroutines to finish. for i := 0; i < 3; i++ { <-cdone } } func TestMapParallelStoresAndDeletes(t *testing.T) { const numWorkers = 2 const numIters = 100_000 const numEntries = 1000 m := NewMap[string, int]() cdone := make(chan bool) // Update random entry in parallel with deletes. for i := 0; i < numWorkers; i++ { go parallelRandMapStorer(t, m, numIters, numEntries, cdone) go parallelRandMapDeleter(t, m, numIters, numEntries, cdone) } // Wait for the goroutines to finish. for i := 0; i < 2*numWorkers; i++ { <-cdone } } func parallelMapComputer(m *Map[uint64, uint64], numIters, numEntries int, cdone chan bool) { for i := 0; i < numIters; i++ { for j := 0; j < numEntries; j++ { m.Compute(uint64(j), func(oldValue uint64, loaded bool) (newValue uint64, op ComputeOp) { return oldValue + 1, UpdateOp }) } } cdone <- true } func TestMapParallelComputes(t *testing.T) { const numWorkers = 4 // Also stands for numEntries. const numIters = 10_000 m := NewMap[uint64, uint64]() cdone := make(chan bool) for i := 0; i < numWorkers; i++ { go parallelMapComputer(m, numIters, numWorkers, cdone) } // Wait for the goroutines to finish. for i := 0; i < numWorkers; i++ { <-cdone } // Verify map contents. for i := 0; i < numWorkers; i++ { v, ok := m.Load(uint64(i)) if !ok { t.Fatalf("value not found for %d", i) } if v != numWorkers*numIters { t.Fatalf("values do not match for %d: %v", i, v) } } } func parallelRangeMapStorer(m *Map[int, int], numEntries int, stopFlag *int64, cdone chan bool) { for { for i := 0; i < numEntries; i++ { m.Store(i, i) } if atomic.LoadInt64(stopFlag) != 0 { break } } cdone <- true } func parallelRangeMapDeleter(m *Map[int, int], numEntries int, stopFlag *int64, cdone chan bool) { for { for i := 0; i < numEntries; i++ { m.Delete(i) } if atomic.LoadInt64(stopFlag) != 0 { break } } cdone <- true } func TestMapParallelRange(t *testing.T) { const numEntries = 10_000 m := NewMap[int, int](WithPresize(numEntries)) for i := 0; i < numEntries; i++ { m.Store(i, i) } // Start goroutines that would be storing and deleting items in parallel. cdone := make(chan bool) stopFlag := int64(0) go parallelRangeMapStorer(m, numEntries, &stopFlag, cdone) go parallelRangeMapDeleter(m, numEntries, &stopFlag, cdone) // Iterate the map and verify that no duplicate keys were met. met := make(map[int]int) m.Range(func(key int, value int) bool { if key != value { t.Fatalf("got unexpected value for key %d: %d", key, value) return false } met[key] += 1 return true }) if len(met) == 0 { t.Fatal("no entries were met when iterating") } for k, c := range met { if c != 1 { t.Fatalf("met key %d multiple times: %d", k, c) } } // Make sure that both goroutines finish. atomic.StoreInt64(&stopFlag, 1) <-cdone <-cdone } func parallelMapShrinker(t *testing.T, m *Map[uint64, *point], numIters, numEntries int, stopFlag *int64, cdone chan bool) { for i := 0; i < numIters; i++ { for j := 0; j < numEntries; j++ { if p, loaded := m.LoadOrStore(uint64(j), &point{int32(j), int32(j)}); loaded { t.Errorf("value was present for %d: %v", j, p) } } for j := 0; j < numEntries; j++ { m.Delete(uint64(j)) } } atomic.StoreInt64(stopFlag, 1) cdone <- true } func parallelMapUpdater(t *testing.T, m *Map[uint64, *point], idx int, stopFlag *int64, cdone chan bool) { for atomic.LoadInt64(stopFlag) != 1 { sleepUs := int(randv2.Uint64() % 10) if p, loaded := m.LoadOrStore(uint64(idx), &point{int32(idx), int32(idx)}); loaded { t.Errorf("value was present for %d: %v", idx, p) } time.Sleep(time.Duration(sleepUs) * time.Microsecond) if _, ok := m.Load(uint64(idx)); !ok { t.Errorf("value was not found for %d", idx) } m.Delete(uint64(idx)) } cdone <- true } func TestMapDoesNotLoseEntriesOnResize(t *testing.T) { const numIters = 10_000 const numEntries = 128 m := NewMap[uint64, *point]() cdone := make(chan bool) stopFlag := int64(0) go parallelMapShrinker(t, m, numIters, numEntries, &stopFlag, cdone) go parallelMapUpdater(t, m, numEntries, &stopFlag, cdone) // Wait for the goroutines to finish. <-cdone <-cdone // Verify map contents. if s := m.Size(); s != 0 { t.Fatalf("map is not empty: %d", s) } } func TestMapStats(t *testing.T) { m := NewMap[int, int]() stats := m.Stats() if stats.RootBuckets != defaultMinMapTableLen { t.Fatalf("unexpected number of root buckets: %d", stats.RootBuckets) } if stats.TotalBuckets != stats.RootBuckets { t.Fatalf("unexpected number of total buckets: %d", stats.TotalBuckets) } if stats.EmptyBuckets != stats.RootBuckets { t.Fatalf("unexpected number of empty buckets: %d", stats.EmptyBuckets) } if stats.Capacity != entriesPerMapBucket*defaultMinMapTableLen { t.Fatalf("unexpected capacity: %d", stats.Capacity) } if stats.Size != 0 { t.Fatalf("unexpected size: %d", stats.Size) } if stats.Counter != 0 { t.Fatalf("unexpected counter: %d", stats.Counter) } if stats.CounterLen != 8 { t.Fatalf("unexpected counter length: %d", stats.CounterLen) } for i := 0; i < 200; i++ { m.Store(i, i) } stats = m.Stats() if stats.RootBuckets != 2*defaultMinMapTableLen { t.Fatalf("unexpected number of root buckets: %d", stats.RootBuckets) } if stats.TotalBuckets < stats.RootBuckets { t.Fatalf("unexpected number of total buckets: %d", stats.TotalBuckets) } if stats.EmptyBuckets >= stats.RootBuckets { t.Fatalf("unexpected number of empty buckets: %d", stats.EmptyBuckets) } if stats.Capacity < 2*entriesPerMapBucket*defaultMinMapTableLen { t.Fatalf("unexpected capacity: %d", stats.Capacity) } if stats.Size != 200 { t.Fatalf("unexpected size: %d", stats.Size) } if stats.Counter != 200 { t.Fatalf("unexpected counter: %d", stats.Counter) } if stats.CounterLen != 8 { t.Fatalf("unexpected counter length: %d", stats.CounterLen) } } func TestToPlainMap_NilPointer(t *testing.T) { pm := ToPlainMap[int, int](nil) if len(pm) != 0 { t.Fatalf("got unexpected size of nil map copy: %d", len(pm)) } } func TestToPlainMap(t *testing.T) { const numEntries = 1000 m := NewMap[int, int]() for i := 0; i < numEntries; i++ { m.Store(i, i) } pm := ToPlainMap[int, int](m) if len(pm) != numEntries { t.Fatalf("got unexpected size of nil map copy: %d", len(pm)) } for i := 0; i < numEntries; i++ { if v := pm[i]; v != i { t.Fatalf("unexpected value for key %d: %d", i, v) } } } func BenchmarkMap_NoWarmUp(b *testing.B) { for _, bc := range benchmarkCases { if bc.readPercentage == 100 { // This benchmark doesn't make sense without a warm-up. continue } b.Run(bc.name, func(b *testing.B) { m := NewMap[string, int]() benchmarkMapStringKeys(b, func(k string) (int, bool) { return m.Load(k) }, func(k string, v int) { m.Store(k, v) }, func(k string) { m.Delete(k) }, bc.readPercentage) }) } } func BenchmarkMap_WarmUp(b *testing.B) { for _, bc := range benchmarkCases { b.Run(bc.name, func(b *testing.B) { m := NewMap[string, int](WithPresize(benchmarkNumEntries)) for i := 0; i < benchmarkNumEntries; i++ { m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i) } b.ResetTimer() benchmarkMapStringKeys(b, func(k string) (int, bool) { return m.Load(k) }, func(k string, v int) { m.Store(k, v) }, func(k string) { m.Delete(k) }, bc.readPercentage) }) } } func benchmarkMapStringKeys( b *testing.B, loadFn func(k string) (int, bool), storeFn func(k string, v int), deleteFn func(k string), readPercentage int, ) { runParallel(b, func(pb *testing.PB) { // convert percent to permille to support 99% case storeThreshold := 10 * readPercentage deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2) for pb.Next() { op := int(randv2.Uint64() % 1000) i := int(randv2.Uint64() % benchmarkNumEntries) if op >= deleteThreshold { deleteFn(benchmarkKeys[i]) } else if op >= storeThreshold { storeFn(benchmarkKeys[i], i) } else { loadFn(benchmarkKeys[i]) } } }) } func BenchmarkMapInt_NoWarmUp(b *testing.B) { for _, bc := range benchmarkCases { if bc.readPercentage == 100 { // This benchmark doesn't make sense without a warm-up. continue } b.Run(bc.name, func(b *testing.B) { m := NewMap[int, int]() benchmarkMapIntKeys(b, func(k int) (int, bool) { return m.Load(k) }, func(k int, v int) { m.Store(k, v) }, func(k int) { m.Delete(k) }, bc.readPercentage) }) } } func BenchmarkMapInt_WarmUp(b *testing.B) { for _, bc := range benchmarkCases { b.Run(bc.name, func(b *testing.B) { m := NewMap[int, int](WithPresize(benchmarkNumEntries)) for i := 0; i < benchmarkNumEntries; i++ { m.Store(i, i) } b.ResetTimer() benchmarkMapIntKeys(b, func(k int) (int, bool) { return m.Load(k) }, func(k int, v int) { m.Store(k, v) }, func(k int) { m.Delete(k) }, bc.readPercentage) }) } } func BenchmarkIntMapStandard_NoWarmUp(b *testing.B) { for _, bc := range benchmarkCases { if bc.readPercentage == 100 { // This benchmark doesn't make sense without a warm-up. continue } b.Run(bc.name, func(b *testing.B) { var m sync.Map benchmarkMapIntKeys(b, func(k int) (value int, ok bool) { v, ok := m.Load(k) if ok { return v.(int), ok } else { return 0, false } }, func(k int, v int) { m.Store(k, v) }, func(k int) { m.Delete(k) }, bc.readPercentage) }) } } // This is a nice scenario for sync.Map since a lot of updates // will hit the readOnly part of the map. func BenchmarkIntMapStandard_WarmUp(b *testing.B) { for _, bc := range benchmarkCases { b.Run(bc.name, func(b *testing.B) { var m sync.Map for i := 0; i < benchmarkNumEntries; i++ { m.Store(i, i) } b.ResetTimer() benchmarkMapIntKeys(b, func(k int) (value int, ok bool) { v, ok := m.Load(k) if ok { return v.(int), ok } else { return 0, false } }, func(k int, v int) { m.Store(k, v) }, func(k int) { m.Delete(k) }, bc.readPercentage) }) } } func benchmarkMapIntKeys( b *testing.B, loadFn func(k int) (int, bool), storeFn func(k int, v int), deleteFn func(k int), readPercentage int, ) { runParallel(b, func(pb *testing.PB) { // convert percent to permille to support 99% case storeThreshold := 10 * readPercentage deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2) for pb.Next() { op := int(randv2.Uint64() % 1000) i := int(randv2.Uint64() % benchmarkNumEntries) if op >= deleteThreshold { deleteFn(i) } else if op >= storeThreshold { storeFn(i, i) } else { loadFn(i) } } }) } func BenchmarkMapRange(b *testing.B) { m := NewMap[string, int](WithPresize(benchmarkNumEntries)) for i := 0; i < benchmarkNumEntries; i++ { m.Store(benchmarkKeys[i], i) } b.ResetTimer() runParallel(b, func(pb *testing.PB) { foo := 0 for pb.Next() { m.Range(func(key string, value int) bool { foo++ return true }) _ = foo } }) } // Benchmarks noop performance of Compute func BenchmarkMapCompute(b *testing.B) { tests := []struct { Name string Op ComputeOp }{ { Name: "UpdateOp", Op: UpdateOp, }, { Name: "CancelOp", Op: CancelOp, }, } for _, test := range tests { b.Run("op="+test.Name, func(b *testing.B) { m := NewMap[struct{}, bool]() m.Store(struct{}{}, true) b.ResetTimer() for i := 0; i < b.N; i++ { m.Compute(struct{}{}, func(oldValue bool, loaded bool) (newValue bool, op ComputeOp) { return oldValue, test.Op }) } }) } } func BenchmarkMapParallelRehashing(b *testing.B) { tests := []struct { name string goroutines int numEntries int }{ {"1goroutine_10M", 1, 10_000_000}, {"4goroutines_10M", 4, 10_000_000}, {"8goroutines_10M", 8, 10_000_000}, } for _, test := range tests { b.Run(test.name, func(b *testing.B) { for i := 0; i < b.N; i++ { m := NewMap[int, int]() var wg sync.WaitGroup entriesPerGoroutine := test.numEntries / test.goroutines start := time.Now() for g := 0; g < test.goroutines; g++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() base := goroutineID * entriesPerGoroutine for j := 0; j < entriesPerGoroutine; j++ { key := base + j m.Store(key, key) } }(g) } wg.Wait() duration := time.Since(start) b.ReportMetric(float64(test.numEntries)/duration.Seconds(), "entries/s") finalSize := m.Size() if finalSize != test.numEntries { b.Fatalf("Expected size %d, got %d", test.numEntries, finalSize) } stats := m.Stats() if stats.TotalGrowths == 0 { b.Error("Expected at least one table growth during rehashing") } } }) } } func TestNextPowOf2(t *testing.T) { if nextPowOf2(0) != 1 { t.Error("nextPowOf2 failed") } if nextPowOf2(1) != 1 { t.Error("nextPowOf2 failed") } if nextPowOf2(2) != 2 { t.Error("nextPowOf2 failed") } if nextPowOf2(3) != 4 { t.Error("nextPowOf2 failed") } } func TestBroadcast(t *testing.T) { testCases := []struct { input uint8 expected uint64 }{ { input: 0, expected: 0, }, { input: 1, expected: 0x0101010101010101, }, { input: 2, expected: 0x0202020202020202, }, { input: 42, expected: 0x2a2a2a2a2a2a2a2a, }, { input: 127, expected: 0x7f7f7f7f7f7f7f7f, }, { input: 255, expected: 0xffffffffffffffff, }, } for _, tc := range testCases { t.Run(strconv.Itoa(int(tc.input)), func(t *testing.T) { if broadcast(tc.input) != tc.expected { t.Errorf("unexpected result: %x", broadcast(tc.input)) } }) } } func TestFirstMarkedByteIndex(t *testing.T) { testCases := []struct { input uint64 expected int }{ { input: 0, expected: 8, }, { input: 0x8080808080808080, expected: 0, }, { input: 0x0000000000000080, expected: 0, }, { input: 0x0000000000008000, expected: 1, }, { input: 0x0000000000800000, expected: 2, }, { input: 0x0000000080000000, expected: 3, }, { input: 0x0000008000000000, expected: 4, }, { input: 0x0000800000000000, expected: 5, }, { input: 0x0080000000000000, expected: 6, }, { input: 0x8000000000000000, expected: 7, }, } for _, tc := range testCases { t.Run(strconv.Itoa(int(tc.input)), func(t *testing.T) { if firstMarkedByteIndex(tc.input) != tc.expected { t.Errorf("unexpected result: %x", firstMarkedByteIndex(tc.input)) } }) } } func TestMarkZeroBytes(t *testing.T) { testCases := []struct { input uint64 expected uint64 }{ { input: 0xffffffffffffffff, expected: 0, }, { input: 0, expected: 0x8080808080808080, }, { input: 1, expected: 0x8080808080808000, }, { input: 1 << 9, expected: 0x8080808080800080, }, { input: 1 << 17, expected: 0x8080808080008080, }, { input: 1 << 25, expected: 0x8080808000808080, }, { input: 1 << 33, expected: 0x8080800080808080, }, { input: 1 << 41, expected: 0x8080008080808080, }, { input: 1 << 49, expected: 0x8000808080808080, }, { input: 1 << 57, expected: 0x0080808080808080, }, // false positive { input: 0x0100, expected: 0x8080808080808080, }, } for _, tc := range testCases { t.Run(strconv.Itoa(int(tc.input)), func(t *testing.T) { if markZeroBytes(tc.input) != tc.expected { t.Errorf("unexpected result: %x", markZeroBytes(tc.input)) } }) } } func TestSetByte(t *testing.T) { testCases := []struct { word uint64 b uint8 idx int expected uint64 }{ { word: 0xffffffffffffffff, b: 0, idx: 0, expected: 0xffffffffffffff00, }, { word: 0xffffffffffffffff, b: 1, idx: 1, expected: 0xffffffffffff01ff, }, { word: 0xffffffffffffffff, b: 2, idx: 2, expected: 0xffffffffff02ffff, }, { word: 0xffffffffffffffff, b: 3, idx: 3, expected: 0xffffffff03ffffff, }, { word: 0xffffffffffffffff, b: 4, idx: 4, expected: 0xffffff04ffffffff, }, { word: 0xffffffffffffffff, b: 5, idx: 5, expected: 0xffff05ffffffffff, }, { word: 0xffffffffffffffff, b: 6, idx: 6, expected: 0xff06ffffffffffff, }, { word: 0xffffffffffffffff, b: 7, idx: 7, expected: 0x07ffffffffffffff, }, { word: 0, b: 0xff, idx: 7, expected: 0xff00000000000000, }, } for _, tc := range testCases { t.Run(strconv.Itoa(int(tc.word)), func(t *testing.T) { if setByte(tc.word, tc.b, tc.idx) != tc.expected { t.Errorf("unexpected result: %x", setByte(tc.word, tc.b, tc.idx)) } }) } } ================================================ FILE: core/Clash.Meta/common/yaml/yaml.go ================================================ // Package yaml provides a common entrance for YAML marshaling and unmarshalling. package yaml import ( "gopkg.in/yaml.v3" ) func Unmarshal(in []byte, out any) (err error) { return yaml.Unmarshal(in, out) } func Marshal(in any) (out []byte, err error) { return yaml.Marshal(in) } ================================================ FILE: core/Clash.Meta/component/auth/auth.go ================================================ package auth type Authenticator interface { Verify(user string, pass string) bool Users() []string } type AuthStore interface { Authenticator() Authenticator SetAuthenticator(Authenticator) } type AuthUser struct { User string Pass string } type inMemoryAuthenticator struct { storage map[string]string usernames []string } func (au *inMemoryAuthenticator) Verify(user string, pass string) bool { realPass, ok := au.storage[user] return ok && realPass == pass } func (au *inMemoryAuthenticator) Users() []string { return au.usernames } func NewAuthenticator(users []AuthUser) Authenticator { if len(users) == 0 { return nil } au := &inMemoryAuthenticator{ storage: make(map[string]string), usernames: make([]string, 0, len(users)), } for _, user := range users { au.storage[user.User] = user.Pass au.usernames = append(au.usernames, user.User) } return au } var AlwaysValid Authenticator = alwaysValid{} type alwaysValid struct{} func (alwaysValid) Verify(string, string) bool { return true } func (alwaysValid) Users() []string { return nil } ================================================ FILE: core/Clash.Meta/component/ca/auth.go ================================================ package ca import ( "github.com/metacubex/tls" ) type ClientAuthType = tls.ClientAuthType const ( NoClientCert = tls.NoClientCert RequestClientCert = tls.RequestClientCert RequireAnyClientCert = tls.RequireAnyClientCert VerifyClientCertIfGiven = tls.VerifyClientCertIfGiven RequireAndVerifyClientCert = tls.RequireAndVerifyClientCert ) func ClientAuthTypeFromString(s string) ClientAuthType { switch s { case "request": return RequestClientCert case "require-any": return RequireAnyClientCert case "verify-if-given": return VerifyClientCertIfGiven case "require-and-verify": return RequireAndVerifyClientCert default: return NoClientCert } } func ClientAuthTypeToString(t ClientAuthType) string { switch t { case RequestClientCert: return "request" case RequireAnyClientCert: return "require-any" case VerifyClientCertIfGiven: return "verify-if-given" case RequireAndVerifyClientCert: return "require-and-verify" default: return "" } } ================================================ FILE: core/Clash.Meta/component/ca/ca-certificates.crt ================================================ ================================================ FILE: core/Clash.Meta/component/ca/config.go ================================================ package ca import ( "crypto/x509" _ "embed" "errors" "fmt" "os" "strconv" "sync" "github.com/metacubex/mihomo/common/once" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/tls" ) var globalCertPool *x509.CertPool var mutex sync.RWMutex var errNotMatch = errors.New("certificate fingerprints do not match") //go:embed ca-certificates.crt var _CaCertificates []byte var DisableEmbedCa, _ = strconv.ParseBool(os.Getenv("DISABLE_EMBED_CA")) var DisableSystemCa, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_CA")) func AddCertificate(certificate string) error { mutex.Lock() defer mutex.Unlock() if certificate == "" { return fmt.Errorf("certificate is empty") } if globalCertPool == nil { initializeCertPool() } if globalCertPool.AppendCertsFromPEM([]byte(certificate)) { return nil } else if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil { globalCertPool.AddCert(cert) return nil } else { return fmt.Errorf("add certificate failed") } } func initializeCertPool() { var err error if DisableSystemCa { globalCertPool = x509.NewCertPool() } else { globalCertPool, err = x509.SystemCertPool() if err != nil { globalCertPool = x509.NewCertPool() } } if !DisableEmbedCa { globalCertPool.AppendCertsFromPEM(_CaCertificates) } } func ResetCertificate() { mutex.Lock() defer mutex.Unlock() initializeCertPool() } func GetCertPool() *x509.CertPool { mutex.Lock() defer mutex.Unlock() if globalCertPool == nil { initializeCertPool() } return globalCertPool } type Option struct { TLSConfig *tls.Config Fingerprint string ZeroTrust bool Certificate string PrivateKey string } func GetTLSConfig(opt Option) (tlsConfig *tls.Config, err error) { tlsConfig = opt.TLSConfig if tlsConfig == nil { tlsConfig = &tls.Config{} } tlsConfig.Time = ntp.Now if opt.ZeroTrust { tlsConfig.RootCAs = zeroTrustCertPool() } else { tlsConfig.RootCAs = GetCertPool() } if len(opt.Fingerprint) > 0 { verifier, err := NewFingerprintVerifier(opt.Fingerprint, tlsConfig.Time) if err != nil { return nil, err } tlsConfig.VerifyConnection = func(state tls.ConnectionState) error { // [ConnectionState.ServerName] can return the actual ServerName needed for verification, // avoiding inconsistencies caused by [tlsConfig.ServerName] being modified after the [NewFingerprintVerifier] call. // https://github.com/golang/go/issues/36736#issuecomment-587925536 return verifier(state.PeerCertificates, state.ServerName) } tlsConfig.InsecureSkipVerify = true } if len(opt.Certificate) > 0 || len(opt.PrivateKey) > 0 { certLoader, err := NewTLSKeyPairLoader(opt.Certificate, opt.PrivateKey) if err != nil { return nil, err } tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return certLoader() } } return tlsConfig, nil } var zeroTrustCertPool = once.OnceValue(func() *x509.CertPool { if len(_CaCertificates) != 0 { // always using embed cert first zeroTrustCertPool := x509.NewCertPool() if zeroTrustCertPool.AppendCertsFromPEM(_CaCertificates) { return zeroTrustCertPool } } return nil // fallback to system pool }) ================================================ FILE: core/Clash.Meta/component/ca/fingerprint.go ================================================ package ca import ( "bytes" "crypto/sha256" "crypto/x509" "encoding/hex" "fmt" "strings" "time" ) // NewFingerprintVerifier returns a function that verifies whether a certificate's SHA-256 fingerprint matches the given one. func NewFingerprintVerifier(fingerprint string, time func() time.Time) (func(certs []*x509.Certificate, serverName string) error, error) { switch fingerprint { case "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random", "randomized": // WTF??? return nil, fmt.Errorf("`fingerprint` is used for TLS certificate pinning. If you need to specify the browser fingerprint, use `client-fingerprint`") } fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1)) fpByte, err := hex.DecodeString(fingerprint) if err != nil { return nil, fmt.Errorf("fingerprint string decode error: %w", err) } if len(fpByte) != 32 { return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint") } return func(certs []*x509.Certificate, serverName string) error { // ssl pining for i, cert := range certs { hash := sha256.Sum256(cert.Raw) if bytes.Equal(fpByte, hash[:]) { if i > 0 { // When the fingerprint matches a non-leaf certificate, // the certificate chain validity is verified using the certificate as the trusted root certificate. opts := x509.VerifyOptions{ Roots: x509.NewCertPool(), Intermediates: x509.NewCertPool(), DNSName: serverName, } if time != nil { opts.CurrentTime = time() } opts.Roots.AddCert(certs[i]) for _, cert := range certs[1 : i+1] { // stop at i opts.Intermediates.AddCert(cert) } _, err := certs[0].Verify(opts) return err } return nil } } return errNotMatch }, nil } // CalculateFingerprint computes the SHA-256 fingerprint of the given DER-encoded certificate and returns it as a hex string. func CalculateFingerprint(certDER []byte) string { hash := sha256.Sum256(certDER) return hex.EncodeToString(hash[:]) } ================================================ FILE: core/Clash.Meta/component/ca/fingerprint_test.go ================================================ package ca import ( "crypto/x509" "encoding/pem" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFingerprintVerifierLeaf(t *testing.T) { leafFingerprint := CalculateFingerprint(leafCert.Raw) verifier, err := NewFingerprintVerifier(leafFingerprint, certTime) require.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, "") assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, leafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, wrongLeafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, "") assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, leafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, wrongLeafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, "") assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, leafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, wrongLeafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, wrongLeafServerName) assert.Error(t, err) } func TestFingerprintVerifierIntermediate(t *testing.T) { intermediateFingerprint := CalculateFingerprint(intermediateCert.Raw) verifier, err := NewFingerprintVerifier(intermediateFingerprint, certTime) require.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, "") assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, leafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, "") assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, leafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, wrongLeafServerName) assert.Error(t, err) } func TestFingerprintVerifierRoot(t *testing.T) { rootFingerprint := CalculateFingerprint(rootCert.Raw) verifier, err := NewFingerprintVerifier(rootFingerprint, certTime) require.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, "") assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, leafServerName) assert.NoError(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, smimeIntermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafCert, intermediateCert, smimeRootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{leafWithInvalidHashCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, rootCert}, wrongLeafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, "") assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, leafServerName) assert.Error(t, err) err = verifier([]*x509.Certificate{smimeLeafCert, intermediateCert, smimeRootCert}, wrongLeafServerName) assert.Error(t, err) } var rootPEM, _ = pem.Decode([]byte(gtsRoot)) var rootCert, _ = x509.ParseCertificate(rootPEM.Bytes) var intermediatePEM, _ = pem.Decode([]byte(gtsIntermediate)) var intermediateCert, _ = x509.ParseCertificate(intermediatePEM.Bytes) var leafPEM, _ = pem.Decode([]byte(googleLeaf)) var leafCert, _ = x509.ParseCertificate(leafPEM.Bytes) var leafWithInvalidHashPEM, _ = pem.Decode([]byte(googleLeafWithInvalidHash)) var leafWithInvalidHashCert, _ = x509.ParseCertificate(leafWithInvalidHashPEM.Bytes) var smimeRootPEM, _ = pem.Decode([]byte(smimeRoot)) var smimeRootCert, _ = x509.ParseCertificate(smimeRootPEM.Bytes) var smimeIntermediatePEM, _ = pem.Decode([]byte(smimeIntermediate)) var smimeIntermediateCert, _ = x509.ParseCertificate(smimeIntermediatePEM.Bytes) var smimeLeafPEM, _ = pem.Decode([]byte(smimeLeaf)) var smimeLeafCert, _ = x509.ParseCertificate(smimeLeafPEM.Bytes) var certTime = func() time.Time { return time.Unix(1677615892, 0) } const leafServerName = "www.google.com" const wrongLeafServerName = "www.google.com.cn" const gtsIntermediate = `-----BEGIN CERTIFICATE----- MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAw MDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp Y2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzp kgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsX lOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcm BA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKA gOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwL tmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1Ud DwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0T AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYD VR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYG CCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcw AoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQt MCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcG A1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3Br aS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcN AQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQ cSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrL RklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U +o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2Yr PxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IER lQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGs Yye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjO z23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJG AJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKw juDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl 1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd -----END CERTIFICATE-----` const gtsRoot = `-----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-----` const googleLeaf = `-----BEGIN CERTIFICATE----- MIIFUjCCBDqgAwIBAgIQERmRWTzVoz0SMeozw2RM3DANBgkqhkiG9w0BAQsFADBG MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM QzETMBEGA1UEAxMKR1RTIENBIDFDMzAeFw0yMzAxMDIwODE5MTlaFw0yMzAzMjcw ODE5MThaMBkxFzAVBgNVBAMTDnd3dy5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEAq30odrKMT54TJikMKL8S+lwoCMT5geP0u9pWjk6a wdB6i3kO+UE4ijCAmhbcZKeKaLnGJ38weZNwB1ayabCYyX7hDiC/nRcZU49LX5+o 55kDVaNn14YKkg2kCeX25HDxSwaOsNAIXKPTqiQL5LPvc4Twhl8HY51hhNWQrTEr N775eYbixEULvyVLq5BLbCOpPo8n0/MTjQ32ku1jQq3GIYMJC/Rf2VW5doF6t9zs KleflAN8OdKp0ME9OHg0T1P3yyb67T7n0SpisHbeG06AmQcKJF9g/9VPJtRf4l1Q WRPDC+6JUqzXCxAGmIRGZ7TNMxPMBW/7DRX6w8oLKVNb0wIDAQABo4ICZzCCAmMw DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQC MAAwHQYDVR0OBBYEFBnboj3lf9+Xat4oEgo6ZtIMr8ZuMB8GA1UdIwQYMBaAFIp0 f6+Fze6VzT2c0OJGFPNxNR0nMGoGCCsGAQUFBwEBBF4wXDAnBggrBgEFBQcwAYYb aHR0cDovL29jc3AucGtpLmdvb2cvZ3RzMWMzMDEGCCsGAQUFBzAChiVodHRwOi8v cGtpLmdvb2cvcmVwby9jZXJ0cy9ndHMxYzMuZGVyMBkGA1UdEQQSMBCCDnd3dy5n b29nbGUuY29tMCEGA1UdIAQaMBgwCAYGZ4EMAQIBMAwGCisGAQQB1nkCBQMwPAYD VR0fBDUwMzAxoC+gLYYraHR0cDovL2NybHMucGtpLmdvb2cvZ3RzMWMzL1FPdkow TjFzVDJBLmNybDCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AHoyjFTYty22IOo4 4FIe6YQWcDIThU070ivBOlejUutSAAABhXHHOiUAAAQDAEcwRQIgBUkikUIXdo+S 3T8PP0/cvokhUlumRE3GRWGL4WRMLpcCIQDY+bwK384mZxyXGZ5lwNRTAPNzT8Fx 1+//nbaGK3BQMAB2AOg+0No+9QY1MudXKLyJa8kD08vREWvs62nhd31tBr1uAAAB hXHHOfQAAAQDAEcwRQIgLoVydNfMFKV9IoZR+M0UuJ2zOqbxIRum7Sn9RMPOBGMC IQD1/BgzCSDTvYvco6kpB6ifKSbg5gcb5KTnYxQYwRW14TANBgkqhkiG9w0BAQsF AAOCAQEA2bQQu30e3OFu0bmvQHmcqYvXBu6tF6e5b5b+hj4O+Rn7BXTTmaYX3M6p MsfRH4YVJJMB/dc3PROR2VtnKFC6gAZX+RKM6nXnZhIlOdmQnonS1ecOL19PliUd VXbwKjXqAO0Ljd9y9oXaXnyPyHmUJNI5YXAcxE+XXiOZhcZuMYyWmoEKJQ/XlSga zWfTn1IcKhA3IC7A1n/5bkkWD1Xi1mdWFQ6DQDMp//667zz7pKOgFMlB93aPDjvI c78zEqNswn6xGKXpWF5xVwdFcsx9HKhJ6UAi2bQ/KQ1yb7LPUOR6wXXWrG1cLnNP i8eNLnKL9PXQ+5SwJFCzfEhcIZuhzg== -----END CERTIFICATE-----` // googleLeafWithInvalidHash is the same as googleLeaf, but the signature // algorithm in the certificate contains a nonsense OID. const googleLeafWithInvalidHash = `-----BEGIN CERTIFICATE----- MIIFUjCCBDqgAwIBAgIQERmRWTzVoz0SMeozw2RM3DANBgkqhkiG9w0BAQ4FADBG MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM QzETMBEGA1UEAxMKR1RTIENBIDFDMzAeFw0yMzAxMDIwODE5MTlaFw0yMzAzMjcw ODE5MThaMBkxFzAVBgNVBAMTDnd3dy5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEAq30odrKMT54TJikMKL8S+lwoCMT5geP0u9pWjk6a wdB6i3kO+UE4ijCAmhbcZKeKaLnGJ38weZNwB1ayabCYyX7hDiC/nRcZU49LX5+o 55kDVaNn14YKkg2kCeX25HDxSwaOsNAIXKPTqiQL5LPvc4Twhl8HY51hhNWQrTEr N775eYbixEULvyVLq5BLbCOpPo8n0/MTjQ32ku1jQq3GIYMJC/Rf2VW5doF6t9zs KleflAN8OdKp0ME9OHg0T1P3yyb67T7n0SpisHbeG06AmQcKJF9g/9VPJtRf4l1Q WRPDC+6JUqzXCxAGmIRGZ7TNMxPMBW/7DRX6w8oLKVNb0wIDAQABo4ICZzCCAmMw DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQC MAAwHQYDVR0OBBYEFBnboj3lf9+Xat4oEgo6ZtIMr8ZuMB8GA1UdIwQYMBaAFIp0 f6+Fze6VzT2c0OJGFPNxNR0nMGoGCCsGAQUFBwEBBF4wXDAnBggrBgEFBQcwAYYb aHR0cDovL29jc3AucGtpLmdvb2cvZ3RzMWMzMDEGCCsGAQUFBzAChiVodHRwOi8v cGtpLmdvb2cvcmVwby9jZXJ0cy9ndHMxYzMuZGVyMBkGA1UdEQQSMBCCDnd3dy5n b29nbGUuY29tMCEGA1UdIAQaMBgwCAYGZ4EMAQIBMAwGCisGAQQB1nkCBQMwPAYD VR0fBDUwMzAxoC+gLYYraHR0cDovL2NybHMucGtpLmdvb2cvZ3RzMWMzL1FPdkow TjFzVDJBLmNybDCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AHoyjFTYty22IOo4 4FIe6YQWcDIThU070ivBOlejUutSAAABhXHHOiUAAAQDAEcwRQIgBUkikUIXdo+S 3T8PP0/cvokhUlumRE3GRWGL4WRMLpcCIQDY+bwK384mZxyXGZ5lwNRTAPNzT8Fx 1+//nbaGK3BQMAB2AOg+0No+9QY1MudXKLyJa8kD08vREWvs62nhd31tBr1uAAAB hXHHOfQAAAQDAEcwRQIgLoVydNfMFKV9IoZR+M0UuJ2zOqbxIRum7Sn9RMPOBGMC IQD1/BgzCSDTvYvco6kpB6ifKSbg5gcb5KTnYxQYwRW14TANBgkqhkiG9w0BAQ4F AAOCAQEA2bQQu30e3OFu0bmvQHmcqYvXBu6tF6e5b5b+hj4O+Rn7BXTTmaYX3M6p MsfRH4YVJJMB/dc3PROR2VtnKFC6gAZX+RKM6nXnZhIlOdmQnonS1ecOL19PliUd VXbwKjXqAO0Ljd9y9oXaXnyPyHmUJNI5YXAcxE+XXiOZhcZuMYyWmoEKJQ/XlSga zWfTn1IcKhA3IC7A1n/5bkkWD1Xi1mdWFQ6DQDMp//667zz7pKOgFMlB93aPDjvI c78zEqNswn6xGKXpWF5xVwdFcsx9HKhJ6UAi2bQ/KQ1yb7LPUOR6wXXWrG1cLnNP i8eNLnKL9PXQ+5SwJFCzfEhcIZuhzg== -----END CERTIFICATE-----` const smimeLeaf = `-----BEGIN CERTIFICATE----- MIIIPDCCBiSgAwIBAgIQaMDxFS0pOMxZZeOBxoTJtjANBgkqhkiG9w0BAQsFADCB nTELMAkGA1UEBhMCRVMxFDASBgNVBAoMC0laRU5QRSBTLkEuMTowOAYDVQQLDDFB WlogWml1cnRhZ2lyaSBwdWJsaWtvYSAtIENlcnRpZmljYWRvIHB1YmxpY28gU0NB MTwwOgYDVQQDDDNFQUVrbyBIZXJyaSBBZG1pbmlzdHJhemlvZW4gQ0EgLSBDQSBB QVBQIFZhc2NhcyAoMikwHhcNMTcwNzEyMDg1MzIxWhcNMjEwNzEyMDg1MzIxWjCC AQwxDzANBgNVBAoMBklaRU5QRTE4MDYGA1UECwwvWml1cnRhZ2lyaSBrb3Jwb3Jh dGlib2EtQ2VydGlmaWNhZG8gY29ycG9yYXRpdm8xQzBBBgNVBAsMOkNvbmRpY2lv bmVzIGRlIHVzbyBlbiB3d3cuaXplbnBlLmNvbSBub2xhIGVyYWJpbGkgamFraXRl a28xFzAVBgNVBC4TDi1kbmkgOTk5OTk5ODlaMSQwIgYDVQQDDBtDT1JQT1JBVElW TyBGSUNUSUNJTyBBQ1RJVk8xFDASBgNVBCoMC0NPUlBPUkFUSVZPMREwDwYDVQQE DAhGSUNUSUNJTzESMBAGA1UEBRMJOTk5OTk5ODlaMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEAwVOMwUDfBtsH0XuxYnb+v/L774jMH8valX7RPH8cl2Lb SiqSo0RchW2RGA2d1yuYHlpChC9jGmt0X/g66/E/+q2hUJlfJtqVDJFwtFYV4u2S yzA3J36V4PRkPQrKxAsbzZriFXAF10XgiHQz9aVeMMJ9GBhmh9+DK8Tm4cMF6i8l +AuC35KdngPF1x0ealTYrYZplpEJFO7CiW42aLi6vQkDR2R7nmZA4AT69teqBWsK 0DZ93/f0G/3+vnWwNTBF0lB6dIXoaz8OMSyHLqGnmmAtMrzbjAr/O/WWgbB/BqhR qjJQ7Ui16cuDldXaWQ/rkMzsxmsAox0UF+zdQNvXUQIDAQABo4IDBDCCAwAwgccG A1UdEgSBvzCBvIYVaHR0cDovL3d3dy5pemVucGUuY29tgQ9pbmZvQGl6ZW5wZS5j b22kgZEwgY4xRzBFBgNVBAoMPklaRU5QRSBTLkEuIC0gQ0lGIEEwMTMzNzI2MC1S TWVyYy5WaXRvcmlhLUdhc3RlaXogVDEwNTUgRjYyIFM4MUMwQQYDVQQJDDpBdmRh IGRlbCBNZWRpdGVycmFuZW8gRXRvcmJpZGVhIDE0IC0gMDEwMTAgVml0b3JpYS1H YXN0ZWl6MB4GA1UdEQQXMBWBE2ZpY3RpY2lvQGl6ZW5wZS5ldXMwDgYDVR0PAQH/ BAQDAgXgMCkGA1UdJQQiMCAGCCsGAQUFBwMCBggrBgEFBQcDBAYKKwYBBAGCNxQC AjAdBgNVHQ4EFgQUyeoOD4cgcljKY0JvrNuX2waFQLAwHwYDVR0jBBgwFoAUwKlK 90clh/+8taaJzoLSRqiJ66MwggEnBgNVHSAEggEeMIIBGjCCARYGCisGAQQB8zkB AQEwggEGMDMGCCsGAQUFBwIBFidodHRwOi8vd3d3Lml6ZW5wZS5jb20vcnBhc2Nh Y29ycG9yYXRpdm8wgc4GCCsGAQUFBwICMIHBGoG+Wml1cnRhZ2lyaWEgRXVza2Fs IEF1dG9ub21pYSBFcmtpZGVnb2tvIHNla3RvcmUgcHVibGlrb2tvIGVyYWt1bmRl ZW4gYmFybmUtc2FyZWV0YW4gYmFrYXJyaWsgZXJhYmlsIGRhaXRla2UuIFVzbyBy ZXN0cmluZ2lkbyBhbCBhbWJpdG8gZGUgcmVkZXMgaW50ZXJuYXMgZGUgRW50aWRh ZGVzIGRlbCBTZWN0b3IgUHVibGljbyBWYXNjbzAyBggrBgEFBQcBAQQmMCQwIgYI KwYBBQUHMAGGFmh0dHA6Ly9vY3NwLml6ZW5wZS5jb20wOgYDVR0fBDMwMTAvoC2g K4YpaHR0cDovL2NybC5pemVucGUuY29tL2NnaS1iaW4vY3JsaW50ZXJuYTIwDQYJ KoZIhvcNAQELBQADggIBAIy5PQ+UZlCRq6ig43vpHwlwuD9daAYeejV0Q+ZbgWAE GtO0kT/ytw95ZEJMNiMw3fYfPRlh27ThqiT0VDXZJDlzmn7JZd6QFcdXkCsiuv4+ ZoXAg/QwnA3SGUUO9aVaXyuOIIuvOfb9MzoGp9xk23SMV3eiLAaLMLqwB5DTfBdt BGI7L1MnGJBv8RfP/TL67aJ5bgq2ri4S8vGHtXSjcZ0+rCEOLJtmDNMnTZxancg3 /H5edeNd+n6Z48LO+JHRxQufbC4mVNxVLMIP9EkGUejlq4E4w6zb5NwCQczJbSWL i31rk2orsNsDlyaLGsWZp3JSNX6RmodU4KAUPor4jUJuUhrrm3Spb73gKlV/gcIw bCE7mML1Kss3x1ySaXsis6SZtLpGWKkW2iguPWPs0ydV6RPhmsCxieMwPPIJ87vS 5IejfgyBae7RSuAIHyNFy4uI5xwvwUFf6OZ7az8qtW7ImFOgng3Ds+W9k1S2CNTx d0cnKTfA6IpjGo8EeHcxnIXT8NPImWaRj0qqonvYady7ci6U4m3lkNSdXNn1afgw mYust+gxVtOZs1gk2MUCgJ1V1X+g7r/Cg7viIn6TLkLrpS1kS1hvMqkl9M+7XqPo Qd95nJKOkusQpy99X4dF/lfbYAQnnjnqh3DLD2gvYObXFaAYFaiBKTiMTV2X72F+ -----END CERTIFICATE-----` const smimeIntermediate = `-----BEGIN CERTIFICATE----- MIIHNzCCBSGgAwIBAgIQJMXIqlZvjuhMvqcFXOFkpDALBgkqhkiG9w0BAQswODEL MAkGA1UEBhMCRVMxFDASBgNVBAoMC0laRU5QRSBTLkEuMRMwEQYDVQQDDApJemVu cGUuY29tMB4XDTEwMTAyMDA4MjMzM1oXDTM3MTIxMjIzMDAwMFowgZ0xCzAJBgNV BAYTAkVTMRQwEgYDVQQKDAtJWkVOUEUgUy5BLjE6MDgGA1UECwwxQVpaIFppdXJ0 YWdpcmkgcHVibGlrb2EgLSBDZXJ0aWZpY2FkbyBwdWJsaWNvIFNDQTE8MDoGA1UE AwwzRUFFa28gSGVycmkgQWRtaW5pc3RyYXppb2VuIENBIC0gQ0EgQUFQUCBWYXNj YXMgKDIpMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoIM7nEdI0N1h rR5T4xuV/usKDoMIasaiKvfLhbwxaNtTt+a7W/6wV5bv3svQFIy3sUXjjdzV1nG2 To2wo/YSPQiOt8exWvOapvL21ogiof+kelWnXFjWaKJI/vThHYLgIYEMj/y4HdtU ojI646rZwqsb4YGAopwgmkDfUh5jOhV2IcYE3TgJAYWVkj6jku9PLaIsHiarAHjD PY8dig8a4SRv0gm5Yk7FXLmW1d14oxQBDeHZ7zOEXfpafxdEDO2SNaRJjpkh8XRr PGqkg2y1Q3gT6b4537jz+StyDIJ3omylmlJsGCwqT7p8mEqjGJ5kC5I2VnjXKuNn soShc72khWZVUJiJo5SGuAkNE2ZXqltBVm5Jv6QweQKsX6bkcMc4IZok4a+hx8FM 8IBpGf/I94pU6HzGXqCyc1d46drJgDY9mXa+6YDAJFl3xeXOOW2iGCfwXqhiCrKL MYvyMZzqF3QH5q4nb3ZnehYvraeMFXJXDn+Utqp8vd2r7ShfQJz01KtM4hgKdgSg jtW+shkVVN5ng/fPN85ovfAH2BHXFfHmQn4zKsYnLitpwYM/7S1HxlT61cdQ7Nnk 3LZTYEgAoOmEmdheklT40WAYakksXGM5VrzG7x9S7s1Tm+Vb5LSThdHC8bxxwyTb KsDRDNJ84N9fPDO6qHnzaL2upQ43PycCAwEAAaOCAdkwggHVMIHHBgNVHREEgb8w gbyGFWh0dHA6Ly93d3cuaXplbnBlLmNvbYEPaW5mb0BpemVucGUuY29tpIGRMIGO MUcwRQYDVQQKDD5JWkVOUEUgUy5BLiAtIENJRiBBMDEzMzcyNjAtUk1lcmMuVml0 b3JpYS1HYXN0ZWl6IFQxMDU1IEY2MiBTODFDMEEGA1UECQw6QXZkYSBkZWwgTWVk aXRlcnJhbmVvIEV0b3JiaWRlYSAxNCAtIDAxMDEwIFZpdG9yaWEtR2FzdGVpejAP BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUwKlK90cl h/+8taaJzoLSRqiJ66MwHwYDVR0jBBgwFoAUHRxlDqjyJXu0kc/ksbHmvVV0bAUw OgYDVR0gBDMwMTAvBgRVHSAAMCcwJQYIKwYBBQUHAgEWGWh0dHA6Ly93d3cuaXpl bnBlLmNvbS9jcHMwNwYIKwYBBQUHAQEEKzApMCcGCCsGAQUFBzABhhtodHRwOi8v b2NzcC5pemVucGUuY29tOjgwOTQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2Ny bC5pemVucGUuY29tL2NnaS1iaW4vYXJsMjALBgkqhkiG9w0BAQsDggIBAMbjc3HM 3DG9ubWPkzsF0QsktukpujbTTcGk4h20G7SPRy1DiiTxrRzdAMWGjZioOP3/fKCS M539qH0M+gsySNie+iKlbSZJUyE635T1tKw+G7bDUapjlH1xyv55NC5I6wCXGC6E 3TEP5B/E7dZD0s9E4lS511ubVZivFgOzMYo1DO96diny/N/V1enaTCpRl1qH1OyL xUYTijV4ph2gL6exwuG7pxfRcVNHYlrRaXWfTz3F6NBKyULxrI3P/y6JAtN1GqT4 VF/+vMygx22n0DufGepBwTQz6/rr1ulSZ+eMnuJiTXgh/BzQnkUsXTb8mHII25iR 0oYF2qAsk6ecWbLiDpkHKIDHmML21MZE13MS8NSvTHoqJO4LyAmDe6SaeNHtrPlK b6mzE1BN2ug+ZaX8wLA5IMPFaf0jKhb/Cxu8INsxjt00brsErCc9ip1VNaH0M4bi 1tGxfiew2436FaeyUxW7Pl6G5GgkNbuUc7QIoRy06DdU/U38BxW3uyJMY60zwHvS FlKAn0OvYp4niKhAJwaBVN3kowmJuOU5Rid+TUnfyxbJ9cttSgzaF3hP/N4zgMEM 5tikXUskeckt8LUK96EH0QyssavAMECUEb/xrupyRdYWwjQGvNLq6T5+fViDGyOw k+lzD44wofy8paAy9uC9Owae0zMEzhcsyRm7 -----END CERTIFICATE-----` const smimeRoot = `-----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-----` ================================================ FILE: core/Clash.Meta/component/ca/fix_windows.go ================================================ package ca import ( "github.com/metacubex/mihomo/constant/features" ) func init() { // crypto/x509: certificate validation in Windows fails to validate IP in SAN // https://github.com/golang/go/issues/37176 // As far as I can tell this is still the case on most older versions of Windows (but seems to be fixed in 10) if features.WindowsMajorVersion < 10 && len(_CaCertificates) > 0 { DisableSystemCa = true } } ================================================ FILE: core/Clash.Meta/component/ca/keypair.go ================================================ package ca import ( "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "math/big" "os" "runtime" "sync" "time" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/fswatch" "github.com/metacubex/tls" ) // NewTLSKeyPairLoader creates a loader function for TLS key pairs from the provided certificate and private key data or file paths. // If both certificate and privateKey are empty, generates a random TLS RSA key pair. func NewTLSKeyPairLoader(certificate, privateKey string) (func() (*tls.Certificate, error), error) { if certificate == "" && privateKey == "" { var err error certificate, privateKey, _, err = NewRandomTLSKeyPair(KeyPairTypeRSA) if err != nil { return nil, err } } cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) if painTextErr == nil { return func() (*tls.Certificate, error) { return &cert, nil }, nil } certificate = C.Path.Resolve(certificate) privateKey = C.Path.Resolve(privateKey) var loadErr error if !C.Path.IsSafePath(certificate) { loadErr = C.Path.ErrNotSafePath(certificate) } else if !C.Path.IsSafePath(privateKey) { loadErr = C.Path.ErrNotSafePath(privateKey) } else { cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) } if loadErr != nil { return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } gcFlag := new(os.File) // tiny (on the order of 16 bytes or less) and pointer-free objects may never run the finalizer, so we choose new an os.File updateMutex := sync.RWMutex{} if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{certificate, privateKey}, Callback: func(path string) { updateMutex.Lock() defer updateMutex.Unlock() if newCert, err := tls.LoadX509KeyPair(certificate, privateKey); err == nil { cert = newCert } }}); err == nil { if err = watcher.Start(); err == nil { runtime.SetFinalizer(gcFlag, func(f *os.File) { _ = watcher.Close() }) } } return func() (*tls.Certificate, error) { defer runtime.KeepAlive(gcFlag) updateMutex.RLock() defer updateMutex.RUnlock() return &cert, nil }, nil } func LoadCertificates(certificate string) (*x509.CertPool, error) { pool := x509.NewCertPool() if pool.AppendCertsFromPEM([]byte(certificate)) { return pool, nil } painTextErr := fmt.Errorf("invalid certificate: %s", certificate) certificate = C.Path.Resolve(certificate) var loadErr error if !C.Path.IsSafePath(certificate) { loadErr = C.Path.ErrNotSafePath(certificate) } else { certPEMBlock, err := os.ReadFile(certificate) if pool.AppendCertsFromPEM(certPEMBlock) { return pool, nil } loadErr = err } if loadErr != nil { return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } //TODO: support dynamic update pool too // blocked by: https://github.com/golang/go/issues/64796 // maybe we can direct add `GetRootCAs` and `GetClientCAs` to ourselves tls fork return pool, nil } type KeyPairType string const ( KeyPairTypeRSA KeyPairType = "rsa" KeyPairTypeP256 KeyPairType = "p256" KeyPairTypeP384 KeyPairType = "p384" KeyPairTypeEd25519 KeyPairType = "ed25519" ) // NewRandomTLSKeyPair generates a random TLS key pair based on the specified KeyPairType and returns it with a SHA256 fingerprint. // Note: Most browsers do not support KeyPairTypeEd25519 type of certificate, and utls.UConn will also reject this type of certificate. func NewRandomTLSKeyPair(keyPairType KeyPairType) (certificate string, privateKey string, fingerprint string, err error) { var key crypto.Signer switch keyPairType { case KeyPairTypeRSA: key, err = rsa.GenerateKey(rand.Reader, 2048) case KeyPairTypeP256: key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) case KeyPairTypeP384: key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) case KeyPairTypeEd25519: _, key, err = ed25519.GenerateKey(rand.Reader) default: // fallback to KeyPairTypeRSA key, err = rsa.GenerateKey(rand.Reader, 2048) } if err != nil { return } template := x509.Certificate{ SerialNumber: big.NewInt(1), NotBefore: time.Now().Add(-time.Hour * 24 * 365), NotAfter: time.Now().Add(time.Hour * 24 * 365), } certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) if err != nil { return } privBytes, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { return } fingerprint = CalculateFingerprint(certDER) privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})) certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})) return } ================================================ FILE: core/Clash.Meta/component/cidr/ipcidr_set.go ================================================ package cidr import ( "fmt" "net/netip" "unsafe" "go4.org/netipx" ) type IpCidrSet struct { // must same with netipx.IPSet rr []netipx.IPRange } func NewIpCidrSet() *IpCidrSet { return &IpCidrSet{} } func (set *IpCidrSet) AddIpCidrForString(ipCidr string) error { prefix, err := netip.ParsePrefix(ipCidr) if err != nil { return err } return set.AddIpCidr(prefix) } func (set *IpCidrSet) AddIpCidr(ipCidr netip.Prefix) (err error) { if r := netipx.RangeOfPrefix(ipCidr); r.IsValid() { set.rr = append(set.rr, r) } else { err = fmt.Errorf("not valid ipcidr range: %s", ipCidr) } return } func (set *IpCidrSet) IsContainForString(ipString string) bool { ip, err := netip.ParseAddr(ipString) if err != nil { return false } return set.IsContain(ip) } func (set *IpCidrSet) IsContain(ip netip.Addr) bool { return set.ToIPSet().Contains(ip.WithZone("")) } // MatchIp implements C.IpMatcher func (set *IpCidrSet) MatchIp(ip netip.Addr) bool { if set.IsEmpty() { return false } return set.IsContain(ip) } func (set *IpCidrSet) Merge() error { var b netipx.IPSetBuilder b.AddSet(set.ToIPSet()) i, err := b.IPSet() if err != nil { return err } set.fromIPSet(i) return nil } func (set *IpCidrSet) IsEmpty() bool { return set == nil || len(set.rr) == 0 } func (set *IpCidrSet) Foreach(f func(prefix netip.Prefix) bool) { for _, r := range set.rr { for _, prefix := range r.Prefixes() { if !f(prefix) { return } } } } // ToIPSet not safe convert to *netipx.IPSet // be careful, must be used after Merge func (set *IpCidrSet) ToIPSet() *netipx.IPSet { return (*netipx.IPSet)(unsafe.Pointer(set)) } func (set *IpCidrSet) fromIPSet(i *netipx.IPSet) { *set = *(*IpCidrSet)(unsafe.Pointer(i)) } ================================================ FILE: core/Clash.Meta/component/cidr/ipcidr_set_bin.go ================================================ package cidr import ( "encoding/binary" "errors" "io" "net/netip" "go4.org/netipx" ) func (ss *IpCidrSet) WriteBin(w io.Writer) (err error) { // version _, err = w.Write([]byte{1}) if err != nil { return err } // rr err = binary.Write(w, binary.BigEndian, int64(len(ss.rr))) if err != nil { return err } for _, r := range ss.rr { err = binary.Write(w, binary.BigEndian, r.From().As16()) if err != nil { return err } err = binary.Write(w, binary.BigEndian, r.To().As16()) if err != nil { return err } } return nil } func ReadIpCidrSet(r io.Reader) (ss *IpCidrSet, err error) { // version version := make([]byte, 1) _, err = io.ReadFull(r, version) if err != nil { return nil, err } if version[0] != 1 { return nil, errors.New("version is invalid") } ss = NewIpCidrSet() var length int64 // rr err = binary.Read(r, binary.BigEndian, &length) if err != nil { return nil, err } if length < 1 { return nil, errors.New("length is invalid") } ss.rr = make([]netipx.IPRange, length) for i := int64(0); i < length; i++ { var a16 [16]byte err = binary.Read(r, binary.BigEndian, &a16) if err != nil { return nil, err } from := netip.AddrFrom16(a16).Unmap() err = binary.Read(r, binary.BigEndian, &a16) if err != nil { return nil, err } to := netip.AddrFrom16(a16).Unmap() ss.rr[i] = netipx.IPRangeFrom(from, to) } return ss, nil } ================================================ FILE: core/Clash.Meta/component/cidr/ipcidr_set_test.go ================================================ package cidr import ( "testing" ) func TestIpv4(t *testing.T) { tests := []struct { name string ipCidr string ip string expected bool }{ { name: "Test Case 1", ipCidr: "149.154.160.0/20", ip: "149.154.160.0", expected: true, }, { name: "Test Case 2", ipCidr: "192.168.0.0/16", ip: "10.0.0.1", expected: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { set := &IpCidrSet{} set.AddIpCidrForString(test.ipCidr) result := set.IsContainForString(test.ip) if result != test.expected { t.Errorf("Expected result: %v, got: %v", test.expected, result) } }) } } func TestIpv6(t *testing.T) { tests := []struct { name string ipCidr string ip string expected bool }{ { name: "Test Case 1", ipCidr: "2409:8000::/20", ip: "2409:8087:1e03:21::27", expected: true, }, { name: "Test Case 2", ipCidr: "240e::/16", ip: "240e:964:ea02:100:1800::71", expected: true, }, } // Add more test cases as needed for _, test := range tests { t.Run(test.name, func(t *testing.T) { set := &IpCidrSet{} set.AddIpCidrForString(test.ipCidr) result := set.IsContainForString(test.ip) if result != test.expected { t.Errorf("Expected result: %v, got: %v", test.expected, result) } }) } } func TestMerge(t *testing.T) { tests := []struct { name string ipCidr1 string ipCidr2 string ipCidr3 string expectedLen int }{ { name: "Test Case 1", ipCidr1: "2409:8000::/20", ipCidr2: "2409:8000::/21", ipCidr3: "2409:8000::/48", expectedLen: 1, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { set := &IpCidrSet{} set.AddIpCidrForString(test.ipCidr1) set.AddIpCidrForString(test.ipCidr2) set.Merge() rangesLen := len(set.rr) if rangesLen != test.expectedLen { t.Errorf("Expected len: %v, got: %v", test.expectedLen, rangesLen) } }) } } ================================================ FILE: core/Clash.Meta/component/dhcp/conn.go ================================================ package dhcp import ( "context" "net" "net/netip" "runtime" "github.com/metacubex/mihomo/component/dialer" ) func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, error) { listenAddr := "0.0.0.0:68" if runtime.GOOS == "linux" || runtime.GOOS == "android" { listenAddr = "255.255.255.255:68" } options := []dialer.Option{ dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true), } // fallback bind on windows, because syscall bind can not receive broadcast if runtime.GOOS == "windows" { options = append(options, dialer.WithFallbackBind(true)) } return dialer.ListenPacket(ctx, "udp4", listenAddr, netip.AddrPortFrom(netip.AddrFrom4([4]byte{255, 255, 255, 255}), 67), options...) } ================================================ FILE: core/Clash.Meta/component/dhcp/dhcp.go ================================================ package dhcp import ( "context" "errors" "net" "net/netip" "github.com/metacubex/mihomo/component/iface" "github.com/insomniacslk/dhcp/dhcpv4" ) var ( ErrNotResponding = errors.New("DHCP not responding") ErrNotFound = errors.New("DNS option not found") ) func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]netip.Addr, error) { conn, err := ListenDHCPClient(context, ifaceName) if err != nil { return nil, err } defer func() { _ = conn.Close() }() result := make(chan []netip.Addr, 1) ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return nil, err } discovery, err := dhcpv4.NewDiscovery(ifaceObj.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer)) if err != nil { return nil, err } go receiveOffer(conn, discovery.TransactionID, result) _, err = conn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67}) if err != nil { return nil, err } select { case r, ok := <-result: if !ok { return nil, ErrNotFound } return r, nil case <-context.Done(): return nil, ErrNotResponding } } func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []netip.Addr) { defer close(result) buf := make([]byte, dhcpv4.MaxMessageSize) for { n, _, err := conn.ReadFrom(buf) if err != nil { return } pkt, err := dhcpv4.FromBytes(buf[:n]) if err != nil { continue } if pkt.MessageType() != dhcpv4.MessageTypeOffer { continue } if pkt.TransactionID != id { continue } dns := pkt.DNS() l := len(dns) if l == 0 { return } results := make([]netip.Addr, 0, len(dns)) for _, ip := range dns { if addr, ok := netip.AddrFromSlice(ip); ok { results = append(results, addr.Unmap()) } } result <- results return } } ================================================ FILE: core/Clash.Meta/component/dialer/bind.go ================================================ package dialer import ( "net" "net/netip" "strconv" "strings" "github.com/metacubex/mihomo/component/iface" ) func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return nil, err } destination = destination.Unmap() var addr netip.Prefix switch network { case "udp4", "tcp4": addr, err = ifaceObj.PickIPv4Addr(destination) case "tcp6", "udp6": addr, err = ifaceObj.PickIPv6Addr(destination) default: if destination.IsValid() { if destination.Is4() { addr, err = ifaceObj.PickIPv4Addr(destination) } else { addr, err = ifaceObj.PickIPv6Addr(destination) } } else { addr, err = ifaceObj.PickIPv4Addr(destination) } } if err != nil { return nil, err } if strings.HasPrefix(network, "tcp") { return &net.TCPAddr{ IP: addr.Addr().AsSlice(), Port: port, }, nil } else if strings.HasPrefix(network, "udp") { return &net.UDPAddr{ IP: addr.Addr().AsSlice(), Port: port, }, nil } return nil, iface.ErrAddrNotFound } func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error { if !destination.IsGlobalUnicast() { return nil } local := uint64(0) if dialer.LocalAddr != nil { _, port, err := net.SplitHostPort(dialer.LocalAddr.String()) if err == nil { local, _ = strconv.ParseUint(port, 10, 16) } } addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local)) if err != nil { return err } dialer.LocalAddr = addr return nil } func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string, rAddrPort netip.AddrPort) (string, error) { _, port, err := net.SplitHostPort(address) if err != nil { port = "0" } local, _ := strconv.ParseUint(port, 10, 16) addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local)) if err != nil { return "", err } return addr.String(), nil } func fallbackParseNetwork(network string, addr netip.Addr) string { // fix fallbackBindIfaceToListenConfig() force bind to an ipv4 address if !strings.HasSuffix(network, "4") && !strings.HasSuffix(network, "6") && addr.Unmap().Is6() { network += "6" } return network } ================================================ FILE: core/Clash.Meta/component/dialer/bind_darwin.go ================================================ package dialer import ( "context" "net" "net/netip" "syscall" "github.com/metacubex/mihomo/component/iface" "golang.org/x/sys/unix" ) func bindControl(ifaceIdx int) controlFn { return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { addrPort, err := netip.ParseAddrPort(address) if err == nil && !addrPort.Addr().IsGlobalUnicast() { return } var innerErr error err = c.Control(func(fd uintptr) { switch network { case "tcp6", "udp6", "ip6": innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx) default: innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx) } }) if innerErr != nil { err = innerErr } return } } func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return err } addControlToDialer(dialer, bindControl(ifaceObj.Index)) return nil } func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return "", err } addControlToListenConfig(lc, bindControl(ifaceObj.Index)) return address, nil } func ParseNetwork(network string, addr netip.Addr) string { return network } ================================================ FILE: core/Clash.Meta/component/dialer/bind_linux.go ================================================ package dialer import ( "context" "net" "net/netip" "syscall" "golang.org/x/sys/unix" ) func bindControl(ifaceName string) controlFn { return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { addrPort, err := netip.ParseAddrPort(address) if err == nil && !addrPort.Addr().IsGlobalUnicast() { return } var innerErr error err = c.Control(func(fd uintptr) { innerErr = unix.BindToDevice(int(fd), ifaceName) }) if innerErr != nil { err = innerErr } return } } func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error { addControlToDialer(dialer, bindControl(ifaceName)) return nil } func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { addControlToListenConfig(lc, bindControl(ifaceName)) return address, nil } func ParseNetwork(network string, addr netip.Addr) string { return network } ================================================ FILE: core/Clash.Meta/component/dialer/bind_others.go ================================================ //go:build !linux && !darwin && !windows package dialer import ( "net" "net/netip" ) func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error { return fallbackBindIfaceToDialer(ifaceName, dialer, network, destination) } func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, network, address string, rAddrPort netip.AddrPort) (string, error) { return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address, rAddrPort) } func ParseNetwork(network string, addr netip.Addr) string { return fallbackParseNetwork(network, addr) } ================================================ FILE: core/Clash.Meta/component/dialer/bind_windows.go ================================================ package dialer import ( "context" "encoding/binary" "fmt" "net" "net/netip" "syscall" "unsafe" "github.com/metacubex/mihomo/component/iface" ) const ( IP_UNICAST_IF = 31 IPV6_UNICAST_IF = 31 ) func bind4(handle syscall.Handle, ifaceIdx int) error { var bytes [4]byte binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx)) idx := *(*uint32)(unsafe.Pointer(&bytes[0])) err := syscall.SetsockoptInt(handle, syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)) if err != nil { err = fmt.Errorf("bind4: %w", err) } return err } func bind6(handle syscall.Handle, ifaceIdx int) error { err := syscall.SetsockoptInt(handle, syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx) if err != nil { err = fmt.Errorf("bind6: %w", err) } return err } func bindControl(ifaceIdx int, rAddrPort netip.AddrPort) controlFn { return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { addrPort, err := netip.ParseAddrPort(address) if err == nil && !addrPort.Addr().IsGlobalUnicast() { return } var innerErr error err = c.Control(func(fd uintptr) { handle := syscall.Handle(fd) bind6err := bind6(handle, ifaceIdx) bind4err := bind4(handle, ifaceIdx) switch network { case "ip6", "tcp6": innerErr = bind6err case "ip4", "tcp4", "udp4": innerErr = bind4err case "udp6": // golang will set network to udp6 when listenUDP on wildcard ip (eg: ":0", "") if (!addrPort.Addr().IsValid() || addrPort.Addr().IsUnspecified()) && bind6err != nil && rAddrPort.Addr().Unmap().Is4() { // try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6 if bind4err != nil { innerErr = fmt.Errorf("%w (%s)", bind6err, bind4err) } else { innerErr = nil } } else { innerErr = bind6err } } }) if innerErr != nil { err = innerErr } return } } func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, destination netip.Addr) error { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return err } addControlToDialer(dialer, bindControl(ifaceObj.Index, netip.AddrPortFrom(destination, 0))) return nil } func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return "", err } addControlToListenConfig(lc, bindControl(ifaceObj.Index, rAddrPort)) return address, nil } func ParseNetwork(network string, addr netip.Addr) string { return network } ================================================ FILE: core/Clash.Meta/component/dialer/control.go ================================================ package dialer import ( "context" "net" "syscall" ) type controlFn = func(ctx context.Context, network, address string, c syscall.RawConn) error func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) { llc := *lc lc.Control = func(network, address string, c syscall.RawConn) (err error) { switch { case llc.Control != nil: if err = llc.Control(network, address, c); err != nil { return } } return fn(context.Background(), network, address, c) } } func addControlToDialer(d *net.Dialer, fn controlFn) { ld := *d d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { switch { case ld.ControlContext != nil: if err = ld.ControlContext(ctx, network, address, c); err != nil { return } case ld.Control != nil: if err = ld.Control(network, address, c); err != nil { return } } return fn(ctx, network, address, c) } } ================================================ FILE: core/Clash.Meta/component/dialer/dialer.go ================================================ package dialer import ( "context" "errors" "fmt" "net" "net/netip" "os" "strings" "sync" "syscall" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/mptcp" "github.com/metacubex/mihomo/component/resolver" ) const ( DefaultTCPTimeout = 5 * time.Second DefaultUDPTimeout = DefaultTCPTimeout dualStackFallbackTimeout = 300 * time.Millisecond ) var ( tcpConcurrent = atomic.NewBool(false) ) func SetTcpConcurrent(concurrent bool) { tcpConcurrent.Store(concurrent) } func GetTcpConcurrent() bool { return tcpConcurrent.Load() } func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { opt := applyOptions(options...) if opt.network == 4 || opt.network == 6 { if strings.Contains(network, "tcp") { network = "tcp" } else { network = "udp" } network = fmt.Sprintf("%s%d", network, opt.network) } ips, port, err := parseAddr(ctx, network, address, opt.resolver) if err != nil { return nil, err } tcpConcurrent := GetTcpConcurrent() switch network { case "tcp4", "tcp6", "udp4", "udp6": if tcpConcurrent { return parallelDialContext(ctx, network, ips, port, opt) } return serialDialContext(ctx, network, ips, port, opt) case "tcp", "udp": if tcpConcurrent { if opt.prefer != 4 && opt.prefer != 6 { return parallelDialContext(ctx, network, ips, port, opt) } return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt) } return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt) default: return nil, ErrorInvalidedNetworkStack } } func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) { opt := applyOptions(options...) lc := &net.ListenConfig{} if opt.addrReuse { addrReuseToListenConfig(lc) } if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA) socketHookToListenConfig(lc) } else { if opt.interfaceName == "" { opt.interfaceName = DefaultInterface.Load() } if opt.interfaceName == "" { if finder := DefaultInterfaceFinder.Load(); finder != nil { opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr().Unmap()) } } if rAddrPort.Addr().Unmap().IsLoopback() { // avoid "The requested address is not valid in its context." opt.interfaceName = "" } if opt.interfaceName != "" { bind := bindIfaceToListenConfig if opt.fallbackBind { bind = fallbackBindIfaceToListenConfig } addr, err := bind(opt.interfaceName, lc, network, address, rAddrPort) if err != nil { return nil, err } address = addr } if opt.routingMark == 0 { opt.routingMark = int(DefaultRoutingMark.Load()) } if opt.routingMark != 0 { bindMarkToListenConfig(opt.routingMark, lc, network, address) } } return lc.ListenPacket(ctx, network, address) } func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt option) (net.Conn, error) { var address string destination, port = resolver.LookupIP4P(destination, port) address = net.JoinHostPort(destination.String(), port) netDialer := opt.netDialer switch netDialer.(type) { case nil: netDialer = &net.Dialer{} case *net.Dialer: _netDialer := *netDialer.(*net.Dialer) netDialer = &_netDialer // make a copy default: return netDialer.DialContext(ctx, network, address) } dialer := netDialer.(*net.Dialer) keepalive.SetNetDialer(dialer) mptcp.SetNetDialer(dialer, opt.mpTcp) if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA) socketHookToToDialer(dialer) } else { if opt.interfaceName == "" { opt.interfaceName = DefaultInterface.Load() } if opt.interfaceName == "" { if finder := DefaultInterfaceFinder.Load(); finder != nil { opt.interfaceName = finder.FindInterfaceName(destination) } } if opt.interfaceName != "" { bind := bindIfaceToDialer if opt.fallbackBind { bind = fallbackBindIfaceToDialer } if err := bind(opt.interfaceName, dialer, network, destination); err != nil { return nil, err } } if opt.routingMark == 0 { opt.routingMark = int(DefaultRoutingMark.Load()) } if opt.routingMark != 0 { bindMarkToDialer(opt.routingMark, dialer, network, destination) } if opt.tfo && !DisableTFO { return dialTFO(ctx, *dialer, network, address) } } return dialer.DialContext(ctx, network, address) } func ICMPControl(destination netip.Addr) func(network, address string, conn syscall.RawConn) error { return func(network, address string, conn syscall.RawConn) error { if DefaultSocketHook != nil { return DefaultSocketHook(network, address, conn) } dialer := &net.Dialer{} interfaceName := DefaultInterface.Load() if interfaceName == "" { if finder := DefaultInterfaceFinder.Load(); finder != nil { interfaceName = finder.FindInterfaceName(destination) } } if interfaceName != "" { if err := bindIfaceToDialer(interfaceName, dialer, network, destination); err != nil { return err } } routingMark := int(DefaultRoutingMark.Load()) if routingMark != 0 { bindMarkToDialer(routingMark, dialer, network, destination) } if dialer.ControlContext != nil { return dialer.ControlContext(context.TODO(), network, address, conn) } return nil } } type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { ipv4s, ipv6s := resolver.SortationAddr(ips) if len(ipv4s) == 0 && len(ipv6s) == 0 { return nil, ErrorNoIpAddress } if len(ipv4s) == 0 && len(ipv6s) != 0 { return dialFn(ctx, network, ipv6s, port, opt) } if len(ipv4s) != 0 && len(ipv6s) == 0 { return dialFn(ctx, network, ipv4s, port, opt) } preferIPVersion := opt.prefer fallbackTicker := time.NewTicker(dualStackFallbackTimeout) defer fallbackTicker.Stop() results := make(chan dialResult) returned := make(chan struct{}) defer close(returned) var wg sync.WaitGroup racer := func(ips []netip.Addr, isPrimary bool) { defer wg.Done() result := dialResult{isPrimary: isPrimary} defer func() { select { case results <- result: case <-returned: if result.Conn != nil && result.error == nil { _ = result.Conn.Close() } } }() result.Conn, result.error = dialFn(ctx, network, ips, port, opt) } if len(ipv4s) != 0 { wg.Add(1) go racer(ipv4s, preferIPVersion != 6) } if len(ipv6s) != 0 { wg.Add(1) go racer(ipv6s, preferIPVersion != 4) } go func() { wg.Wait() close(results) }() var fallback dialResult var errs []error loop: for { select { case <-fallbackTicker.C: if fallback.error == nil && fallback.Conn != nil { return fallback.Conn, nil } case res, ok := <-results: if !ok { break loop } if res.error == nil { if res.isPrimary { return res.Conn, nil } fallback = res } else { if res.isPrimary { errs = append([]error{fmt.Errorf("connect failed: %w", res.error)}, errs...) } else { errs = append(errs, fmt.Errorf("connect failed: %w", res.error)) } } } } if fallback.error == nil && fallback.Conn != nil { return fallback.Conn, nil } return nil, errors.Join(errs...) } func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { if len(ips) == 0 { return nil, ErrorNoIpAddress } if len(ips) == 1 { return dialContext(ctx, network, ips[0], port, opt) } results := make(chan dialResult) returned := make(chan struct{}) defer close(returned) racer := func(ctx context.Context, ip netip.Addr) { result := dialResult{isPrimary: true, ip: ip} defer func() { select { case results <- result: case <-returned: if result.Conn != nil && result.error == nil { _ = result.Conn.Close() } } }() result.Conn, result.error = dialContext(ctx, network, ip, port, opt) } for _, ip := range ips { go racer(ctx, ip) } var errs []error for i := 0; i < len(ips); i++ { res := <-results if res.error == nil { return res.Conn, nil } errs = append(errs, res.error) } if len(errs) > 0 { return nil, errors.Join(errs...) } return nil, os.ErrDeadlineExceeded } func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { if len(ips) == 0 { return nil, ErrorNoIpAddress } var errs []error for _, ip := range ips { if conn, err := dialContext(ctx, network, ip, port, opt); err == nil { return conn, nil } else { errs = append(errs, err) } } return nil, errors.Join(errs...) } type dialResult struct { ip netip.Addr net.Conn error isPrimary bool } func parseAddr(ctx context.Context, network, address string, preferResolver resolver.Resolver) ([]netip.Addr, string, error) { host, port, err := net.SplitHostPort(address) if err != nil { return nil, "-1", err } if preferResolver == nil { preferResolver = resolver.ProxyServerHostResolver } var ips []netip.Addr switch network { case "tcp4", "udp4": ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver) case "tcp6", "udp6": ips, err = resolver.LookupIPv6WithResolver(ctx, host, preferResolver) default: ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver) } if err != nil { return nil, "-1", fmt.Errorf("dns resolve failed: %w", err) } for i, ip := range ips { if ip.Is4In6() { ips[i] = ip.Unmap() } } return ips, port, nil } type Dialer struct { Opt option } func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { return DialContext(ctx, network, address, WithOption(d.Opt)) } func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, rAddrPort, WithOption(d.Opt)) } func NewDialer(options ...Option) Dialer { opt := applyOptions(options...) return Dialer{Opt: opt} } ================================================ FILE: core/Clash.Meta/component/dialer/error.go ================================================ package dialer import ( "errors" ) var ( ErrorNoIpAddress = errors.New("no ip address") ErrorInvalidedNetworkStack = errors.New("invalided network stack") ) ================================================ FILE: core/Clash.Meta/component/dialer/mark_linux.go ================================================ //go:build linux package dialer import ( "context" "net" "net/netip" "syscall" ) func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) { addControlToDialer(dialer, bindMarkToControl(mark)) } func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) { addControlToListenConfig(lc, bindMarkToControl(mark)) } func bindMarkToControl(mark int) controlFn { return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { addrPort, err := netip.ParseAddrPort(address) if err == nil && !addrPort.Addr().IsGlobalUnicast() { return } var innerErr error err = c.Control(func(fd uintptr) { innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark) }) if innerErr != nil { err = innerErr } return } } ================================================ FILE: core/Clash.Meta/component/dialer/mark_nonlinux.go ================================================ //go:build !linux package dialer import ( "net" "net/netip" "sync" "github.com/metacubex/mihomo/log" ) var printMarkWarnOnce sync.Once func printMarkWarn() { printMarkWarnOnce.Do(func() { log.Warnln("Routing mark on socket is not supported on current platform") }) } func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) { printMarkWarn() } func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) { printMarkWarn() } ================================================ FILE: core/Clash.Meta/component/dialer/options.go ================================================ package dialer import ( "context" "net" "net/netip" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/component/resolver" ) var ( DefaultInterface = atomic.NewTypedValue[string]("") DefaultRoutingMark = atomic.NewInt32(0) DefaultInterfaceFinder = atomic.NewTypedValue[InterfaceFinder](nil) ) type InterfaceFinder interface { FindInterfaceName(destination netip.Addr) string } type NetDialer interface { DialContext(ctx context.Context, network, address string) (net.Conn, error) } type NetDialerFunc func(ctx context.Context, network, address string) (net.Conn, error) func (f NetDialerFunc) DialContext(ctx context.Context, network, address string) (net.Conn, error) { return f(ctx, network, address) } type option struct { interfaceName string fallbackBind bool addrReuse bool routingMark int network int prefer int tfo bool mpTcp bool resolver resolver.Resolver netDialer NetDialer } type Option func(opt *option) func WithInterface(name string) Option { return func(opt *option) { opt.interfaceName = name } } func WithFallbackBind(fallback bool) Option { return func(opt *option) { opt.fallbackBind = fallback } } func WithAddrReuse(reuse bool) Option { return func(opt *option) { opt.addrReuse = reuse } } func WithRoutingMark(mark int) Option { return func(opt *option) { opt.routingMark = mark } } func WithResolver(r resolver.Resolver) Option { return func(opt *option) { opt.resolver = r } } func WithPreferIPv4() Option { return func(opt *option) { opt.prefer = 4 } } func WithPreferIPv6() Option { return func(opt *option) { opt.prefer = 6 } } func WithOnlySingleStack(isIPv4 bool) Option { return func(opt *option) { if isIPv4 { opt.network = 4 } else { opt.network = 6 } } } func WithTFO(tfo bool) Option { return func(opt *option) { opt.tfo = tfo } } func WithMPTCP(mpTcp bool) Option { return func(opt *option) { opt.mpTcp = mpTcp } } func WithNetDialer(netDialer NetDialer) Option { return func(opt *option) { opt.netDialer = netDialer } } func WithOption(o option) Option { return func(opt *option) { *opt = o } } func WithOptions(options ...Option) Option { return func(opt *option) { for _, o := range options { o(opt) } } } func IsZeroOptions(opts []Option) bool { return applyOptions(opts...) == option{} } func applyOptions(options ...Option) option { opt := option{} for _, o := range options { o(&opt) } return opt } ================================================ FILE: core/Clash.Meta/component/dialer/reuse.go ================================================ package dialer import ( "context" "net" "syscall" "github.com/metacubex/mihomo/common/sockopt" ) func addrReuseToListenConfig(lc *net.ListenConfig) { addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { return sockopt.RawConnReuseaddr(c) }) } ================================================ FILE: core/Clash.Meta/component/dialer/socket_hook.go ================================================ package dialer import ( "context" "net" "syscall" ) // SocketControl // never change type traits because it's used in CMFA type SocketControl func(network, address string, conn syscall.RawConn) error // DefaultSocketHook // never change type traits because it's used in CMFA var DefaultSocketHook SocketControl func socketHookToToDialer(dialer *net.Dialer) { addControlToDialer(dialer, func(ctx context.Context, network, address string, c syscall.RawConn) error { return DefaultSocketHook(network, address, c) }) } func socketHookToListenConfig(lc *net.ListenConfig) { addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { return DefaultSocketHook(network, address, c) }) } ================================================ FILE: core/Clash.Meta/component/dialer/tfo.go ================================================ package dialer import ( "context" "io" "net" "time" "github.com/metacubex/tfo-go" ) var DisableTFO = false type tfoConn struct { net.Conn closed bool dialed chan bool cancel context.CancelFunc ctx context.Context dialFn func(ctx context.Context, earlyData []byte) (net.Conn, error) } func (c *tfoConn) Dial(earlyData []byte) (err error) { conn, err := c.dialFn(c.ctx, earlyData) if err != nil { return } c.Conn = conn c.dialed <- true return err } func (c *tfoConn) Read(b []byte) (n int, err error) { if c.closed { return 0, io.ErrClosedPipe } if c.Conn == nil { select { case <-c.ctx.Done(): return 0, io.ErrUnexpectedEOF case <-c.dialed: } } return c.Conn.Read(b) } func (c *tfoConn) Write(b []byte) (n int, err error) { if c.closed { return 0, io.ErrClosedPipe } if c.Conn == nil { if err := c.Dial(b); err != nil { return 0, err } return len(b), nil } return c.Conn.Write(b) } func (c *tfoConn) Close() error { c.closed = true c.cancel() if c.Conn == nil { return nil } return c.Conn.Close() } func (c *tfoConn) LocalAddr() net.Addr { if c.Conn == nil { return &net.TCPAddr{} } return c.Conn.LocalAddr() } func (c *tfoConn) RemoteAddr() net.Addr { if c.Conn == nil { return &net.TCPAddr{} } return c.Conn.RemoteAddr() } func (c *tfoConn) SetDeadline(t time.Time) error { if err := c.SetReadDeadline(t); err != nil { return err } return c.SetWriteDeadline(t) } func (c *tfoConn) SetReadDeadline(t time.Time) error { if c.Conn == nil { return nil } return c.Conn.SetReadDeadline(t) } func (c *tfoConn) SetWriteDeadline(t time.Time) error { if c.Conn == nil { return nil } return c.Conn.SetWriteDeadline(t) } func (c *tfoConn) Upstream() any { if c.Conn == nil { // ensure return a nil interface not an interface with nil value return nil } return c.Conn } func (c *tfoConn) NeedAdditionalReadDeadline() bool { return c.Conn == nil } func (c *tfoConn) NeedHandshake() bool { return c.Conn == nil } func (c *tfoConn) ReaderReplaceable() bool { return c.Conn != nil } func (c *tfoConn) WriterReplaceable() bool { return c.Conn != nil } func dialTFO(ctx context.Context, netDialer net.Dialer, network, address string) (net.Conn, error) { ctx, cancel := context.WithTimeout(context.Background(), DefaultTCPTimeout) dialer := tfo.Dialer{Dialer: netDialer, DisableTFO: false} return &tfoConn{ dialed: make(chan bool, 1), cancel: cancel, ctx: ctx, dialFn: func(ctx context.Context, earlyData []byte) (net.Conn, error) { return dialer.DialContext(ctx, network, address, earlyData) }, }, nil } ================================================ FILE: core/Clash.Meta/component/dialer/tfo_windows.go ================================================ package dialer import "github.com/metacubex/mihomo/constant/features" func init() { // According to MSDN, this option is available since Windows 10, 1607 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms738596(v=vs.85).aspx if features.WindowsMajorVersion < 10 || (features.WindowsMajorVersion == 10 && features.WindowsBuildNumber < 14393) { DisableTFO = true } } ================================================ FILE: core/Clash.Meta/component/ech/ech.go ================================================ package ech import ( "context" "fmt" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/tls" ) type Config struct { GetEncryptedClientHelloConfigList func(ctx context.Context, serverName string) ([]byte, error) } func (cfg *Config) ClientHandle(ctx context.Context, tlsConfig *tls.Config) (err error) { if cfg == nil { return nil } echConfigList, err := cfg.GetEncryptedClientHelloConfigList(ctx, tlsConfig.ServerName) if err != nil { return fmt.Errorf("resolve ECH config error: %w", err) } tlsConfig.EncryptedClientHelloConfigList = echConfigList if tlsConfig.MinVersion != 0 && tlsConfig.MinVersion < tls.VersionTLS13 { tlsConfig.MinVersion = tls.VersionTLS13 } if tlsConfig.MaxVersion != 0 && tlsConfig.MaxVersion < tls.VersionTLS13 { tlsConfig.MaxVersion = tls.VersionTLS13 } return nil } func (cfg *Config) ClientHandleUTLS(ctx context.Context, tlsConfig *tlsC.Config) (err error) { if cfg == nil { return nil } echConfigList, err := cfg.GetEncryptedClientHelloConfigList(ctx, tlsConfig.ServerName) if err != nil { return fmt.Errorf("resolve ECH config error: %w", err) } tlsConfig.EncryptedClientHelloConfigList = echConfigList if tlsConfig.MinVersion != 0 && tlsConfig.MinVersion < tlsC.VersionTLS13 { tlsConfig.MinVersion = tlsC.VersionTLS13 } if tlsConfig.MaxVersion != 0 && tlsConfig.MaxVersion < tlsC.VersionTLS13 { tlsConfig.MaxVersion = tlsC.VersionTLS13 } return nil } ================================================ FILE: core/Clash.Meta/component/ech/echparser/echparser.go ================================================ package echparser import ( "errors" "fmt" "golang.org/x/crypto/cryptobyte" ) // export from std's crypto/tls/ech.go const extensionEncryptedClientHello = 0xfe0d type ECHCipher struct { KDFID uint16 AEADID uint16 } type ECHExtension struct { Type uint16 Data []byte } type ECHConfig struct { raw []byte Version uint16 Length uint16 ConfigID uint8 KemID uint16 PublicKey []byte SymmetricCipherSuite []ECHCipher MaxNameLength uint8 PublicName []byte Extensions []ECHExtension } var ErrMalformedECHConfigList = errors.New("tls: malformed ECHConfigList") type EchConfigErr struct { field string } func (e *EchConfigErr) Error() string { if e.field == "" { return "tls: malformed ECHConfig" } return fmt.Sprintf("tls: malformed ECHConfig, invalid %s field", e.field) } func ParseECHConfig(enc []byte) (skip bool, ec ECHConfig, err error) { s := cryptobyte.String(enc) ec.raw = []byte(enc) if !s.ReadUint16(&ec.Version) { return false, ECHConfig{}, &EchConfigErr{"version"} } if !s.ReadUint16(&ec.Length) { return false, ECHConfig{}, &EchConfigErr{"length"} } if len(ec.raw) < int(ec.Length)+4 { return false, ECHConfig{}, &EchConfigErr{"length"} } ec.raw = ec.raw[:ec.Length+4] if ec.Version != extensionEncryptedClientHello { s.Skip(int(ec.Length)) return true, ECHConfig{}, nil } if !s.ReadUint8(&ec.ConfigID) { return false, ECHConfig{}, &EchConfigErr{"config_id"} } if !s.ReadUint16(&ec.KemID) { return false, ECHConfig{}, &EchConfigErr{"kem_id"} } if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&ec.PublicKey)) { return false, ECHConfig{}, &EchConfigErr{"public_key"} } var cipherSuites cryptobyte.String if !s.ReadUint16LengthPrefixed(&cipherSuites) { return false, ECHConfig{}, &EchConfigErr{"cipher_suites"} } for !cipherSuites.Empty() { var c ECHCipher if !cipherSuites.ReadUint16(&c.KDFID) { return false, ECHConfig{}, &EchConfigErr{"cipher_suites kdf_id"} } if !cipherSuites.ReadUint16(&c.AEADID) { return false, ECHConfig{}, &EchConfigErr{"cipher_suites aead_id"} } ec.SymmetricCipherSuite = append(ec.SymmetricCipherSuite, c) } if !s.ReadUint8(&ec.MaxNameLength) { return false, ECHConfig{}, &EchConfigErr{"maximum_name_length"} } var publicName cryptobyte.String if !s.ReadUint8LengthPrefixed(&publicName) { return false, ECHConfig{}, &EchConfigErr{"public_name"} } ec.PublicName = publicName var extensions cryptobyte.String if !s.ReadUint16LengthPrefixed(&extensions) { return false, ECHConfig{}, &EchConfigErr{"extensions"} } for !extensions.Empty() { var e ECHExtension if !extensions.ReadUint16(&e.Type) { return false, ECHConfig{}, &EchConfigErr{"extensions type"} } if !extensions.ReadUint16LengthPrefixed((*cryptobyte.String)(&e.Data)) { return false, ECHConfig{}, &EchConfigErr{"extensions data"} } ec.Extensions = append(ec.Extensions, e) } return false, ec, nil } // ParseECHConfigList parses a draft-ietf-tls-esni-18 ECHConfigList, returning a // slice of parsed ECHConfigs, in the same order they were parsed, or an error // if the list is malformed. func ParseECHConfigList(data []byte) ([]ECHConfig, error) { s := cryptobyte.String(data) var length uint16 if !s.ReadUint16(&length) { return nil, ErrMalformedECHConfigList } if length != uint16(len(data)-2) { return nil, ErrMalformedECHConfigList } var configs []ECHConfig for len(s) > 0 { if len(s) < 4 { return nil, errors.New("tls: malformed ECHConfig") } configLen := uint16(s[2])<<8 | uint16(s[3]) skip, ec, err := ParseECHConfig(s) if err != nil { return nil, err } s = s[configLen+4:] if !skip { configs = append(configs, ec) } } return configs, nil } ================================================ FILE: core/Clash.Meta/component/ech/key.go ================================================ package ech import ( "crypto/ecdh" "crypto/rand" "encoding/base64" "encoding/pem" "errors" "fmt" "os" "runtime" "sync" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/fswatch" "github.com/metacubex/tls" "golang.org/x/crypto/cryptobyte" ) const ( AEAD_AES_128_GCM = 0x0001 AEAD_AES_256_GCM = 0x0002 AEAD_ChaCha20Poly1305 = 0x0003 ) const extensionEncryptedClientHello = 0xfe0d const DHKEM_X25519_HKDF_SHA256 = 0x0020 const KDF_HKDF_SHA256 = 0x0001 // sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS. // We need this so that when we insert them into ECHConfigs the ordering // is stable. var sortedSupportedAEADs = []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte { 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) { for _, aeadID := range sortedSupportedAEADs { 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.BytesOrPanic() } func GenECHConfig(publicName string) (configBase64 string, keyPem string, err error) { echKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { return } echConfig := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0) builder := cryptobyte.NewBuilder(nil) builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes(echConfig) }) echConfigList := builder.BytesOrPanic() builder2 := cryptobyte.NewBuilder(nil) builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes(echKey.Bytes()) }) builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes(echConfig) }) echConfigKeys := builder2.BytesOrPanic() configBase64 = base64.StdEncoding.EncodeToString(echConfigList) keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: echConfigKeys})) return } 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, errors.New("error parsing private key") } if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) { return nil, errors.New("error parsing config") } keys = append(keys, key) } if len(keys) == 0 { return nil, errors.New("empty ECH keys") } return keys, nil } func LoadECHKey(key string, tlsConfig *tls.Config) error { if key == "" { return nil } echKeys, painTextErr := loadECHKey([]byte(key)) if painTextErr == nil { tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { return echKeys, nil } return nil } key = C.Path.Resolve(key) var loadErr error if !C.Path.IsSafePath(key) { loadErr = C.Path.ErrNotSafePath(key) } else { var echKey []byte echKey, loadErr = os.ReadFile(key) if loadErr == nil { echKeys, loadErr = loadECHKey(echKey) } } if loadErr != nil { return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) } gcFlag := new(os.File) // tiny (on the order of 16 bytes or less) and pointer-free objects may never run the finalizer, so we choose new an os.File updateMutex := sync.RWMutex{} if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{key}, Callback: func(path string) { updateMutex.Lock() defer updateMutex.Unlock() if echKey, err := os.ReadFile(key); err == nil { if newEchKeys, err := loadECHKey(echKey); err == nil { echKeys = newEchKeys } } }}); err == nil { if err = watcher.Start(); err == nil { runtime.SetFinalizer(gcFlag, func(f *os.File) { _ = watcher.Close() }) } } tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { defer runtime.KeepAlive(gcFlag) updateMutex.RLock() defer updateMutex.RUnlock() return echKeys, nil } return nil } func loadECHKey(echKey []byte) ([]tls.EncryptedClientHelloKey, error) { block, rest := pem.Decode(echKey) if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { return nil, errors.New("invalid ECH keys pem") } echKeys, err := UnmarshalECHKeys(block.Bytes) if err != nil { return nil, fmt.Errorf("parse ECH keys: %w", err) } return echKeys, err } ================================================ FILE: core/Clash.Meta/component/ech/key_test.go ================================================ package ech import ( "encoding/base64" "testing" "github.com/metacubex/mihomo/component/ech/echparser" ) func TestGenECHConfig(t *testing.T) { domain := "www.example.com" configBase64, _, err := GenECHConfig(domain) if err != nil { t.Error(err) } echConfigList, err := base64.StdEncoding.DecodeString(configBase64) if err != nil { t.Error(err) } echConfigs, err := echparser.ParseECHConfigList(echConfigList) if err != nil { t.Error(err) } if len(echConfigs) == 0 { t.Error("no ech config") } if publicName := string(echConfigs[0].PublicName); publicName != domain { t.Error("ech config domain error, expect ", domain, " got", publicName) } } ================================================ FILE: core/Clash.Meta/component/fakeip/cachefile.go ================================================ package fakeip import ( "net/netip" "github.com/metacubex/mihomo/component/profile/cachefile" ) type cachefileStore struct { cache *cachefile.FakeIpStore } // GetByHost implements store.GetByHost func (c *cachefileStore) GetByHost(host string) (netip.Addr, bool) { return c.cache.GetByHost(host) } // PutByHost implements store.PutByHost func (c *cachefileStore) PutByHost(host string, ip netip.Addr) { c.cache.PutByHost(host, ip) } // GetByIP implements store.GetByIP func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) { return c.cache.GetByIP(ip) } // PutByIP implements store.PutByIP func (c *cachefileStore) PutByIP(ip netip.Addr, host string) { c.cache.PutByIP(ip, host) } // DelByIP implements store.DelByIP func (c *cachefileStore) DelByIP(ip netip.Addr) { c.cache.DelByIP(ip) } // Exist implements store.Exist func (c *cachefileStore) Exist(ip netip.Addr) bool { _, exist := c.GetByIP(ip) return exist } // CloneTo implements store.CloneTo // already persistence func (c *cachefileStore) CloneTo(store store) {} // FlushFakeIP implements store.FlushFakeIP func (c *cachefileStore) FlushFakeIP() error { return c.cache.FlushFakeIP() } func newCachefileStore(cache *cachefile.CacheFile, prefix netip.Prefix) *cachefileStore { if prefix.Addr().Is6() { return &cachefileStore{cache.FakeIpStore6()} } else { return &cachefileStore{cache.FakeIpStore()} } } ================================================ FILE: core/Clash.Meta/component/fakeip/memory.go ================================================ package fakeip import ( "net/netip" "github.com/metacubex/mihomo/common/lru" ) type memoryStore struct { cacheIP *lru.LruCache[string, netip.Addr] cacheHost *lru.LruCache[netip.Addr, string] } // GetByHost implements store.GetByHost func (m *memoryStore) GetByHost(host string) (netip.Addr, bool) { if ip, exist := m.cacheIP.Get(host); exist { // ensure ip --> host on head of linked list m.cacheHost.Get(ip) return ip, true } return netip.Addr{}, false } // PutByHost implements store.PutByHost func (m *memoryStore) PutByHost(host string, ip netip.Addr) { m.cacheIP.Set(host, ip) } // GetByIP implements store.GetByIP func (m *memoryStore) GetByIP(ip netip.Addr) (string, bool) { if host, exist := m.cacheHost.Get(ip); exist { // ensure host --> ip on head of linked list m.cacheIP.Get(host) return host, true } return "", false } // PutByIP implements store.PutByIP func (m *memoryStore) PutByIP(ip netip.Addr, host string) { m.cacheHost.Set(ip, host) } // DelByIP implements store.DelByIP func (m *memoryStore) DelByIP(ip netip.Addr) { if host, exist := m.cacheHost.Get(ip); exist { m.cacheIP.Delete(host) } m.cacheHost.Delete(ip) } // Exist implements store.Exist func (m *memoryStore) Exist(ip netip.Addr) bool { return m.cacheHost.Exist(ip) } // CloneTo implements store.CloneTo // only for memoryStore to memoryStore func (m *memoryStore) CloneTo(store store) { if ms, ok := store.(*memoryStore); ok { m.cacheIP.CloneTo(ms.cacheIP) m.cacheHost.CloneTo(ms.cacheHost) } } // FlushFakeIP implements store.FlushFakeIP func (m *memoryStore) FlushFakeIP() error { m.cacheIP.Clear() m.cacheHost.Clear() return nil } func newMemoryStore(size int) *memoryStore { return &memoryStore{ cacheIP: lru.New[string, netip.Addr](lru.WithSize[string, netip.Addr](size)), cacheHost: lru.New[netip.Addr, string](lru.WithSize[netip.Addr, string](size)), } } ================================================ FILE: core/Clash.Meta/component/fakeip/pool.go ================================================ package fakeip import ( "errors" "net/netip" "strings" "sync" "github.com/metacubex/mihomo/component/profile/cachefile" "go4.org/netipx" ) const ( offsetKey = "key-offset-fake-ip" cycleKey = "key-cycle-fake-ip" ) type store interface { GetByHost(host string) (netip.Addr, bool) PutByHost(host string, ip netip.Addr) GetByIP(ip netip.Addr) (string, bool) PutByIP(ip netip.Addr, host string) DelByIP(ip netip.Addr) Exist(ip netip.Addr) bool CloneTo(store) FlushFakeIP() error } // Pool is an implementation about fake ip generator without storage type Pool struct { gateway netip.Addr first netip.Addr last netip.Addr offset netip.Addr cycle bool mux sync.Mutex ipnet netip.Prefix store store } // Lookup return a fake ip with host func (p *Pool) Lookup(host string) netip.Addr { p.mux.Lock() defer p.mux.Unlock() // RFC4343: DNS Case Insensitive, we SHOULD return result with all cases. host = strings.ToLower(host) if ip, exist := p.store.GetByHost(host); exist { return ip } ip := p.get(host) p.store.PutByHost(host, ip) return ip } // LookBack return host with the fake ip func (p *Pool) LookBack(ip netip.Addr) (string, bool) { p.mux.Lock() defer p.mux.Unlock() return p.store.GetByIP(ip) } // Exist returns if given ip exists in fake-ip pool func (p *Pool) Exist(ip netip.Addr) bool { p.mux.Lock() defer p.mux.Unlock() return p.store.Exist(ip) } // Gateway return gateway ip func (p *Pool) Gateway() netip.Addr { return p.gateway } // Broadcast return the last ip func (p *Pool) Broadcast() netip.Addr { return p.last } // IPNet return raw ipnet func (p *Pool) IPNet() netip.Prefix { return p.ipnet } // CloneFrom clone cache from old pool func (p *Pool) CloneFrom(o *Pool) { o.store.CloneTo(p.store) } func (p *Pool) get(host string) netip.Addr { p.offset = p.offset.Next() if !p.offset.Less(p.last) { p.cycle = true p.offset = p.first } if p.cycle || p.store.Exist(p.offset) { p.store.DelByIP(p.offset) } p.store.PutByIP(p.offset, host) return p.offset } func (p *Pool) FlushFakeIP() error { err := p.store.FlushFakeIP() if err == nil { p.cycle = false p.offset = p.first.Prev() } return err } func (p *Pool) StoreState() { if s, ok := p.store.(*cachefileStore); ok { s.PutByHost(offsetKey, p.offset) if p.cycle { s.PutByHost(cycleKey, p.offset) } } } func (p *Pool) restoreState() { if s, ok := p.store.(*cachefileStore); ok { if _, exist := s.GetByHost(cycleKey); exist { p.cycle = true } if offset, exist := s.GetByHost(offsetKey); exist { if p.ipnet.Contains(offset) { p.offset = offset } else { _ = p.FlushFakeIP() } } else if s.Exist(p.first) { _ = p.FlushFakeIP() } } } type Options struct { IPNet netip.Prefix // Size sets the maximum number of entries in memory // and does not work if Persistence is true Size int // Persistence will save the data to disk. // Size will not work and record will be fully stored. Persistence bool } // New return Pool instance func New(options Options) (*Pool, error) { var ( hostAddr = options.IPNet.Masked().Addr() gateway = hostAddr.Next() first = gateway.Next().Next().Next() // default start with 198.18.0.4 last = netipx.PrefixLastIP(options.IPNet) ) if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) { return nil, errors.New("ipnet don't have valid ip") } pool := &Pool{ gateway: gateway, first: first, last: last, offset: first.Prev(), cycle: false, ipnet: options.IPNet, } if options.Persistence { pool.store = newCachefileStore(cachefile.Cache(), options.IPNet) } else { pool.store = newMemoryStore(options.Size) } pool.restoreState() return pool, nil } ================================================ FILE: core/Clash.Meta/component/fakeip/pool_test.go ================================================ package fakeip import ( "fmt" "net/netip" "os" "testing" "time" "github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/bbolt" "github.com/stretchr/testify/assert" ) func createPools(options Options) ([]*Pool, string, error) { pool, err := New(options) if err != nil { return nil, "", err } filePool, tempfile, err := createCachefileStore(options) if err != nil { return nil, "", err } return []*Pool{pool, filePool}, tempfile, nil } func createCachefileStore(options Options) (*Pool, string, error) { pool, err := New(options) if err != nil { return nil, "", err } f, err := os.CreateTemp("", "mihomo") if err != nil { return nil, "", err } db, err := bbolt.Open(f.Name(), 0o666, &bbolt.Options{Timeout: time.Second}) if err != nil { return nil, "", err } pool.store = newCachefileStore(&cachefile.CacheFile{DB: db}, options.IPNet) return pool, f.Name(), nil } func TestPool_Basic(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.0/28") pools, tempfile, err := createPools(Options{ IPNet: ipnet, Size: 10, }) assert.Nil(t, err) defer os.Remove(tempfile) for _, pool := range pools { first := pool.Lookup("foo.com") last := pool.Lookup("bar.com") bar, exist := pool.LookBack(last) assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.Equal(t, pool.Lookup("foo.com"), netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5})) assert.True(t, exist) assert.Equal(t, bar, "bar.com") assert.Equal(t, pool.Gateway(), netip.AddrFrom4([4]byte{192, 168, 0, 1})) assert.Equal(t, pool.Broadcast(), netip.AddrFrom4([4]byte{192, 168, 0, 15})) assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5}))) assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 6}))) assert.False(t, pool.Exist(netip.MustParseAddr("::1"))) } } func TestPool_BasicV6(t *testing.T) { ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118") pools, tempfile, err := createPools(Options{ IPNet: ipnet, Size: 10, }) assert.Nil(t, err) defer os.Remove(tempfile) for _, pool := range pools { first := pool.Lookup("foo.com") last := pool.Lookup("bar.com") bar, exist := pool.LookBack(last) assert.Equal(t, first, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) assert.Equal(t, pool.Lookup("foo.com"), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) assert.Equal(t, last, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")) assert.True(t, exist) assert.Equal(t, bar, "bar.com") assert.Equal(t, pool.Gateway(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801")) assert.Equal(t, pool.Broadcast(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff")) assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))) assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8806"))) assert.False(t, pool.Exist(netip.MustParseAddr("127.0.0.1"))) } } func TestPool_Case_Insensitive(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/29") pools, tempfile, err := createPools(Options{ IPNet: ipnet, Size: 10, }) assert.Nil(t, err) defer os.Remove(tempfile) for _, pool := range pools { first := pool.Lookup("foo.com") last := pool.Lookup("Foo.Com") foo, exist := pool.LookBack(last) assert.Equal(t, first, pool.Lookup("Foo.Com")) assert.Equal(t, pool.Lookup("fOo.cOM"), first) assert.True(t, exist) assert.Equal(t, foo, "foo.com") } } func TestPool_CycleUsed(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.16/28") pools, tempfile, err := createPools(Options{ IPNet: ipnet, Size: 10, }) assert.Nil(t, err) defer os.Remove(tempfile) for _, pool := range pools { foo := pool.Lookup("foo.com") bar := pool.Lookup("bar.com") for i := 0; i < 9; i++ { pool.Lookup(fmt.Sprintf("%d.com", i)) } baz := pool.Lookup("baz.com") next := pool.Lookup("foo.com") assert.Equal(t, foo, baz) assert.Equal(t, next, bar) } } func TestPool_MaxCacheSize(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/24") pool, _ := New(Options{ IPNet: ipnet, Size: 2, }) first := pool.Lookup("foo.com") pool.Lookup("bar.com") pool.Lookup("baz.com") next := pool.Lookup("foo.com") assert.NotEqual(t, first, next) } func TestPool_DoubleMapping(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/24") pool, _ := New(Options{ IPNet: ipnet, Size: 2, }) // fill cache fooIP := pool.Lookup("foo.com") bazIP := pool.Lookup("baz.com") // make foo.com hot pool.Lookup("foo.com") // should drop baz.com barIP := pool.Lookup("bar.com") _, fooExist := pool.LookBack(fooIP) _, bazExist := pool.LookBack(bazIP) _, barExist := pool.LookBack(barIP) newBazIP := pool.Lookup("baz.com") assert.True(t, fooExist) assert.False(t, bazExist) assert.True(t, barExist) assert.NotEqual(t, bazIP, newBazIP) } func TestPool_Clone(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/24") pool, _ := New(Options{ IPNet: ipnet, Size: 2, }) first := pool.Lookup("foo.com") last := pool.Lookup("bar.com") assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5})) newPool, _ := New(Options{ IPNet: ipnet, Size: 2, }) newPool.CloneFrom(pool) _, firstExist := newPool.LookBack(first) _, lastExist := newPool.LookBack(last) assert.True(t, firstExist) assert.True(t, lastExist) } func TestPool_Error(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/31") _, err := New(Options{ IPNet: ipnet, Size: 10, }) assert.Error(t, err) } func TestPool_FlushFileCache(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/28") pools, tempfile, err := createPools(Options{ IPNet: ipnet, Size: 10, }) assert.Nil(t, err) defer os.Remove(tempfile) for _, pool := range pools { foo := pool.Lookup("foo.com") bar := pool.Lookup("baz.com") bax := pool.Lookup("baz.com") fox := pool.Lookup("foo.com") err = pool.FlushFakeIP() assert.Nil(t, err) next := pool.Lookup("baz.com") baz := pool.Lookup("foo.com") nero := pool.Lookup("foo.com") assert.Equal(t, foo, fox) assert.Equal(t, foo, next) assert.NotEqual(t, foo, baz) assert.Equal(t, bar, bax) assert.Equal(t, bar, baz) assert.NotEqual(t, bar, next) assert.Equal(t, baz, nero) } } func TestPool_FlushMemoryCache(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/28") pool, _ := New(Options{ IPNet: ipnet, Size: 10, }) foo := pool.Lookup("foo.com") bar := pool.Lookup("baz.com") bax := pool.Lookup("baz.com") fox := pool.Lookup("foo.com") err := pool.FlushFakeIP() assert.Nil(t, err) next := pool.Lookup("baz.com") baz := pool.Lookup("foo.com") nero := pool.Lookup("foo.com") assert.Equal(t, foo, fox) assert.Equal(t, foo, next) assert.NotEqual(t, foo, baz) assert.Equal(t, bar, bax) assert.Equal(t, bar, baz) assert.NotEqual(t, bar, next) assert.Equal(t, baz, nero) } ================================================ FILE: core/Clash.Meta/component/fakeip/skipper.go ================================================ package fakeip import ( C "github.com/metacubex/mihomo/constant" ) const ( UseFakeIP = "fake-ip" UseRealIP = "real-ip" ) type Skipper struct { Rules []C.Rule Host []C.DomainMatcher Mode C.FilterMode } // ShouldSkipped return if domain should be skipped func (p *Skipper) ShouldSkipped(domain string) bool { if len(p.Rules) > 0 { metadata := &C.Metadata{Host: domain} for _, rule := range p.Rules { if matched, action := rule.Match(metadata, C.RuleMatchHelper{}); matched { return action == UseRealIP } } return false } should := p.shouldSkipped(domain) if p.Mode == C.FilterWhiteList { return !should } return should } func (p *Skipper) shouldSkipped(domain string) bool { for _, matcher := range p.Host { if matcher.MatchDomain(domain) { return true } } return false } ================================================ FILE: core/Clash.Meta/component/fakeip/skipper_test.go ================================================ package fakeip import ( "testing" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/assert" ) func TestSkipper_BlackList(t *testing.T) { tree := trie.New[struct{}]() assert.NoError(t, tree.Insert("example.com", struct{}{})) assert.False(t, tree.IsEmpty()) skipper := &Skipper{ Host: []C.DomainMatcher{tree.NewDomainSet()}, } assert.True(t, skipper.ShouldSkipped("example.com")) assert.False(t, skipper.ShouldSkipped("foo.com")) assert.False(t, skipper.shouldSkipped("baz.com")) } func TestSkipper_WhiteList(t *testing.T) { tree := trie.New[struct{}]() assert.NoError(t, tree.Insert("example.com", struct{}{})) assert.False(t, tree.IsEmpty()) skipper := &Skipper{ Host: []C.DomainMatcher{tree.NewDomainSet()}, Mode: C.FilterWhiteList, } assert.False(t, skipper.ShouldSkipped("example.com")) assert.True(t, skipper.ShouldSkipped("foo.com")) assert.True(t, skipper.ShouldSkipped("baz.com")) } ================================================ FILE: core/Clash.Meta/component/generator/cmd.go ================================================ package generator import ( "encoding/base64" "fmt" "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/transport/sudoku" "github.com/metacubex/mihomo/transport/vless/encryption" "github.com/gofrs/uuid/v5" ) func Main(args []string) { if len(args) < 1 { panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair/vless-mlkem768/vless-x25519/sudoku-keypair") } switch args[0] { case "uuid": newUUID, err := uuid.NewV4() if err != nil { panic(err) } fmt.Println(newUUID.String()) case "reality-keypair": privateKey, err := GenX25519PrivateKey() if err != nil { panic(err) } fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey.Bytes())) fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(privateKey.PublicKey().Bytes())) case "wg-keypair": privateKey, err := GenX25519PrivateKey() if err != nil { panic(err) } fmt.Println("PrivateKey: " + base64.StdEncoding.EncodeToString(privateKey.Bytes())) fmt.Println("PublicKey: " + base64.StdEncoding.EncodeToString(privateKey.PublicKey().Bytes())) case "ech-keypair": if len(args) < 2 { panic("Using: generate ech-keypair ") } configBase64, keyPem, err := ech.GenECHConfig(args[1]) if err != nil { panic(err) } fmt.Println("Config:", configBase64) fmt.Println("Key:", keyPem) case "vless-mlkem768": var seed string if len(args) > 1 { seed = args[1] } seedBase64, clientBase64, hash32Base64, err := encryption.GenMLKEM768(seed) if err != nil { panic(err) } fmt.Println("Seed: " + seedBase64) fmt.Println("Client: " + clientBase64) fmt.Println("Hash32: " + hash32Base64) fmt.Println("-----------------------") fmt.Println(" Lazy-Config ") fmt.Println("-----------------------") fmt.Printf("[Server] decryption: \"mlkem768x25519plus.native.600s.%s\"\n", seedBase64) fmt.Printf("[Client] encryption: \"mlkem768x25519plus.native.0rtt.%s\"\n", clientBase64) case "vless-x25519": var privateKey string if len(args) > 1 { privateKey = args[1] } privateKeyBase64, passwordBase64, hash32Base64, err := encryption.GenX25519(privateKey) if err != nil { panic(err) } fmt.Println("PrivateKey: " + privateKeyBase64) fmt.Println("Password: " + passwordBase64) fmt.Println("Hash32: " + hash32Base64) fmt.Println("-----------------------") fmt.Println(" Lazy-Config ") fmt.Println("-----------------------") fmt.Printf("[Server] decryption: \"mlkem768x25519plus.native.600s.%s\"\n", privateKeyBase64) fmt.Printf("[Client] encryption: \"mlkem768x25519plus.native.0rtt.%s\"\n", passwordBase64) case "sudoku-keypair": privateKey, publicKey, err := sudoku.GenKeyPair() if err != nil { panic(err) } // Output: Available Private Key for client, Master Public Key for server fmt.Println("PrivateKey: " + privateKey) fmt.Println("PublicKey: " + publicKey) } } ================================================ FILE: core/Clash.Meta/component/generator/x25519.go ================================================ package generator import ( "crypto/ecdh" "crypto/rand" ) const X25519KeySize = 32 func GenX25519PrivateKey() (*ecdh.PrivateKey, error) { var privateKey [X25519KeySize]byte _, err := rand.Read(privateKey[:]) if err != nil { return nil, err } // Avoid generating equivalent X25519 private keys // https://github.com/XTLS/Xray-core/pull/1747 // // Modify random bytes using algorithm described at: // https://cr.yp.to/ecdh.html. privateKey[0] &= 248 privateKey[31] &= 127 privateKey[31] |= 64 return ecdh.X25519().NewPrivateKey(privateKey[:]) } ================================================ FILE: core/Clash.Meta/component/geodata/attr.go ================================================ package geodata import ( "strings" "github.com/metacubex/mihomo/component/geodata/router" ) type AttributeList struct { matcher []BooleanMatcher } func (al *AttributeList) Match(domain *router.Domain) bool { for _, matcher := range al.matcher { if !matcher.Match(domain) { return false } } return true } func (al *AttributeList) IsEmpty() bool { return len(al.matcher) == 0 } func (al *AttributeList) String() string { matcher := make([]string, len(al.matcher)) for i, match := range al.matcher { matcher[i] = string(match) } return strings.Join(matcher, ",") } func parseAttrs(attrs []string) *AttributeList { al := new(AttributeList) for _, attr := range attrs { trimmedAttr := strings.ToLower(strings.TrimSpace(attr)) if len(trimmedAttr) == 0 { continue } al.matcher = append(al.matcher, BooleanMatcher(trimmedAttr)) } return al } type AttributeMatcher interface { Match(*router.Domain) bool } type BooleanMatcher string func (m BooleanMatcher) Match(domain *router.Domain) bool { for _, attr := range domain.Attribute { if strings.EqualFold(attr.GetKey(), string(m)) { return true } } return false } ================================================ FILE: core/Clash.Meta/component/geodata/geodata.go ================================================ package geodata import ( "fmt" "github.com/metacubex/mihomo/component/geodata/router" C "github.com/metacubex/mihomo/constant" ) type loader struct { LoaderImplementation } func (l *loader) LoadGeoSite(list string) ([]*router.Domain, error) { return l.LoadSiteByPath(C.GeositeName, list) } func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) { return l.LoadIPByPath(C.GeoipName, country) } var loaders map[string]func() LoaderImplementation func RegisterGeoDataLoaderImplementationCreator(name string, loader func() LoaderImplementation) { if loaders == nil { loaders = map[string]func() LoaderImplementation{} } loaders[name] = loader } func getGeoDataLoaderImplementation(name string) (LoaderImplementation, error) { if geoLoader, ok := loaders[name]; ok { return geoLoader(), nil } return nil, fmt.Errorf("unable to locate GeoData loader %s", name) } func GetGeoDataLoader(name string) (Loader, error) { loadImpl, err := getGeoDataLoaderImplementation(name) if err == nil { return &loader{loadImpl}, nil } return nil, err } ================================================ FILE: core/Clash.Meta/component/geodata/geodataproto.go ================================================ package geodata import ( "github.com/metacubex/mihomo/component/geodata/router" ) type LoaderImplementation interface { LoadSiteByPath(filename, list string) ([]*router.Domain, error) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) LoadIPByPath(filename, country string) ([]*router.CIDR, error) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) } type Loader interface { LoaderImplementation LoadGeoSite(list string) ([]*router.Domain, error) LoadGeoIP(country string) ([]*router.CIDR, error) } ================================================ FILE: core/Clash.Meta/component/geodata/init.go ================================================ package geodata import ( "context" "fmt" "io" "os" "sync" "time" "github.com/metacubex/mihomo/common/atomic" mihomoHttp "github.com/metacubex/mihomo/component/http" "github.com/metacubex/mihomo/component/mmdb" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/http" ) var ( initGeoSite bool initGeoIP int initASN bool initGeoSiteMutex sync.Mutex initGeoIPMutex sync.Mutex initASNMutex sync.Mutex geoIpEnable atomic.Bool geoSiteEnable atomic.Bool asnEnable atomic.Bool geoIpUrl string mmdbUrl string geoSiteUrl string asnUrl string ) func GeoIpUrl() string { return geoIpUrl } func SetGeoIpUrl(url string) { geoIpUrl = url } func MmdbUrl() string { return mmdbUrl } func SetMmdbUrl(url string) { mmdbUrl = url } func GeoSiteUrl() string { return geoSiteUrl } func SetGeoSiteUrl(url string) { geoSiteUrl = url } func ASNUrl() string { return asnUrl } func SetASNUrl(url string) { asnUrl = url } func downloadToPath(url string, path string) (err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) defer cancel() resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil) if err != nil { return } defer resp.Body.Close() f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return err } defer f.Close() _, err = io.Copy(f, resp.Body) return err } func InitGeoSite() error { geoSiteEnable.Store(true) initGeoSiteMutex.Lock() defer initGeoSiteMutex.Unlock() if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { log.Infoln("Can't find GeoSite.dat, start download") if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil { return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) } log.Infoln("Download GeoSite.dat finish") initGeoSite = false } if !initGeoSite { if err := Verify(C.GeositeName); err != nil { log.Warnln("GeoSite.dat invalid, remove and download: %s", err) if err := os.Remove(C.Path.GeoSite()); err != nil { return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error()) } if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil { return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) } } initGeoSite = true } return nil } func InitGeoIP() error { geoIpEnable.Store(true) initGeoIPMutex.Lock() defer initGeoIPMutex.Unlock() if GeodataMode() { if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) { log.Infoln("Can't find GeoIP.dat, start download") if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil { return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) } log.Infoln("Download GeoIP.dat finish") initGeoIP = 0 } if initGeoIP != 1 { if err := Verify(C.GeoipName); err != nil { log.Warnln("GeoIP.dat invalid, remove and download: %s", err) if err := os.Remove(C.Path.GeoIP()); err != nil { return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error()) } if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil { return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) } } initGeoIP = 1 } return nil } if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) { log.Infoln("Can't find MMDB, start download") if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil { return fmt.Errorf("can't download MMDB: %s", err.Error()) } } if initGeoIP != 2 { if !mmdb.Verify(C.Path.MMDB()) { log.Warnln("MMDB invalid, remove and download") if err := os.Remove(C.Path.MMDB()); err != nil { return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) } if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil { return fmt.Errorf("can't download MMDB: %s", err.Error()) } } initGeoIP = 2 } return nil } func InitASN() error { asnEnable.Store(true) initASNMutex.Lock() defer initASNMutex.Unlock() if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) { log.Infoln("Can't find ASN.mmdb, start download") if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil { return fmt.Errorf("can't download ASN.mmdb: %s", err.Error()) } log.Infoln("Download ASN.mmdb finish") initASN = false } if !initASN { if !mmdb.Verify(C.Path.ASN()) { log.Warnln("ASN invalid, remove and download") if err := os.Remove(C.Path.ASN()); err != nil { return fmt.Errorf("can't remove invalid ASN: %s", err.Error()) } if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil { return fmt.Errorf("can't download ASN: %s", err.Error()) } } initASN = true } return nil } func GeoIpEnable() bool { return geoIpEnable.Load() } func GeoSiteEnable() bool { return geoSiteEnable.Load() } func ASNEnable() bool { return asnEnable.Load() } ================================================ FILE: core/Clash.Meta/component/geodata/memconservative/cache.go ================================================ package memconservative import ( "fmt" "os" "strings" "github.com/metacubex/mihomo/component/geodata/router" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "google.golang.org/protobuf/proto" ) type GeoIPCache map[string]*router.GeoIP func (g GeoIPCache) Has(key string) bool { return !(g.Get(key) == nil) } func (g GeoIPCache) Get(key string) *router.GeoIP { if g == nil { return nil } return g[key] } func (g GeoIPCache) Set(key string, value *router.GeoIP) { if g == nil { g = make(map[string]*router.GeoIP) } g[key] = value } func (g GeoIPCache) Unmarshal(filename, code string) (*router.GeoIP, error) { asset := C.Path.GetAssetLocation(filename) idx := strings.ToLower(asset + ":" + code) if g.Has(idx) { return g.Get(idx), nil } geoipBytes, err := Decode(asset, code) switch err { case nil: var geoip router.GeoIP if err := proto.Unmarshal(geoipBytes, &geoip); err != nil { return nil, err } g.Set(idx, &geoip) return &geoip, nil case errCodeNotFound: return nil, fmt.Errorf("country code %s%s%s", code, " not found in ", filename) case errFailedToReadBytes, errFailedToReadExpectedLenBytes, errInvalidGeodataFile, errInvalidGeodataVarintLength: log.Warnln("failed to decode geoip file: %s%s", filename, ", fallback to the original ReadFile method") geoipBytes, err = os.ReadFile(asset) if err != nil { return nil, err } var geoipList router.GeoIPList if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { return nil, err } for _, geoip := range geoipList.GetEntry() { if strings.EqualFold(code, geoip.GetCountryCode()) { g.Set(idx, geoip) return geoip, nil } } default: return nil, err } return nil, fmt.Errorf("country code %s%s%s", code, " not found in ", filename) } type GeoSiteCache map[string]*router.GeoSite func (g GeoSiteCache) Has(key string) bool { return !(g.Get(key) == nil) } func (g GeoSiteCache) Get(key string) *router.GeoSite { if g == nil { return nil } return g[key] } func (g GeoSiteCache) Set(key string, value *router.GeoSite) { if g == nil { g = make(map[string]*router.GeoSite) } g[key] = value } func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) { asset := C.Path.GetAssetLocation(filename) idx := strings.ToLower(asset + ":" + code) if g.Has(idx) { return g.Get(idx), nil } geositeBytes, err := Decode(asset, code) switch err { case nil: var geosite router.GeoSite if err := proto.Unmarshal(geositeBytes, &geosite); err != nil { return nil, err } g.Set(idx, &geosite) return &geosite, nil case errCodeNotFound: return nil, fmt.Errorf("list %s%s%s", code, " not found in ", filename) case errFailedToReadBytes, errFailedToReadExpectedLenBytes, errInvalidGeodataFile, errInvalidGeodataVarintLength: log.Warnln("failed to decode geosite file: %s%s", filename, ", fallback to the original ReadFile method") geositeBytes, err = os.ReadFile(asset) if err != nil { return nil, err } var geositeList router.GeoSiteList if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { return nil, err } for _, geosite := range geositeList.GetEntry() { if strings.EqualFold(code, geosite.GetCountryCode()) { g.Set(idx, geosite) return geosite, nil } } default: return nil, err } return nil, fmt.Errorf("list %s%s%s", code, " not found in ", filename) } ================================================ FILE: core/Clash.Meta/component/geodata/memconservative/decode.go ================================================ package memconservative import ( "errors" "fmt" "io" "os" "strings" "google.golang.org/protobuf/encoding/protowire" ) var ( errFailedToReadBytes = errors.New("failed to read bytes") errFailedToReadExpectedLenBytes = errors.New("failed to read expected length of bytes") errInvalidGeodataFile = errors.New("invalid geodata file") errInvalidGeodataVarintLength = errors.New("invalid geodata varint length") errCodeNotFound = errors.New("code not found") ) func emitBytes(f io.ReadSeeker, code string) ([]byte, error) { count := 1 isInner := false tempContainer := make([]byte, 0, 5) var result []byte var advancedN uint64 = 1 var geoDataVarintLength, codeVarintLength, varintLenByteLen uint64 = 0, 0, 0 Loop: for { container := make([]byte, advancedN) bytesRead, err := f.Read(container) if err == io.EOF { return nil, errCodeNotFound } if err != nil { return nil, errFailedToReadBytes } if bytesRead != len(container) { return nil, errFailedToReadExpectedLenBytes } switch count { case 1, 3: // data type ((field_number << 3) | wire_type) if container[0] != 10 { // byte `0A` equals to `10` in decimal return nil, errInvalidGeodataFile } advancedN = 1 count++ case 2, 4: // data length tempContainer = append(tempContainer, container...) if container[0] > 127 { // max one-byte-length byte `7F`(0FFF FFFF) equals to `127` in decimal advancedN = 1 goto Loop } lenVarint, n := protowire.ConsumeVarint(tempContainer) if n < 0 { return nil, errInvalidGeodataVarintLength } tempContainer = nil if !isInner { isInner = true geoDataVarintLength = lenVarint advancedN = 1 } else { isInner = false codeVarintLength = lenVarint varintLenByteLen = uint64(n) advancedN = codeVarintLength } count++ case 5: // data value if strings.EqualFold(string(container), code) { count++ offset := -(1 + int64(varintLenByteLen) + int64(codeVarintLength)) _, _ = f.Seek(offset, 1) // back to the start of GeoIP or GeoSite varint advancedN = geoDataVarintLength // the number of bytes to be read in next round } else { count = 1 offset := int64(geoDataVarintLength) - int64(codeVarintLength) - int64(varintLenByteLen) - 1 _, _ = f.Seek(offset, 1) // skip the unmatched GeoIP or GeoSite varint advancedN = 1 // the next round will be the start of another GeoIPList or GeoSiteList } case 6: // matched GeoIP or GeoSite varint result = container break Loop } } return result, nil } func Decode(filename, code string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error()) } defer func(f *os.File) { _ = f.Close() }(f) geoBytes, err := emitBytes(f, code) if err != nil { return nil, err } return geoBytes, nil } ================================================ FILE: core/Clash.Meta/component/geodata/memconservative/memc.go ================================================ package memconservative import ( "errors" "fmt" "runtime" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/geodata/router" ) type memConservativeLoader struct { geoipcache GeoIPCache geositecache GeoSiteCache } func (m *memConservativeLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) { defer runtime.GC() geoip, err := m.geoipcache.Unmarshal(filename, country) if err != nil { return nil, fmt.Errorf("failed to decode geodata file: %s, base error: %s", filename, err.Error()) } return geoip.Cidr, nil } func (m *memConservativeLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) { return nil, errors.New("memConservative do not support LoadIPByBytes") } func (m *memConservativeLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) { defer runtime.GC() geosite, err := m.geositecache.Unmarshal(filename, list) if err != nil { return nil, fmt.Errorf("failed to decode geodata file: %s, base error: %s", filename, err.Error()) } return geosite.Domain, nil } func (m *memConservativeLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) { return nil, errors.New("memConservative do not support LoadSiteByBytes") } func newMemConservativeLoader() geodata.LoaderImplementation { return &memConservativeLoader{make(map[string]*router.GeoIP), make(map[string]*router.GeoSite)} } func init() { geodata.RegisterGeoDataLoaderImplementationCreator("memconservative", newMemConservativeLoader) } ================================================ FILE: core/Clash.Meta/component/geodata/package_info.go ================================================ // Modified from: https://github.com/v2fly/v2ray-core/tree/master/infra/conf/geodata // License: MIT package geodata ================================================ FILE: core/Clash.Meta/component/geodata/router/condition.go ================================================ package router import ( "fmt" "net/netip" "strings" "github.com/metacubex/mihomo/component/cidr" "github.com/metacubex/mihomo/component/geodata/strmatcher" "github.com/metacubex/mihomo/component/trie" ) var matcherTypeMap = map[Domain_Type]strmatcher.Type{ Domain_Plain: strmatcher.Substr, Domain_Regex: strmatcher.Regex, Domain_Domain: strmatcher.Domain, Domain_Full: strmatcher.Full, } func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) { matcherType, f := matcherTypeMap[domain.Type] if !f { return nil, fmt.Errorf("unsupported domain type %v", domain.Type) } matcher, err := matcherType.New(domain.Value) if err != nil { return nil, fmt.Errorf("failed to create domain matcher, base error: %s", err.Error()) } return matcher, nil } type DomainMatcher interface { ApplyDomain(string) bool Count() int } type succinctDomainMatcher struct { set *trie.DomainSet otherMatchers []strmatcher.Matcher count int } func (m *succinctDomainMatcher) ApplyDomain(domain string) bool { isMatched := m.set.Has(domain) if !isMatched { for _, matcher := range m.otherMatchers { isMatched = matcher.Match(domain) if isMatched { break } } } return isMatched } func (m *succinctDomainMatcher) Count() int { return m.count } func NewSuccinctMatcherGroup(domains []*Domain) (DomainMatcher, error) { t := trie.New[struct{}]() m := &succinctDomainMatcher{ count: len(domains), } for _, d := range domains { switch d.Type { case Domain_Plain, Domain_Regex: matcher, err := matcherTypeMap[d.Type].New(d.Value) if err != nil { return nil, err } m.otherMatchers = append(m.otherMatchers, matcher) case Domain_Domain: err := t.Insert("+."+d.Value, struct{}{}) if err != nil { return nil, err } case Domain_Full: err := t.Insert(d.Value, struct{}{}) if err != nil { return nil, err } } } m.set = t.NewDomainSet() return m, nil } type v2rayDomainMatcher struct { matchers strmatcher.IndexMatcher count int } func NewMphMatcherGroup(domains []*Domain) (DomainMatcher, error) { g := strmatcher.NewMphMatcherGroup() for _, d := range domains { matcherType, f := matcherTypeMap[d.Type] if !f { return nil, fmt.Errorf("unsupported domain type %v", d.Type) } _, err := g.AddPattern(d.Value, matcherType) if err != nil { return nil, err } } g.Build() return &v2rayDomainMatcher{ matchers: g, count: len(domains), }, nil } func (m *v2rayDomainMatcher) ApplyDomain(domain string) bool { return len(m.matchers.Match(strings.ToLower(domain))) > 0 } func (m *v2rayDomainMatcher) Count() int { return m.count } type notDomainMatcher struct { DomainMatcher } func (m notDomainMatcher) ApplyDomain(domain string) bool { return !m.DomainMatcher.ApplyDomain(domain) } func NewNotDomainMatcherGroup(matcher DomainMatcher) DomainMatcher { return notDomainMatcher{matcher} } type IPMatcher interface { Match(ip netip.Addr) bool Count() int } type geoIPMatcher struct { cidrSet *cidr.IpCidrSet count int } // Match returns true if the given ip is included by the GeoIP. func (m *geoIPMatcher) Match(ip netip.Addr) bool { return m.cidrSet.IsContain(ip) } func (m *geoIPMatcher) Count() int { return m.count } func NewGeoIPMatcher(cidrList []*CIDR) (IPMatcher, error) { m := &geoIPMatcher{ cidrSet: cidr.NewIpCidrSet(), count: len(cidrList), } for _, cidr := range cidrList { addr, ok := netip.AddrFromSlice(cidr.Ip) if !ok { return nil, fmt.Errorf("error when loading GeoIP: invalid IP: %s", cidr.Ip) } err := m.cidrSet.AddIpCidr(netip.PrefixFrom(addr, int(cidr.Prefix))) if err != nil { return nil, fmt.Errorf("error when loading GeoIP: %w", err) } } err := m.cidrSet.Merge() if err != nil { return nil, err } return m, nil } type notIPMatcher struct { IPMatcher } func (m notIPMatcher) Match(ip netip.Addr) bool { return !m.IPMatcher.Match(ip) } func NewNotIpMatcherGroup(matcher IPMatcher) IPMatcher { return notIPMatcher{matcher} } ================================================ FILE: core/Clash.Meta/component/geodata/router/config.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.0 // protoc v3.19.1 // source: component/geodata/router/config.proto package router import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) 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 of domain value. type Domain_Type int32 const ( // The value is used as is. Domain_Plain Domain_Type = 0 // The value is used as a regular expression. Domain_Regex Domain_Type = 1 // The value is a root domain. Domain_Domain Domain_Type = 2 // The value is a domain. Domain_Full Domain_Type = 3 ) // Enum value maps for Domain_Type. var ( Domain_Type_name = map[int32]string{ 0: "Plain", 1: "Regex", 2: "Domain", 3: "Full", } Domain_Type_value = map[string]int32{ "Plain": 0, "Regex": 1, "Domain": 2, "Full": 3, } ) func (x Domain_Type) Enum() *Domain_Type { p := new(Domain_Type) *p = x return p } func (x Domain_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Domain_Type) Descriptor() protoreflect.EnumDescriptor { return file_component_geodata_router_config_proto_enumTypes[0].Descriptor() } func (Domain_Type) Type() protoreflect.EnumType { return &file_component_geodata_router_config_proto_enumTypes[0] } func (x Domain_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Domain_Type.Descriptor instead. func (Domain_Type) EnumDescriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{0, 0} } // Domain for routing decision. type Domain struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Domain matching type. Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=mihomo.component.geodata.router.Domain_Type" json:"type,omitempty"` // Domain value. Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // Attributes of this domain. May be used for filtering. Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"` } func (x *Domain) Reset() { *x = Domain{} if protoimpl.UnsafeEnabled { mi := &file_component_geodata_router_config_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Domain) String() string { return protoimpl.X.MessageStringOf(x) } func (*Domain) ProtoMessage() {} func (x *Domain) ProtoReflect() protoreflect.Message { mi := &file_component_geodata_router_config_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Domain.ProtoReflect.Descriptor instead. func (*Domain) Descriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{0} } func (x *Domain) GetType() Domain_Type { if x != nil { return x.Type } return Domain_Plain } func (x *Domain) GetValue() string { if x != nil { return x.Value } return "" } func (x *Domain) GetAttribute() []*Domain_Attribute { if x != nil { return x.Attribute } return nil } // IP for routing decision, in CIDR form. type CIDR struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // IP address, should be either 4 or 16 bytes. Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` // Number of leading ones in the network mask. Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"` } func (x *CIDR) Reset() { *x = CIDR{} if protoimpl.UnsafeEnabled { mi := &file_component_geodata_router_config_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CIDR) String() string { return protoimpl.X.MessageStringOf(x) } func (*CIDR) ProtoMessage() {} func (x *CIDR) ProtoReflect() protoreflect.Message { mi := &file_component_geodata_router_config_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CIDR.ProtoReflect.Descriptor instead. func (*CIDR) Descriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{1} } func (x *CIDR) GetIp() []byte { if x != nil { return x.Ip } return nil } func (x *CIDR) GetPrefix() uint32 { if x != nil { return x.Prefix } return 0 } type GeoIP struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"` ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"` } func (x *GeoIP) Reset() { *x = GeoIP{} if protoimpl.UnsafeEnabled { mi := &file_component_geodata_router_config_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoIP) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoIP) ProtoMessage() {} func (x *GeoIP) ProtoReflect() protoreflect.Message { mi := &file_component_geodata_router_config_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoIP.ProtoReflect.Descriptor instead. func (*GeoIP) Descriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{2} } func (x *GeoIP) GetCountryCode() string { if x != nil { return x.CountryCode } return "" } func (x *GeoIP) GetCidr() []*CIDR { if x != nil { return x.Cidr } return nil } func (x *GeoIP) GetReverseMatch() bool { if x != nil { return x.ReverseMatch } return false } type GeoIPList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` } func (x *GeoIPList) Reset() { *x = GeoIPList{} if protoimpl.UnsafeEnabled { mi := &file_component_geodata_router_config_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoIPList) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoIPList) ProtoMessage() {} func (x *GeoIPList) ProtoReflect() protoreflect.Message { mi := &file_component_geodata_router_config_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead. func (*GeoIPList) Descriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{3} } func (x *GeoIPList) GetEntry() []*GeoIP { if x != nil { return x.Entry } return nil } type GeoSite struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` } func (x *GeoSite) Reset() { *x = GeoSite{} if protoimpl.UnsafeEnabled { mi := &file_component_geodata_router_config_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoSite) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoSite) ProtoMessage() {} func (x *GeoSite) ProtoReflect() protoreflect.Message { mi := &file_component_geodata_router_config_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoSite.ProtoReflect.Descriptor instead. func (*GeoSite) Descriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{4} } func (x *GeoSite) GetCountryCode() string { if x != nil { return x.CountryCode } return "" } func (x *GeoSite) GetDomain() []*Domain { if x != nil { return x.Domain } return nil } type GeoSiteList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` } func (x *GeoSiteList) Reset() { *x = GeoSiteList{} if protoimpl.UnsafeEnabled { mi := &file_component_geodata_router_config_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoSiteList) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoSiteList) ProtoMessage() {} func (x *GeoSiteList) ProtoReflect() protoreflect.Message { mi := &file_component_geodata_router_config_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead. func (*GeoSiteList) Descriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{5} } func (x *GeoSiteList) GetEntry() []*GeoSite { if x != nil { return x.Entry } return nil } type Domain_Attribute struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Types that are assignable to TypedValue: // *Domain_Attribute_BoolValue // *Domain_Attribute_IntValue TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"` } func (x *Domain_Attribute) Reset() { *x = Domain_Attribute{} if protoimpl.UnsafeEnabled { mi := &file_component_geodata_router_config_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Domain_Attribute) String() string { return protoimpl.X.MessageStringOf(x) } func (*Domain_Attribute) ProtoMessage() {} func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { mi := &file_component_geodata_router_config_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead. func (*Domain_Attribute) Descriptor() ([]byte, []int) { return file_component_geodata_router_config_proto_rawDescGZIP(), []int{0, 0} } func (x *Domain_Attribute) GetKey() string { if x != nil { return x.Key } return "" } func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue { if m != nil { return m.TypedValue } return nil } func (x *Domain_Attribute) GetBoolValue() bool { if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok { return x.BoolValue } return false } func (x *Domain_Attribute) GetIntValue() int64 { if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok { return x.IntValue } return 0 } type isDomain_Attribute_TypedValue interface { isDomain_Attribute_TypedValue() } type Domain_Attribute_BoolValue struct { BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` } type Domain_Attribute_IntValue struct { IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` } func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {} func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {} var File_component_geodata_router_config_proto protoreflect.FileDescriptor var file_component_geodata_router_config_proto_rawDesc = []byte{ 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x22, 0xd1, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4e, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x32, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x89, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x38, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x22, 0x48, 0x0a, 0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x6c, 0x0a, 0x07, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x4c, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x3d, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x7c, 0x0a, 0x22, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x61, 0x63, 0x72, 0x6f, 0x2f, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x1e, 0x43, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_component_geodata_router_config_proto_rawDescOnce sync.Once file_component_geodata_router_config_proto_rawDescData = file_component_geodata_router_config_proto_rawDesc ) func file_component_geodata_router_config_proto_rawDescGZIP() []byte { file_component_geodata_router_config_proto_rawDescOnce.Do(func() { file_component_geodata_router_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_component_geodata_router_config_proto_rawDescData) }) return file_component_geodata_router_config_proto_rawDescData } var file_component_geodata_router_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_component_geodata_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_component_geodata_router_config_proto_goTypes = []interface{}{ (Domain_Type)(0), // 0: mihomo.component.geodata.router.Domain.Type (*Domain)(nil), // 1: mihomo.component.geodata.router.Domain (*CIDR)(nil), // 2: mihomo.component.geodata.router.CIDR (*GeoIP)(nil), // 3: mihomo.component.geodata.router.GeoIP (*GeoIPList)(nil), // 4: mihomo.component.geodata.router.GeoIPList (*GeoSite)(nil), // 5: mihomo.component.geodata.router.GeoSite (*GeoSiteList)(nil), // 6: mihomo.component.geodata.router.GeoSiteList (*Domain_Attribute)(nil), // 7: mihomo.component.geodata.router.Domain.Attribute } var file_component_geodata_router_config_proto_depIdxs = []int32{ 0, // 0: mihomo.component.geodata.router.Domain.type:type_name -> mihomo.component.geodata.router.Domain.Type 7, // 1: mihomo.component.geodata.router.Domain.attribute:type_name -> mihomo.component.geodata.router.Domain.Attribute 2, // 2: mihomo.component.geodata.router.GeoIP.cidr:type_name -> mihomo.component.geodata.router.CIDR 3, // 3: mihomo.component.geodata.router.GeoIPList.entry:type_name -> mihomo.component.geodata.router.GeoIP 1, // 4: mihomo.component.geodata.router.GeoSite.domain:type_name -> mihomo.component.geodata.router.Domain 5, // 5: mihomo.component.geodata.router.GeoSiteList.entry:type_name -> mihomo.component.geodata.router.GeoSite 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name } func init() { file_component_geodata_router_config_proto_init() } func file_component_geodata_router_config_proto_init() { if File_component_geodata_router_config_proto != nil { return } if !protoimpl.UnsafeEnabled { file_component_geodata_router_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Domain); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_component_geodata_router_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CIDR); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_component_geodata_router_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoIP); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_component_geodata_router_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoIPList); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_component_geodata_router_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoSite); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_component_geodata_router_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoSiteList); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_component_geodata_router_config_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Domain_Attribute); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } file_component_geodata_router_config_proto_msgTypes[6].OneofWrappers = []interface{}{ (*Domain_Attribute_BoolValue)(nil), (*Domain_Attribute_IntValue)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_component_geodata_router_config_proto_rawDesc, NumEnums: 1, NumMessages: 7, NumExtensions: 0, NumServices: 0, }, GoTypes: file_component_geodata_router_config_proto_goTypes, DependencyIndexes: file_component_geodata_router_config_proto_depIdxs, EnumInfos: file_component_geodata_router_config_proto_enumTypes, MessageInfos: file_component_geodata_router_config_proto_msgTypes, }.Build() File_component_geodata_router_config_proto = out.File file_component_geodata_router_config_proto_rawDesc = nil file_component_geodata_router_config_proto_goTypes = nil file_component_geodata_router_config_proto_depIdxs = nil } ================================================ FILE: core/Clash.Meta/component/geodata/router/config.proto ================================================ syntax = "proto3"; package mihomo.component.geodata.router; option csharp_namespace = "Mihomo.Component.Geodata.Router"; option go_package = "github.com/metacubex/mihomo/component/geodata/router"; option java_package = "com.mihomo.component.geodata.router"; option java_multiple_files = true; // Domain for routing decision. message Domain { // Type of domain value. enum Type { // The value is used as is. Plain = 0; // The value is used as a regular expression. Regex = 1; // The value is a root domain. Domain = 2; // The value is a domain. Full = 3; } // Domain matching type. Type type = 1; // Domain value. string value = 2; message Attribute { string key = 1; oneof typed_value { bool bool_value = 2; int64 int_value = 3; } } // Attributes of this domain. May be used for filtering. repeated Attribute attribute = 3; } // IP for routing decision, in CIDR form. message CIDR { // IP address, should be either 4 or 16 bytes. bytes ip = 1; // Number of leading ones in the network mask. uint32 prefix = 2; } message GeoIP { string country_code = 1; repeated CIDR cidr = 2; bool reverse_match = 3; } message GeoIPList { repeated GeoIP entry = 1; } message GeoSite { string country_code = 1; repeated Domain domain = 2; } message GeoSiteList { repeated GeoSite entry = 1; } ================================================ FILE: core/Clash.Meta/component/geodata/standard/standard.go ================================================ package standard import ( "fmt" "io" "os" "strings" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/geodata/router" C "github.com/metacubex/mihomo/constant" "google.golang.org/protobuf/proto" ) func ReadFile(path string) ([]byte, error) { reader, err := os.Open(path) if err != nil { return nil, err } defer func(reader *os.File) { _ = reader.Close() }(reader) return io.ReadAll(reader) } func ReadAsset(file string) ([]byte, error) { return ReadFile(C.Path.GetAssetLocation(file)) } func loadIP(geoipBytes []byte, country string) ([]*router.CIDR, error) { var geoipList router.GeoIPList if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { return nil, err } for _, geoip := range geoipList.Entry { if strings.EqualFold(geoip.CountryCode, country) { return geoip.Cidr, nil } } return nil, fmt.Errorf("country %s not found", country) } func loadSite(geositeBytes []byte, list string) ([]*router.Domain, error) { var geositeList router.GeoSiteList if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { return nil, err } for _, site := range geositeList.Entry { if strings.EqualFold(site.CountryCode, list) { return site.Domain, nil } } return nil, fmt.Errorf("list %s not found", list) } type standardLoader struct{} func (d standardLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) { geositeBytes, err := ReadAsset(filename) if err != nil { return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error()) } return loadSite(geositeBytes, list) } func (d standardLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) { return loadSite(geositeBytes, list) } func (d standardLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) { geoipBytes, err := ReadAsset(filename) if err != nil { return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error()) } return loadIP(geoipBytes, country) } func (d standardLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) { return loadIP(geoipBytes, country) } func init() { geodata.RegisterGeoDataLoaderImplementationCreator("standard", func() geodata.LoaderImplementation { return standardLoader{} }) } ================================================ FILE: core/Clash.Meta/component/geodata/strmatcher/ac_automaton_matcher.go ================================================ package strmatcher import ( list "github.com/bahlo/generic-list-go" ) const validCharCount = 53 type MatchType struct { matchType Type exist bool } const ( TrieEdge bool = true FailEdge bool = false ) type Edge struct { edgeType bool nextNode int } type ACAutomaton struct { trie [][validCharCount]Edge fail []int exists []MatchType count int } func newNode() [validCharCount]Edge { var s [validCharCount]Edge for i := range s { s[i] = Edge{ edgeType: FailEdge, nextNode: 0, } } return s } var char2Index = [...]int{ 'A': 0, 'a': 0, 'B': 1, 'b': 1, 'C': 2, 'c': 2, 'D': 3, 'd': 3, 'E': 4, 'e': 4, 'F': 5, 'f': 5, 'G': 6, 'g': 6, 'H': 7, 'h': 7, 'I': 8, 'i': 8, 'J': 9, 'j': 9, 'K': 10, 'k': 10, 'L': 11, 'l': 11, 'M': 12, 'm': 12, 'N': 13, 'n': 13, 'O': 14, 'o': 14, 'P': 15, 'p': 15, 'Q': 16, 'q': 16, 'R': 17, 'r': 17, 'S': 18, 's': 18, 'T': 19, 't': 19, 'U': 20, 'u': 20, 'V': 21, 'v': 21, 'W': 22, 'w': 22, 'X': 23, 'x': 23, 'Y': 24, 'y': 24, 'Z': 25, 'z': 25, '!': 26, '$': 27, '&': 28, '\'': 29, '(': 30, ')': 31, '*': 32, '+': 33, ',': 34, ';': 35, '=': 36, ':': 37, '%': 38, '-': 39, '.': 40, '_': 41, '~': 42, '0': 43, '1': 44, '2': 45, '3': 46, '4': 47, '5': 48, '6': 49, '7': 50, '8': 51, '9': 52, } func NewACAutomaton() *ACAutomaton { ac := new(ACAutomaton) ac.trie = append(ac.trie, newNode()) ac.fail = append(ac.fail, 0) ac.exists = append(ac.exists, MatchType{ matchType: Full, exist: false, }) return ac } func (ac *ACAutomaton) Add(domain string, t Type) { node := 0 for i := len(domain) - 1; i >= 0; i-- { idx := char2Index[domain[i]] if ac.trie[node][idx].nextNode == 0 { ac.count++ if len(ac.trie) < ac.count+1 { ac.trie = append(ac.trie, newNode()) ac.fail = append(ac.fail, 0) ac.exists = append(ac.exists, MatchType{ matchType: Full, exist: false, }) } ac.trie[node][idx] = Edge{ edgeType: TrieEdge, nextNode: ac.count, } } node = ac.trie[node][idx].nextNode } ac.exists[node] = MatchType{ matchType: t, exist: true, } switch t { case Domain: ac.exists[node] = MatchType{ matchType: Full, exist: true, } idx := char2Index['.'] if ac.trie[node][idx].nextNode == 0 { ac.count++ if len(ac.trie) < ac.count+1 { ac.trie = append(ac.trie, newNode()) ac.fail = append(ac.fail, 0) ac.exists = append(ac.exists, MatchType{ matchType: Full, exist: false, }) } ac.trie[node][idx] = Edge{ edgeType: TrieEdge, nextNode: ac.count, } } node = ac.trie[node][idx].nextNode ac.exists[node] = MatchType{ matchType: t, exist: true, } default: break } } func (ac *ACAutomaton) Build() { queue := list.New[Edge]() for i := 0; i < validCharCount; i++ { if ac.trie[0][i].nextNode != 0 { queue.PushBack(ac.trie[0][i]) } } for { front := queue.Front() if front == nil { break } else { node := front.Value.nextNode queue.Remove(front) for i := 0; i < validCharCount; i++ { if ac.trie[node][i].nextNode != 0 { ac.fail[ac.trie[node][i].nextNode] = ac.trie[ac.fail[node]][i].nextNode queue.PushBack(ac.trie[node][i]) } else { ac.trie[node][i] = Edge{ edgeType: FailEdge, nextNode: ac.trie[ac.fail[node]][i].nextNode, } } } } } } func (ac *ACAutomaton) Match(s string) bool { node := 0 fullMatch := true // 1. the match string is all through trie edge. FULL MATCH or DOMAIN // 2. the match string is through a fail edge. NOT FULL MATCH // 2.1 Through a fail edge, but there exists a valid node. SUBSTR for i := len(s) - 1; i >= 0; i-- { idx := char2Index[s[i]] fullMatch = fullMatch && ac.trie[node][idx].edgeType node = ac.trie[node][idx].nextNode switch ac.exists[node].matchType { case Substr: return true case Domain: if fullMatch { return true } } } return fullMatch && ac.exists[node].exist } ================================================ FILE: core/Clash.Meta/component/geodata/strmatcher/matchers.go ================================================ package strmatcher import ( "regexp" "strings" ) type fullMatcher string func (m fullMatcher) Match(s string) bool { return string(m) == s } func (m fullMatcher) String() string { return "full:" + string(m) } type substrMatcher string func (m substrMatcher) Match(s string) bool { return strings.Contains(s, string(m)) } func (m substrMatcher) String() string { return "keyword:" + string(m) } type domainMatcher string func (m domainMatcher) Match(s string) bool { pattern := string(m) if !strings.HasSuffix(s, pattern) { return false } return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.' } func (m domainMatcher) String() string { return "domain:" + string(m) } type regexMatcher struct { pattern *regexp.Regexp } func (m *regexMatcher) Match(s string) bool { return m.pattern.MatchString(s) } func (m *regexMatcher) String() string { return "regexp:" + m.pattern.String() } ================================================ FILE: core/Clash.Meta/component/geodata/strmatcher/mph_matcher.go ================================================ package strmatcher import ( "math/bits" "regexp" "sort" "strings" "unsafe" ) // PrimeRK is the prime base used in Rabin-Karp algorithm. const PrimeRK = 16777619 // calculate the rolling murmurHash of given string func RollingHash(s string) uint32 { h := uint32(0) for i := len(s) - 1; i >= 0; i-- { h = h*PrimeRK + uint32(s[i]) } return h } // A MphMatcherGroup is divided into three parts: // 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table; // 2. `substr` patterns are matched by ac automaton; // 3. `regex` patterns are matched with the regex library. type MphMatcherGroup struct { ac *ACAutomaton otherMatchers []matcherEntry rules []string level0 []uint32 level0Mask int level1 []uint32 level1Mask int count uint32 ruleMap *map[string]uint32 } func (g *MphMatcherGroup) AddFullOrDomainPattern(pattern string, t Type) { h := RollingHash(pattern) switch t { case Domain: (*g.ruleMap)["."+pattern] = h*PrimeRK + uint32('.') fallthrough case Full: (*g.ruleMap)[pattern] = h default: } } func NewMphMatcherGroup() *MphMatcherGroup { return &MphMatcherGroup{ ac: nil, otherMatchers: nil, rules: nil, level0: nil, level0Mask: 0, level1: nil, level1Mask: 0, count: 1, ruleMap: &map[string]uint32{}, } } // AddPattern adds a pattern to MphMatcherGroup func (g *MphMatcherGroup) AddPattern(pattern string, t Type) (uint32, error) { switch t { case Substr: if g.ac == nil { g.ac = NewACAutomaton() } g.ac.Add(pattern, t) case Full, Domain: pattern = strings.ToLower(pattern) g.AddFullOrDomainPattern(pattern, t) case Regex: r, err := regexp.Compile(pattern) if err != nil { return 0, err } g.otherMatchers = append(g.otherMatchers, matcherEntry{ m: ®exMatcher{pattern: r}, id: g.count, }) default: panic("Unknown type") } return g.count, nil } // Build builds a minimal perfect hash table and ac automaton from insert rules func (g *MphMatcherGroup) Build() { if g.ac != nil { g.ac.Build() } keyLen := len(*g.ruleMap) if keyLen == 0 { keyLen = 1 (*g.ruleMap)["empty___"] = RollingHash("empty___") } g.level0 = make([]uint32, nextPow2(keyLen/4)) g.level0Mask = len(g.level0) - 1 g.level1 = make([]uint32, nextPow2(keyLen)) g.level1Mask = len(g.level1) - 1 sparseBuckets := make([][]int, len(g.level0)) var ruleIdx int for rule, hash := range *g.ruleMap { n := int(hash) & g.level0Mask g.rules = append(g.rules, rule) sparseBuckets[n] = append(sparseBuckets[n], ruleIdx) ruleIdx++ } g.ruleMap = nil var buckets []indexBucket for n, vals := range sparseBuckets { if len(vals) > 0 { buckets = append(buckets, indexBucket{n, vals}) } } sort.Sort(bySize(buckets)) occ := make([]bool, len(g.level1)) var tmpOcc []int for _, bucket := range buckets { seed := uint32(0) for { findSeed := true tmpOcc = tmpOcc[:0] for _, i := range bucket.vals { n := int(strhashFallback(unsafe.Pointer(&g.rules[i]), uintptr(seed))) & g.level1Mask if occ[n] { for _, n := range tmpOcc { occ[n] = false } seed++ findSeed = false break } occ[n] = true tmpOcc = append(tmpOcc, n) g.level1[n] = uint32(i) } if findSeed { g.level0[bucket.n] = seed break } } } } func nextPow2(v int) int { if v <= 1 { return 1 } const MaxUInt = ^uint(0) n := (MaxUInt >> bits.LeadingZeros(uint(v))) + 1 return int(n) } // Lookup searches for s in t and returns its index and whether it was found. func (g *MphMatcherGroup) Lookup(h uint32, s string) bool { i0 := int(h) & g.level0Mask seed := g.level0[i0] i1 := int(strhashFallback(unsafe.Pointer(&s), uintptr(seed))) & g.level1Mask n := g.level1[i1] return s == g.rules[int(n)] } // Match implements IndexMatcher.Match. func (g *MphMatcherGroup) Match(pattern string) []uint32 { result := []uint32{} hash := uint32(0) for i := len(pattern) - 1; i >= 0; i-- { hash = hash*PrimeRK + uint32(pattern[i]) if pattern[i] == '.' { if g.Lookup(hash, pattern[i:]) { result = append(result, 1) return result } } } if g.Lookup(hash, pattern) { result = append(result, 1) return result } if g.ac != nil && g.ac.Match(pattern) { result = append(result, 1) return result } for _, e := range g.otherMatchers { if e.m.Match(pattern) { result = append(result, e.id) return result } } return nil } type indexBucket struct { n int vals []int } type bySize []indexBucket func (s bySize) Len() int { return len(s) } func (s bySize) Less(i, j int) bool { return len(s[i].vals) > len(s[j].vals) } func (s bySize) Swap(i, j int) { s[i], s[j] = s[j], s[i] } type stringStruct struct { str unsafe.Pointer len int } func strhashFallback(a unsafe.Pointer, h uintptr) uintptr { x := (*stringStruct)(a) return memhashFallback(x.str, h, uintptr(x.len)) } const ( // Constants for multiplication: four random odd 64-bit numbers. m1 = 16877499708836156737 m2 = 2820277070424839065 m3 = 9497967016996688599 m4 = 15839092249703872147 ) var hashkey = [4]uintptr{1, 1, 1, 1} func memhashFallback(p unsafe.Pointer, seed, s uintptr) uintptr { h := uint64(seed + s*hashkey[0]) tail: switch { case s == 0: case s < 4: h ^= uint64(*(*byte)(p)) h ^= uint64(*(*byte)(unsafe.Add(p, s>>1))) << 8 h ^= uint64(*(*byte)(unsafe.Add(p, s-1))) << 16 h = bits.RotateLeft64(h*m1, 31) * m2 case s <= 8: h ^= uint64(readUnaligned32(p)) h ^= uint64(readUnaligned32(unsafe.Add(p, s-4))) << 32 h = bits.RotateLeft64(h*m1, 31) * m2 case s <= 16: h ^= readUnaligned64(p) h = bits.RotateLeft64(h*m1, 31) * m2 h ^= readUnaligned64(unsafe.Add(p, s-8)) h = bits.RotateLeft64(h*m1, 31) * m2 case s <= 32: h ^= readUnaligned64(p) h = bits.RotateLeft64(h*m1, 31) * m2 h ^= readUnaligned64(unsafe.Add(p, 8)) h = bits.RotateLeft64(h*m1, 31) * m2 h ^= readUnaligned64(unsafe.Add(p, s-16)) h = bits.RotateLeft64(h*m1, 31) * m2 h ^= readUnaligned64(unsafe.Add(p, s-8)) h = bits.RotateLeft64(h*m1, 31) * m2 default: v1 := h v2 := uint64(seed * hashkey[1]) v3 := uint64(seed * hashkey[2]) v4 := uint64(seed * hashkey[3]) for s >= 32 { v1 ^= readUnaligned64(p) v1 = bits.RotateLeft64(v1*m1, 31) * m2 p = unsafe.Add(p, 8) v2 ^= readUnaligned64(p) v2 = bits.RotateLeft64(v2*m2, 31) * m3 p = unsafe.Add(p, 8) v3 ^= readUnaligned64(p) v3 = bits.RotateLeft64(v3*m3, 31) * m4 p = unsafe.Add(p, 8) v4 ^= readUnaligned64(p) v4 = bits.RotateLeft64(v4*m4, 31) * m1 p = unsafe.Add(p, 8) s -= 32 } h = v1 ^ v2 ^ v3 ^ v4 goto tail } h ^= h >> 29 h *= m3 h ^= h >> 32 return uintptr(h) } func readUnaligned32(p unsafe.Pointer) uint32 { q := (*[4]byte)(p) return uint32(q[0]) | uint32(q[1])<<8 | uint32(q[2])<<16 | uint32(q[3])<<24 } func readUnaligned64(p unsafe.Pointer) uint64 { q := (*[8]byte)(p) return uint64(q[0]) | uint64(q[1])<<8 | uint64(q[2])<<16 | uint64(q[3])<<24 | uint64(q[4])<<32 | uint64(q[5])<<40 | uint64(q[6])<<48 | uint64(q[7])<<56 } ================================================ FILE: core/Clash.Meta/component/geodata/strmatcher/package_info.go ================================================ // Modified from: https://github.com/v2fly/v2ray-core/tree/master/common/strmatcher // License: MIT package strmatcher ================================================ FILE: core/Clash.Meta/component/geodata/strmatcher/strmatcher.go ================================================ package strmatcher import ( "regexp" ) // Matcher is the interface to determine a string matches a pattern. type Matcher interface { // Match returns true if the given string matches a predefined pattern. Match(string) bool String() string } // Type is the type of the matcher. type Type byte const ( // Full is the type of matcher that the input string must exactly equal to the pattern. Full Type = iota // Substr is the type of matcher that the input string must contain the pattern as a sub-string. Substr // Domain is the type of matcher that the input string must be a sub-domain or itself of the pattern. Domain // Regex is the type of matcher that the input string must matches the regular-expression pattern. Regex ) // New creates a new Matcher based on the given pattern. func (t Type) New(pattern string) (Matcher, error) { // 1. regex matching is case-sensitive switch t { case Full: return fullMatcher(pattern), nil case Substr: return substrMatcher(pattern), nil case Domain: return domainMatcher(pattern), nil case Regex: r, err := regexp.Compile(pattern) if err != nil { return nil, err } return ®exMatcher{ pattern: r, }, nil default: panic("Unknown type") } } // IndexMatcher is the interface for matching with a group of matchers. type IndexMatcher interface { // Match returns the index of a matcher that matches the input. It returns empty array if no such matcher exists. Match(input string) []uint32 } type matcherEntry struct { m Matcher id uint32 } ================================================ FILE: core/Clash.Meta/component/geodata/utils.go ================================================ package geodata import ( "fmt" "strings" "github.com/metacubex/mihomo/common/singleflight" "github.com/metacubex/mihomo/component/geodata/router" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) var ( geoMode bool geoLoaderName = "memconservative" geoSiteMatcher = "succinct" ) // geoLoaderName = "standard" func GeodataMode() bool { return geoMode } func LoaderName() string { return geoLoaderName } func SiteMatcherName() string { return geoSiteMatcher } func SetGeodataMode(newGeodataMode bool) { geoMode = newGeodataMode } func SetLoader(newLoader string) { if newLoader == "memc" { newLoader = "memconservative" } geoLoaderName = newLoader } func SetSiteMatcher(newMatcher string) { switch newMatcher { case "mph", "hybrid": geoSiteMatcher = "mph" default: geoSiteMatcher = "succinct" } } func Verify(name string) error { switch name { case C.GeositeName: _, err := LoadGeoSiteMatcher("CN") return err case C.GeoipName: _, err := LoadGeoIPMatcher("CN") return err default: return fmt.Errorf("not support name") } } var loadGeoSiteMatcherListSF = singleflight.Group[[]*router.Domain]{StoreResult: true} var loadGeoSiteMatcherSF = singleflight.Group[router.DomainMatcher]{StoreResult: true} func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) { if countryCode == "" { return nil, fmt.Errorf("country code could not be empty") } not := false if countryCode[0] == '!' { not = true countryCode = countryCode[1:] if countryCode == "" { return nil, fmt.Errorf("country code could not be empty") } } countryCode = strings.ToLower(countryCode) parts := strings.Split(countryCode, "@") listName := strings.TrimSpace(parts[0]) attrVal := parts[1:] attrs := parseAttrs(attrVal) if listName == "" { return nil, fmt.Errorf("empty listname in rule: %s", countryCode) } matcherName := listName if !attrs.IsEmpty() { matcherName += "@" + attrs.String() } matcher, err, shared := loadGeoSiteMatcherSF.Do(matcherName, func() (router.DomainMatcher, error) { log.Infoln("Load GeoSite rule: %s", matcherName) domains, err, shared := loadGeoSiteMatcherListSF.Do(listName, func() ([]*router.Domain, error) { geoLoader, err := GetGeoDataLoader(geoLoaderName) if err != nil { return nil, err } return geoLoader.LoadGeoSite(listName) }) if err != nil { if !shared { loadGeoSiteMatcherListSF.Forget(listName) // don't store the error result } return nil, err } if attrs.IsEmpty() { if strings.Contains(countryCode, "@") { log.Warnln("empty attribute list: %s", countryCode) } } else { filteredDomains := make([]*router.Domain, 0, len(domains)) hasAttrMatched := false for _, domain := range domains { if attrs.Match(domain) { hasAttrMatched = true filteredDomains = append(filteredDomains, domain) } } if !hasAttrMatched { log.Warnln("attribute match no rule: geosite: %s", countryCode) } domains = filteredDomains } /** linear: linear algorithm matcher, err := router.NewDomainMatcher(domains) mph:minimal perfect hash algorithm */ if geoSiteMatcher == "mph" { return router.NewMphMatcherGroup(domains) } else { return router.NewSuccinctMatcherGroup(domains) } }) if err != nil { if !shared { loadGeoSiteMatcherSF.Forget(matcherName) // don't store the error result } return nil, err } if not { matcher = router.NewNotDomainMatcherGroup(matcher) } return matcher, nil } var loadGeoIPMatcherSF = singleflight.Group[router.IPMatcher]{StoreResult: true} func LoadGeoIPMatcher(country string) (router.IPMatcher, error) { if len(country) == 0 { return nil, fmt.Errorf("country code could not be empty") } not := false if country[0] == '!' { not = true country = country[1:] } country = strings.ToLower(country) matcher, err, shared := loadGeoIPMatcherSF.Do(country, func() (router.IPMatcher, error) { log.Infoln("Load GeoIP rule: %s", country) geoLoader, err := GetGeoDataLoader(geoLoaderName) if err != nil { return nil, err } cidrList, err := geoLoader.LoadGeoIP(country) if err != nil { return nil, err } return router.NewGeoIPMatcher(cidrList) }) if err != nil { if !shared { loadGeoIPMatcherSF.Forget(country) // don't store the error result log.Warnln("Load GeoIP rule: %s", country) } return nil, err } if not { matcher = router.NewNotIpMatcherGroup(matcher) } return matcher, nil } func ClearGeoSiteCache() { loadGeoSiteMatcherListSF.Reset() loadGeoSiteMatcherSF.Reset() } func ClearGeoIPCache() { loadGeoIPMatcherSF.Reset() } ================================================ FILE: core/Clash.Meta/component/http/http.go ================================================ package http import ( "context" "io" "net" URL "net/url" "runtime" "strings" "time" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/listener/inner" "github.com/metacubex/http" ) var ( ua string ) func UA() string { return ua } func SetUA(UA string) { ua = UA } func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader, options ...Option) (*http.Response, error) { opt := option{} for _, o := range options { o(&opt) } method = strings.ToUpper(method) urlRes, err := URL.Parse(url) if err != nil { return nil, err } req, err := http.NewRequest(method, urlRes.String(), body) if err != nil { return nil, err } for k, v := range header { for _, v := range v { req.Header.Add(k, v) } } if req.Header.Get("User-Agent") == "" { req.Header.Set("User-Agent", UA()) } if user := urlRes.User; user != nil { password, _ := user.Password() req.SetBasicAuth(user.Username(), password) } req = req.WithContext(ctx) tlsConfig, err := ca.GetTLSConfig(opt.caOption) if err != nil { return nil, err } transport := &http.Transport{ // from http.DefaultTransport DisableKeepAlives: runtime.GOOS == "android", MaxIdleConns: 100, IdleConnTimeout: 30 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { if conn, err := inner.HandleTcp(inner.GetTunnel(), address, opt.specialProxy); err == nil { return conn, nil } else { return dialer.DialContext(ctx, network, address) } }, TLSClientConfig: tlsConfig, } client := http.Client{Transport: transport} return client.Do(req) } type Option func(opt *option) type option struct { specialProxy string caOption ca.Option } func WithSpecialProxy(name string) Option { return func(opt *option) { opt.specialProxy = name } } func WithCAOption(caOption ca.Option) Option { return func(opt *option) { opt.caOption = caOption } } ================================================ FILE: core/Clash.Meta/component/iface/iface.go ================================================ package iface import ( "errors" "net" "net/netip" "sync" "time" "github.com/metacubex/mihomo/common/singledo" "github.com/metacubex/bart" ) type Interface struct { Index int MTU int Name string HardwareAddr net.HardwareAddr Flags net.Flags Addresses []netip.Prefix } var ( ErrIfaceNotFound = errors.New("interface not found") ErrAddrNotFound = errors.New("addr not found") ) var ( ifaceLogMu sync.Mutex ifaceLogLastTime time.Time ifaceLogCount int ifaceLogSuppressed bool ) type ifaceCache struct { ifMapByName map[string]*Interface ifMapByAddr map[netip.Addr]*Interface ifTable bart.Table[*Interface] } var caches = singledo.NewSingle[*ifaceCache](time.Second * 20) func ShouldLogIfaceError() bool { ifaceLogMu.Lock() defer ifaceLogMu.Unlock() now := time.Now() if now.Sub(ifaceLogLastTime) >= time.Second { ifaceLogLastTime = now ifaceLogCount = 0 if ifaceLogSuppressed { ifaceLogSuppressed = false return true } } if ifaceLogCount >= 10 { if !ifaceLogSuppressed { ifaceLogSuppressed = true return true } return false } ifaceLogCount++ return true } func getCache() (*ifaceCache, error) { value, err, _ := caches.Do(func() (*ifaceCache, error) { ifaces, err := net.Interfaces() if err != nil { return nil, err } cache := &ifaceCache{ ifMapByName: make(map[string]*Interface), ifMapByAddr: make(map[netip.Addr]*Interface), } for _, iface := range ifaces { addrs, err := iface.Addrs() if err != nil { continue } ipNets := make([]netip.Prefix, 0, len(addrs)) for _, addr := range addrs { var pf netip.Prefix switch ipNet := addr.(type) { case *net.IPNet: ip, _ := netip.AddrFromSlice(ipNet.IP) ones, bits := ipNet.Mask.Size() if bits == 32 { ip = ip.Unmap() } pf = netip.PrefixFrom(ip, ones) case *net.IPAddr: ip, _ := netip.AddrFromSlice(ipNet.IP) ip = ip.Unmap() pf = netip.PrefixFrom(ip, ip.BitLen()) } if pf.IsValid() { ipNets = append(ipNets, pf) } } ifaceObj := &Interface{ Index: iface.Index, MTU: iface.MTU, Name: iface.Name, HardwareAddr: iface.HardwareAddr, Flags: iface.Flags, Addresses: ipNets, } cache.ifMapByName[iface.Name] = ifaceObj if iface.Flags&net.FlagUp == 0 { continue // interface down } for _, prefix := range ipNets { cache.ifMapByAddr[prefix.Addr()] = ifaceObj cache.ifTable.Insert(prefix, ifaceObj) } } return cache, nil }) return value, err } func Interfaces() (map[string]*Interface, error) { cache, err := getCache() if err != nil { return nil, err } return cache.ifMapByName, nil } func ResolveInterface(name string) (*Interface, error) { ifaces, err := Interfaces() if err != nil { return nil, err } iface, ok := ifaces[name] if !ok { if ShouldLogIfaceError() { return nil, ErrIfaceNotFound } return nil, ErrIfaceNotFound } return iface, nil } func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) { cache, err := getCache() if err != nil { return nil, err } // maybe two interfaces have the same prefix but different address // so direct check address equal before do a route lookup (longest prefix match) if iface, ok := cache.ifMapByAddr[addr]; ok { return iface, nil } iface, ok := cache.ifTable.Lookup(addr) if !ok { // 始终返回错误,但只在需要时记录日志 if ShouldLogIfaceError() { return nil, ErrIfaceNotFound } return nil, ErrIfaceNotFound } return iface, nil } func IsLocalIp(addr netip.Addr) (bool, error) { cache, err := getCache() if err != nil { return false, err } _, ok := cache.ifMapByAddr[addr] return ok, nil } func FlushCache() { caches.Reset() } func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) { return iface.pickIPAddr(destination, func(addr netip.Prefix) bool { return addr.Addr().Is4() }) } func (iface *Interface) PickIPv6Addr(destination netip.Addr) (netip.Prefix, error) { return iface.pickIPAddr(destination, func(addr netip.Prefix) bool { return addr.Addr().Is6() }) } func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr netip.Prefix) bool) (netip.Prefix, error) { var fallback netip.Prefix for _, addr := range iface.Addresses { if !accept(addr) { continue } if !fallback.IsValid() && !addr.Addr().IsLinkLocalUnicast() { fallback = addr if !destination.IsValid() { break } } if destination.IsValid() && addr.Contains(destination) { return addr, nil } } if !fallback.IsValid() { return netip.Prefix{}, ErrAddrNotFound } return fallback, nil } ================================================ FILE: core/Clash.Meta/component/keepalive/tcp_keepalive.go ================================================ package keepalive import ( "net" "runtime" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/utils" ) var ( keepAliveIdle = atomic.NewInt64(0) keepAliveInterval = atomic.NewInt64(0) disableKeepAlive = atomic.NewBool(false) SetDisableKeepAliveCallback = utils.NewCallback[bool]() ) func SetKeepAliveIdle(t time.Duration) { keepAliveIdle.Store(int64(t)) } func SetKeepAliveInterval(t time.Duration) { keepAliveInterval.Store(int64(t)) } func KeepAliveIdle() time.Duration { return time.Duration(keepAliveIdle.Load()) } func KeepAliveInterval() time.Duration { return time.Duration(keepAliveInterval.Load()) } func SetDisableKeepAlive(disable bool) { if runtime.GOOS == "android" { setDisableKeepAlive(true) } else { setDisableKeepAlive(disable) } } func setDisableKeepAlive(disable bool) { disableKeepAlive.Store(disable) SetDisableKeepAliveCallback.Emit(disable) } func DisableKeepAlive() bool { return disableKeepAlive.Load() } func SetNetDialer(dialer *net.Dialer) { setNetDialer(dialer) } func SetNetListenConfig(lc *net.ListenConfig) { setNetListenConfig(lc) } func TCPKeepAlive(c net.Conn) { if tcp, ok := c.(TCPConn); ok && tcp != nil { tcpKeepAlive(tcp) } } ================================================ FILE: core/Clash.Meta/component/keepalive/tcp_keepalive_go122.go ================================================ //go:build !go1.23 package keepalive import ( "net" "time" ) type TCPConn interface { net.Conn SetKeepAlive(keepalive bool) error SetKeepAlivePeriod(d time.Duration) error } func tcpKeepAlive(tcp TCPConn) { if DisableKeepAlive() { _ = tcp.SetKeepAlive(false) } else { _ = tcp.SetKeepAlive(true) _ = tcp.SetKeepAlivePeriod(KeepAliveInterval()) } } func setNetDialer(dialer *net.Dialer) { if DisableKeepAlive() { dialer.KeepAlive = -1 // If negative, keep-alive probes are disabled. } else { dialer.KeepAlive = KeepAliveInterval() } } func setNetListenConfig(lc *net.ListenConfig) { if DisableKeepAlive() { lc.KeepAlive = -1 // If negative, keep-alive probes are disabled. } else { lc.KeepAlive = KeepAliveInterval() } } ================================================ FILE: core/Clash.Meta/component/keepalive/tcp_keepalive_go123.go ================================================ //go:build go1.23 package keepalive import "net" type TCPConn interface { net.Conn SetKeepAlive(keepalive bool) error SetKeepAliveConfig(config net.KeepAliveConfig) error } func keepAliveConfig() net.KeepAliveConfig { config := net.KeepAliveConfig{ Enable: true, Idle: KeepAliveIdle(), Interval: KeepAliveInterval(), } if !SupportTCPKeepAliveCount() { // it's recommended to set both Idle and Interval to non-negative values in conjunction with a -1 // for Count on those old Windows if you intend to customize the TCP keep-alive settings. config.Count = -1 } return config } func tcpKeepAlive(tcp TCPConn) { if DisableKeepAlive() { _ = tcp.SetKeepAlive(false) } else { _ = tcp.SetKeepAliveConfig(keepAliveConfig()) } } func setNetDialer(dialer *net.Dialer) { if DisableKeepAlive() { dialer.KeepAlive = -1 // If negative, keep-alive probes are disabled. dialer.KeepAliveConfig.Enable = false } else { dialer.KeepAliveConfig = keepAliveConfig() } } func setNetListenConfig(lc *net.ListenConfig) { if DisableKeepAlive() { lc.KeepAlive = -1 // If negative, keep-alive probes are disabled. lc.KeepAliveConfig.Enable = false } else { lc.KeepAliveConfig = keepAliveConfig() } } ================================================ FILE: core/Clash.Meta/component/keepalive/tcp_keepalive_go123_unix.go ================================================ //go:build go1.23 && unix package keepalive func SupportTCPKeepAliveIdle() bool { return true } func SupportTCPKeepAliveInterval() bool { return true } func SupportTCPKeepAliveCount() bool { return true } ================================================ FILE: core/Clash.Meta/component/keepalive/tcp_keepalive_go123_windows.go ================================================ //go:build go1.23 && windows // copy and modify from golang1.23's internal/syscall/windows/version_windows.go package keepalive import ( "errors" "sync" "syscall" "github.com/metacubex/mihomo/constant/features" "golang.org/x/sys/windows" ) var ( supportTCPKeepAliveIdle bool supportTCPKeepAliveInterval bool supportTCPKeepAliveCount bool ) var initTCPKeepAlive = sync.OnceFunc(func() { s, err := windows.WSASocket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP, nil, 0, windows.WSA_FLAG_NO_HANDLE_INHERIT) if err != nil { // Fallback to checking the Windows version. major, build := features.WindowsMajorVersion, features.WindowsBuildNumber supportTCPKeepAliveIdle = major >= 10 && build >= 16299 supportTCPKeepAliveInterval = major >= 10 && build >= 16299 supportTCPKeepAliveCount = major >= 10 && build >= 15063 return } defer windows.Closesocket(s) var optSupported = func(opt int) bool { err := windows.SetsockoptInt(s, syscall.IPPROTO_TCP, opt, 1) return !errors.Is(err, syscall.WSAENOPROTOOPT) } supportTCPKeepAliveIdle = optSupported(windows.TCP_KEEPIDLE) supportTCPKeepAliveInterval = optSupported(windows.TCP_KEEPINTVL) supportTCPKeepAliveCount = optSupported(windows.TCP_KEEPCNT) }) // SupportTCPKeepAliveIdle indicates whether TCP_KEEPIDLE is supported. // The minimal requirement is Windows 10.0.16299. func SupportTCPKeepAliveIdle() bool { initTCPKeepAlive() return supportTCPKeepAliveIdle } // SupportTCPKeepAliveInterval indicates whether TCP_KEEPINTVL is supported. // The minimal requirement is Windows 10.0.16299. func SupportTCPKeepAliveInterval() bool { initTCPKeepAlive() return supportTCPKeepAliveInterval } // SupportTCPKeepAliveCount indicates whether TCP_KEEPCNT is supported. // supports TCP_KEEPCNT. // The minimal requirement is Windows 10.0.15063. func SupportTCPKeepAliveCount() bool { initTCPKeepAlive() return supportTCPKeepAliveCount } ================================================ FILE: core/Clash.Meta/component/loopback/detector.go ================================================ package loopback import ( "errors" "fmt" "net/netip" "os" "strconv" "github.com/metacubex/mihomo/common/callback" "github.com/metacubex/mihomo/common/xsync" "github.com/metacubex/mihomo/component/iface" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" ) var disableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR")) func init() { if features.Android { disableLoopBackDetector = true } } var ErrReject = errors.New("reject loopback connection") type Detector struct { connMap xsync.Map[netip.AddrPort, struct{}] packetConnMap xsync.Map[uint16, struct{}] } func NewDetector() *Detector { if disableLoopBackDetector { return nil } return &Detector{} } func (l *Detector) NewConn(conn C.Conn) C.Conn { if l == nil { return conn } metadata := C.Metadata{} if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { return conn } connAddr := metadata.AddrPort() if !connAddr.IsValid() { return conn } l.connMap.Store(connAddr, struct{}{}) return callback.NewCloseCallbackConn(conn, func() { l.connMap.Delete(connAddr) }) } func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn { if l == nil { return conn } metadata := C.Metadata{} if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { return conn } connAddr := metadata.AddrPort() if !connAddr.IsValid() { return conn } port := connAddr.Port() l.packetConnMap.Store(port, struct{}{}) return callback.NewCloseCallbackPacketConn(conn, func() { l.packetConnMap.Delete(port) }) } func (l *Detector) CheckConn(metadata *C.Metadata) error { if l == nil { return nil } connAddr := metadata.SourceAddrPort() if !connAddr.IsValid() { return nil } if _, ok := l.connMap.Load(connAddr); ok { return fmt.Errorf("%w to: %s", ErrReject, metadata.RemoteAddress()) } return nil } func (l *Detector) CheckPacketConn(metadata *C.Metadata) error { if l == nil { return nil } connAddr := metadata.SourceAddrPort() if !connAddr.IsValid() { return nil } isLocalIp, err := iface.IsLocalIp(connAddr.Addr()) if err != nil { return err } if !isLocalIp && !connAddr.Addr().IsLoopback() { return nil } if _, ok := l.packetConnMap.Load(connAddr.Port()); ok { return fmt.Errorf("%w to: %s", ErrReject, metadata.RemoteAddress()) } return nil } ================================================ FILE: core/Clash.Meta/component/memory/memory.go ================================================ // Package memory return MemoryInfoStat // modify from https://github.com/shirou/gopsutil/tree/v4.25.8/process package memory import ( "errors" "fmt" "math" ) var ErrNotImplementedError = errors.New("not implemented yet") type MemoryInfoStat struct { RSS uint64 `json:"rss"` // bytes VMS uint64 `json:"vms"` // bytes } // PrettyByteSize convert size in bytes to Bytes, Kilobytes, Megabytes, GB and TB // https://gist.github.com/anikitenko/b41206a49727b83a530142c76b1cb82d?permalink_comment_id=4467913#gistcomment-4467913 func PrettyByteSize(b uint64) string { bf := float64(b) for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { if math.Abs(bf) < 1024.0 { return fmt.Sprintf("%3.1f%sB", bf, unit) } bf /= 1024.0 } return fmt.Sprintf("%.1fYiB", bf) } ================================================ FILE: core/Clash.Meta/component/memory/memory_darwin.go ================================================ package memory import ( "syscall" "unsafe" _ "unsafe" ) const PROC_PIDTASKINFO = 4 type ProcTaskInfo struct { Virtual_size uint64 Resident_size uint64 Total_user uint64 Total_system uint64 Threads_user uint64 Threads_system uint64 Policy int32 Faults int32 Pageins int32 Cow_faults int32 Messages_sent int32 Messages_received int32 Syscalls_mach int32 Syscalls_unix int32 Csw int32 Threadnum int32 Numrunning int32 Priority int32 } func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { var ti ProcTaskInfo _, _, errno := syscall_syscall6(proc_pidinfo_trampoline_addr, uintptr(pid), PROC_PIDTASKINFO, 0, uintptr(unsafe.Pointer(&ti)), unsafe.Sizeof(ti), 0) if errno != 0 { return nil, errno } ret := &MemoryInfoStat{ RSS: uint64(ti.Resident_size), VMS: uint64(ti.Virtual_size), } return ret, nil } var proc_pidinfo_trampoline_addr uintptr //go:cgo_import_dynamic proc_pidinfo proc_pidinfo "/usr/lib/libSystem.B.dylib" // from golang.org/x/sys@v0.30.0/unix/syscall_darwin_libSystem.go // Implemented in the runtime package (runtime/sys_darwin.go) func syscall_syscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) func syscall_syscall6X(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) func syscall_syscall9(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err syscall.Errno) // 32-bit only func syscall_rawSyscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) func syscall_rawSyscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) func syscall_syscallPtr(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) //go:linkname syscall_syscall syscall.syscall //go:linkname syscall_syscall6 syscall.syscall6 //go:linkname syscall_syscall6X syscall.syscall6X //go:linkname syscall_syscall9 syscall.syscall9 //go:linkname syscall_rawSyscall syscall.rawSyscall //go:linkname syscall_rawSyscall6 syscall.rawSyscall6 //go:linkname syscall_syscallPtr syscall.syscallPtr ================================================ FILE: core/Clash.Meta/component/memory/memory_darwin_amd64.s ================================================ // go run mkasm.go darwin amd64 // Code generated by the command above; DO NOT EDIT. #include "textflag.h" TEXT proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0 JMP proc_pidinfo(SB) GLOBL ·proc_pidinfo_trampoline_addr(SB), RODATA, $8 DATA ·proc_pidinfo_trampoline_addr(SB)/8, $proc_pidinfo_trampoline<>(SB) ================================================ FILE: core/Clash.Meta/component/memory/memory_darwin_arm64.s ================================================ // go run mkasm.go darwin arm64 // Code generated by the command above; DO NOT EDIT. #include "textflag.h" TEXT proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0 JMP proc_pidinfo(SB) GLOBL ·proc_pidinfo_trampoline_addr(SB), RODATA, $8 DATA ·proc_pidinfo_trampoline_addr(SB)/8, $proc_pidinfo_trampoline<>(SB) ================================================ FILE: core/Clash.Meta/component/memory/memory_falllback.go ================================================ //go:build !darwin && !linux && !freebsd && !openbsd && !windows package memory func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { return nil, ErrNotImplementedError } ================================================ FILE: core/Clash.Meta/component/memory/memory_freebsd.go ================================================ package memory import ( "bytes" "encoding/binary" "errors" "unsafe" "golang.org/x/sys/unix" ) const ( CTLKern = 1 KernProc = 14 KernProcPID = 1 ) func CallSyscall(mib []int32) ([]byte, uint64, error) { mibptr := unsafe.Pointer(&mib[0]) miblen := uint64(len(mib)) // get required buffer size length := uint64(0) _, _, err := unix.Syscall6( unix.SYS___SYSCTL, uintptr(mibptr), uintptr(miblen), 0, uintptr(unsafe.Pointer(&length)), 0, 0) if err != 0 { var b []byte return b, length, err } if length == 0 { var b []byte return b, length, err } // get proc info itself buf := make([]byte, length) _, _, err = unix.Syscall6( unix.SYS___SYSCTL, uintptr(mibptr), uintptr(miblen), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&length)), 0, 0) if err != 0 { return buf, length, err } return buf, length, nil } func parseKinfoProc(buf []byte) (KinfoProc, error) { var k KinfoProc br := bytes.NewReader(buf) err := binary.Read(br, binary.LittleEndian, &k) return k, err } func getKProc(pid int32) (*KinfoProc, error) { mib := []int32{CTLKern, KernProc, KernProcPID, pid} buf, length, err := CallSyscall(mib) if err != nil { return nil, err } if length != sizeOfKinfoProc { return nil, errors.New("unexpected size of KinfoProc") } k, err := parseKinfoProc(buf) if err != nil { return nil, err } return &k, nil } func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { k, err := getKProc(pid) if err != nil { return nil, err } v, err := unix.Sysctl("vm.stats.vm.v_page_size") if err != nil { return nil, err } pageSize := binary.LittleEndian.Uint16([]byte(v)) return &MemoryInfoStat{ RSS: uint64(k.Rssize) * uint64(pageSize), VMS: uint64(k.Size), }, nil } ================================================ FILE: core/Clash.Meta/component/memory/memory_freebsd_386.go ================================================ package memory const sizeOfKinfoProc = 0x300 type Timeval struct { Sec int32 Usec int32 } type Rusage struct { Utime Timeval Stime Timeval Maxrss int32 Ixrss int32 Idrss int32 Isrss int32 Minflt int32 Majflt int32 Nswap int32 Inblock int32 Oublock int32 Msgsnd int32 Msgrcv int32 Nsignals int32 Nvcsw int32 Nivcsw int32 } type KinfoProc struct { Structsize int32 Layout int32 Args int32 /* pargs */ Paddr int32 /* proc */ Addr int32 /* user */ Tracep int32 /* vnode */ Textvp int32 /* vnode */ Fd int32 /* filedesc */ Vmspace int32 /* vmspace */ Wchan int32 Pid int32 Ppid int32 Pgid int32 Tpgid int32 Sid int32 Tsid int32 Jobc int16 Spare_short1 int16 Tdev uint32 Siglist [16]byte /* sigset */ Sigmask [16]byte /* sigset */ Sigignore [16]byte /* sigset */ Sigcatch [16]byte /* sigset */ Uid uint32 Ruid uint32 Svuid uint32 Rgid uint32 Svgid uint32 Ngroups int16 Spare_short2 int16 Groups [16]uint32 Size uint32 Rssize int32 Swrss int32 Tsize int32 Dsize int32 Ssize int32 Xstat uint16 Acflag uint16 Pctcpu uint32 Estcpu uint32 Slptime uint32 Swtime uint32 Cow uint32 Runtime uint64 Start Timeval Childtime Timeval Flag int32 Kiflag int32 Traceflag int32 Stat int8 Nice int8 Lock int8 Rqindex int8 Oncpu uint8 Lastcpu uint8 Tdname [17]int8 Wmesg [9]int8 Login [18]int8 Lockname [9]int8 Comm [20]int8 Emul [17]int8 Loginclass [18]int8 Sparestrings [50]int8 Spareints [7]int32 Flag2 int32 Fibnum int32 Cr_flags uint32 Jid int32 Numthreads int32 Tid int32 Pri Priority Rusage Rusage Rusage_ch Rusage Pcb int32 /* pcb */ Kstack int32 Udata int32 Tdaddr int32 /* thread */ Spareptrs [6]int32 Sparelongs [12]int32 Sflag int32 Tdflags int32 } type Priority struct { Class uint8 Level uint8 Native uint8 User uint8 } ================================================ FILE: core/Clash.Meta/component/memory/memory_freebsd_amd64.go ================================================ package memory const sizeOfKinfoProc = 0x440 type Timeval struct { Sec int64 Usec int64 } type Rusage struct { Utime Timeval Stime Timeval Maxrss int64 Ixrss int64 Idrss int64 Isrss int64 Minflt int64 Majflt int64 Nswap int64 Inblock int64 Oublock int64 Msgsnd int64 Msgrcv int64 Nsignals int64 Nvcsw int64 Nivcsw int64 } type KinfoProc struct { Structsize int32 Layout int32 Args int64 /* pargs */ Paddr int64 /* proc */ Addr int64 /* user */ Tracep int64 /* vnode */ Textvp int64 /* vnode */ Fd int64 /* filedesc */ Vmspace int64 /* vmspace */ Wchan int64 Pid int32 Ppid int32 Pgid int32 Tpgid int32 Sid int32 Tsid int32 Jobc int16 Spare_short1 int16 Tdev_freebsd11 uint32 Siglist [16]byte /* sigset */ Sigmask [16]byte /* sigset */ Sigignore [16]byte /* sigset */ Sigcatch [16]byte /* sigset */ Uid uint32 Ruid uint32 Svuid uint32 Rgid uint32 Svgid uint32 Ngroups int16 Spare_short2 int16 Groups [16]uint32 Size uint64 Rssize int64 Swrss int64 Tsize int64 Dsize int64 Ssize int64 Xstat uint16 Acflag uint16 Pctcpu uint32 Estcpu uint32 Slptime uint32 Swtime uint32 Cow uint32 Runtime uint64 Start Timeval Childtime Timeval Flag int64 Kiflag int64 Traceflag int32 Stat int8 Nice int8 Lock int8 Rqindex int8 Oncpu_old uint8 Lastcpu_old uint8 Tdname [17]int8 Wmesg [9]int8 Login [18]int8 Lockname [9]int8 Comm [20]int8 Emul [17]int8 Loginclass [18]int8 Moretdname [4]int8 Sparestrings [46]int8 Spareints [2]int32 Tdev uint64 Oncpu int32 Lastcpu int32 Tracer int32 Flag2 int32 Fibnum int32 Cr_flags uint32 Jid int32 Numthreads int32 Tid int32 Pri Priority Rusage Rusage Rusage_ch Rusage Pcb int64 /* pcb */ Kstack int64 Udata int64 Tdaddr int64 /* thread */ Pd int64 /* pwddesc, not accurate */ Spareptrs [5]int64 Sparelongs [12]int64 Sflag int64 Tdflags int64 } type Priority struct { Class uint8 Level uint8 Native uint8 User uint8 } ================================================ FILE: core/Clash.Meta/component/memory/memory_freebsd_arm.go ================================================ package memory const sizeOfKinfoProc = 0x440 type Timeval struct { Sec int64 Usec int64 } type Rusage struct { Utime Timeval Stime Timeval Maxrss int32 Ixrss int32 Idrss int32 Isrss int32 Minflt int32 Majflt int32 Nswap int32 Inblock int32 Oublock int32 Msgsnd int32 Msgrcv int32 Nsignals int32 Nvcsw int32 Nivcsw int32 } type KinfoProc struct { Structsize int32 Layout int32 Args int32 /* pargs */ Paddr int32 /* proc */ Addr int32 /* user */ Tracep int32 /* vnode */ Textvp int32 /* vnode */ Fd int32 /* filedesc */ Vmspace int32 /* vmspace */ Wchan int32 Pid int32 Ppid int32 Pgid int32 Tpgid int32 Sid int32 Tsid int32 Jobc int16 Spare_short1 int16 Tdev uint32 Siglist [16]byte /* sigset */ Sigmask [16]byte /* sigset */ Sigignore [16]byte /* sigset */ Sigcatch [16]byte /* sigset */ Uid uint32 Ruid uint32 Svuid uint32 Rgid uint32 Svgid uint32 Ngroups int16 Spare_short2 int16 Groups [16]uint32 Size uint32 Rssize int32 Swrss int32 Tsize int32 Dsize int32 Ssize int32 Xstat uint16 Acflag uint16 Pctcpu uint32 Estcpu uint32 Slptime uint32 Swtime uint32 Cow uint32 Runtime uint64 Start Timeval Childtime Timeval Flag int32 Kiflag int32 Traceflag int32 Stat int8 Nice int8 Lock int8 Rqindex int8 Oncpu uint8 Lastcpu uint8 Tdname [17]int8 Wmesg [9]int8 Login [18]int8 Lockname [9]int8 Comm [20]int8 Emul [17]int8 Loginclass [18]int8 Sparestrings [50]int8 Spareints [4]int32 Flag2 int32 Fibnum int32 Cr_flags uint32 Jid int32 Numthreads int32 Tid int32 Pri Priority Rusage Rusage Rusage_ch Rusage Pcb int32 /* pcb */ Kstack int32 Udata int32 Tdaddr int32 /* thread */ Spareptrs [6]int64 Sparelongs [12]int64 Sflag int64 Tdflags int64 } type Priority struct { Class uint8 Level uint8 Native uint8 User uint8 } ================================================ FILE: core/Clash.Meta/component/memory/memory_freebsd_arm64.go ================================================ package memory const sizeOfKinfoProc = 0x440 type Timeval struct { Sec int64 Usec int64 } type Rusage struct { Utime Timeval Stime Timeval Maxrss int64 Ixrss int64 Idrss int64 Isrss int64 Minflt int64 Majflt int64 Nswap int64 Inblock int64 Oublock int64 Msgsnd int64 Msgrcv int64 Nsignals int64 Nvcsw int64 Nivcsw int64 } type KinfoProc struct { Structsize int32 Layout int32 Args int64 /* pargs */ Paddr int64 /* proc */ Addr int64 /* user */ Tracep int64 /* vnode */ Textvp int64 /* vnode */ Fd int64 /* filedesc */ Vmspace int64 /* vmspace */ Wchan int64 Pid int32 Ppid int32 Pgid int32 Tpgid int32 Sid int32 Tsid int32 Jobc int16 Spare_short1 int16 Tdev_freebsd11 uint32 Siglist [16]byte /* sigset */ Sigmask [16]byte /* sigset */ Sigignore [16]byte /* sigset */ Sigcatch [16]byte /* sigset */ Uid uint32 Ruid uint32 Svuid uint32 Rgid uint32 Svgid uint32 Ngroups int16 Spare_short2 int16 Groups [16]uint32 Size uint64 Rssize int64 Swrss int64 Tsize int64 Dsize int64 Ssize int64 Xstat uint16 Acflag uint16 Pctcpu uint32 Estcpu uint32 Slptime uint32 Swtime uint32 Cow uint32 Runtime uint64 Start Timeval Childtime Timeval Flag int64 Kiflag int64 Traceflag int32 Stat uint8 Nice int8 Lock uint8 Rqindex uint8 Oncpu_old uint8 Lastcpu_old uint8 Tdname [17]uint8 Wmesg [9]uint8 Login [18]uint8 Lockname [9]uint8 Comm [20]int8 // changed from uint8 by hand Emul [17]uint8 Loginclass [18]uint8 Moretdname [4]uint8 Sparestrings [46]uint8 Spareints [2]int32 Tdev uint64 Oncpu int32 Lastcpu int32 Tracer int32 Flag2 int32 Fibnum int32 Cr_flags uint32 Jid int32 Numthreads int32 Tid int32 Pri Priority Rusage Rusage Rusage_ch Rusage Pcb int64 /* pcb */ Kstack int64 Udata int64 Tdaddr int64 /* thread */ Pd int64 /* pwddesc, not accurate */ Spareptrs [5]int64 Sparelongs [12]int64 Sflag int64 Tdflags int64 } type Priority struct { Class uint8 Level uint8 Native uint8 User uint8 } ================================================ FILE: core/Clash.Meta/component/memory/memory_linux.go ================================================ package memory import ( "os" "path/filepath" "strconv" "strings" ) var pageSize = uint64(os.Getpagesize()) func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { proc := os.Getenv("HOST_PROC") if proc == "" { proc = "/proc" } memPath := filepath.Join(proc, strconv.Itoa(int(pid)), "statm") contents, err := os.ReadFile(memPath) if err != nil { return nil, err } fields := strings.Split(string(contents), " ") vms, err := strconv.ParseUint(fields[0], 10, 64) if err != nil { return nil, err } rss, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { return nil, err } memInfo := &MemoryInfoStat{ RSS: rss * pageSize, VMS: vms * pageSize, } return memInfo, nil } ================================================ FILE: core/Clash.Meta/component/memory/memory_openbsd.go ================================================ package memory import ( "bytes" "encoding/binary" "errors" "unsafe" "golang.org/x/sys/unix" ) const ( CTLKern = 1 KernProc = 14 KernProcPID = 1 ) func callKernProcSyscall(op int32, arg int32) ([]byte, uint64, error) { mib := []int32{CTLKern, KernProc, op, arg, sizeOfKinfoProc, 0} mibptr := unsafe.Pointer(&mib[0]) miblen := uint64(len(mib)) length := uint64(0) _, _, err := unix.Syscall6( unix.SYS___SYSCTL, uintptr(mibptr), uintptr(miblen), 0, uintptr(unsafe.Pointer(&length)), 0, 0) if err != 0 { return nil, length, err } count := int32(length / uint64(sizeOfKinfoProc)) mib = []int32{CTLKern, KernProc, op, arg, sizeOfKinfoProc, count} mibptr = unsafe.Pointer(&mib[0]) miblen = uint64(len(mib)) // get proc info itself buf := make([]byte, length) _, _, err = unix.Syscall6( unix.SYS___SYSCTL, uintptr(mibptr), uintptr(miblen), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&length)), 0, 0) if err != 0 { return buf, length, err } return buf, length, nil } func parseKinfoProc(buf []byte) (KinfoProc, error) { var k KinfoProc br := bytes.NewReader(buf) err := binary.Read(br, binary.LittleEndian, &k) return k, err } func getKProc(pid int32) (*KinfoProc, error) { buf, length, err := callKernProcSyscall(KernProcPID, pid) if err != nil { return nil, err } if length != sizeOfKinfoProc { return nil, errors.New("unexpected size of KinfoProc") } k, err := parseKinfoProc(buf) if err != nil { return nil, err } return &k, nil } func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { k, err := getKProc(pid) if err != nil { return nil, err } uvmexp, err := unix.SysctlUvmexp("vm.uvmexp") if err != nil { return nil, err } pageSize := uint64(uvmexp.Pagesize) return &MemoryInfoStat{ RSS: uint64(k.Vm_rssize) * pageSize, VMS: uint64(k.Vm_tsize) + uint64(k.Vm_dsize) + uint64(k.Vm_ssize), }, nil } ================================================ FILE: core/Clash.Meta/component/memory/memory_openbsd_386.go ================================================ package memory const sizeOfKinfoProc = 0x264 type KinfoProc struct { Forw uint64 Back uint64 Paddr uint64 Addr uint64 Fd uint64 Stats uint64 Limit uint64 Vmspace uint64 Sigacts uint64 Sess uint64 Tsess uint64 Ru uint64 Eflag int32 Exitsig int32 Flag int32 Pid int32 Ppid int32 Sid int32 X_pgid int32 Tpgid int32 Uid uint32 Ruid uint32 Gid uint32 Rgid uint32 Groups [16]uint32 Ngroups int16 Jobc int16 Tdev uint32 Estcpu uint32 Rtime_sec uint32 Rtime_usec uint32 Cpticks int32 Pctcpu uint32 Swtime uint32 Slptime uint32 Schedflags int32 Uticks uint64 Sticks uint64 Iticks uint64 Tracep uint64 Traceflag int32 Holdcnt int32 Siglist int32 Sigmask uint32 Sigignore uint32 Sigcatch uint32 Stat int8 Priority uint8 Usrpri uint8 Nice uint8 Xstat uint16 Acflag uint16 Comm [24]int8 Wmesg [8]int8 Wchan uint64 Login [32]int8 Vm_rssize int32 Vm_tsize int32 Vm_dsize int32 Vm_ssize int32 Uvalid int64 Ustart_sec uint64 Ustart_usec uint32 Uutime_sec uint32 Uutime_usec uint32 Ustime_sec uint32 Ustime_usec uint32 Uru_maxrss uint64 Uru_ixrss uint64 Uru_idrss uint64 Uru_isrss uint64 Uru_minflt uint64 Uru_majflt uint64 Uru_nswap uint64 Uru_inblock uint64 Uru_oublock uint64 Uru_msgsnd uint64 Uru_msgrcv uint64 Uru_nsignals uint64 Uru_nvcsw uint64 Uru_nivcsw uint64 Uctime_sec uint32 Uctime_usec uint32 Psflags int32 Spare int32 Svuid uint32 Svgid uint32 Emul [8]int8 Rlim_rss_cur uint64 Cpuid uint64 Vm_map_size uint64 Tid int32 Rtableid uint32 } ================================================ FILE: core/Clash.Meta/component/memory/memory_openbsd_amd64.go ================================================ package memory const sizeOfKinfoProc = 0x268 type KinfoProc struct { Forw uint64 Back uint64 Paddr uint64 Addr uint64 Fd uint64 Stats uint64 Limit uint64 Vmspace uint64 Sigacts uint64 Sess uint64 Tsess uint64 Ru uint64 Eflag int32 Exitsig int32 Flag int32 Pid int32 Ppid int32 Sid int32 X_pgid int32 Tpgid int32 Uid uint32 Ruid uint32 Gid uint32 Rgid uint32 Groups [16]uint32 Ngroups int16 Jobc int16 Tdev uint32 Estcpu uint32 Rtime_sec uint32 Rtime_usec uint32 Cpticks int32 Pctcpu uint32 Swtime uint32 Slptime uint32 Schedflags int32 Uticks uint64 Sticks uint64 Iticks uint64 Tracep uint64 Traceflag int32 Holdcnt int32 Siglist int32 Sigmask uint32 Sigignore uint32 Sigcatch uint32 Stat int8 Priority uint8 Usrpri uint8 Nice uint8 Xstat uint16 Acflag uint16 Comm [24]int8 Wmesg [8]int8 Wchan uint64 Login [32]int8 Vm_rssize int32 Vm_tsize int32 Vm_dsize int32 Vm_ssize int32 Uvalid int64 Ustart_sec uint64 Ustart_usec uint32 Uutime_sec uint32 Uutime_usec uint32 Ustime_sec uint32 Ustime_usec uint32 Pad_cgo_0 [4]byte Uru_maxrss uint64 Uru_ixrss uint64 Uru_idrss uint64 Uru_isrss uint64 Uru_minflt uint64 Uru_majflt uint64 Uru_nswap uint64 Uru_inblock uint64 Uru_oublock uint64 Uru_msgsnd uint64 Uru_msgrcv uint64 Uru_nsignals uint64 Uru_nvcsw uint64 Uru_nivcsw uint64 Uctime_sec uint32 Uctime_usec uint32 Psflags int32 Spare int32 Svuid uint32 Svgid uint32 Emul [8]int8 Rlim_rss_cur uint64 Cpuid uint64 Vm_map_size uint64 Tid int32 Rtableid uint32 } ================================================ FILE: core/Clash.Meta/component/memory/memory_openbsd_arm.go ================================================ package memory const sizeOfKinfoProc = 0x264 type KinfoProc struct { Forw uint64 Back uint64 Paddr uint64 Addr uint64 Fd uint64 Stats uint64 Limit uint64 Vmspace uint64 Sigacts uint64 Sess uint64 Tsess uint64 Ru uint64 Eflag int32 Exitsig int32 Flag int32 Pid int32 Ppid int32 Sid int32 X_pgid int32 Tpgid int32 Uid uint32 Ruid uint32 Gid uint32 Rgid uint32 Groups [16]uint32 Ngroups int16 Jobc int16 Tdev uint32 Estcpu uint32 Rtime_sec uint32 Rtime_usec uint32 Cpticks int32 Pctcpu uint32 Swtime uint32 Slptime uint32 Schedflags int32 Uticks uint64 Sticks uint64 Iticks uint64 Tracep uint64 Traceflag int32 Holdcnt int32 Siglist int32 Sigmask uint32 Sigignore uint32 Sigcatch uint32 Stat int8 Priority uint8 Usrpri uint8 Nice uint8 Xstat uint16 Acflag uint16 Comm [24]int8 Wmesg [8]int8 Wchan uint64 Login [32]int8 Vm_rssize int32 Vm_tsize int32 Vm_dsize int32 Vm_ssize int32 Uvalid int64 Ustart_sec uint64 Ustart_usec uint32 Uutime_sec uint32 Uutime_usec uint32 Ustime_sec uint32 Ustime_usec uint32 Uru_maxrss uint64 Uru_ixrss uint64 Uru_idrss uint64 Uru_isrss uint64 Uru_minflt uint64 Uru_majflt uint64 Uru_nswap uint64 Uru_inblock uint64 Uru_oublock uint64 Uru_msgsnd uint64 Uru_msgrcv uint64 Uru_nsignals uint64 Uru_nvcsw uint64 Uru_nivcsw uint64 Uctime_sec uint32 Uctime_usec uint32 Psflags int32 Spare int32 Svuid uint32 Svgid uint32 Emul [8]int8 Rlim_rss_cur uint64 Cpuid uint64 Vm_map_size uint64 Tid int32 Rtableid uint32 } ================================================ FILE: core/Clash.Meta/component/memory/memory_openbsd_arm64.go ================================================ package memory const sizeOfKinfoProc = 0x270 type KinfoProc struct { Forw uint64 Back uint64 Paddr uint64 Addr uint64 Fd uint64 Stats uint64 Limit uint64 Vmspace uint64 Sigacts uint64 Sess uint64 Tsess uint64 Ru uint64 Eflag int32 Exitsig int32 Flag int32 Pid int32 Ppid int32 Sid int32 X_pgid int32 Tpgid int32 Uid uint32 Ruid uint32 Gid uint32 Rgid uint32 Groups [16]uint32 Ngroups int16 Jobc int16 Tdev uint32 Estcpu uint32 Rtime_sec uint32 Rtime_usec uint32 Cpticks int32 Pctcpu uint32 Swtime uint32 Slptime uint32 Schedflags int32 Uticks uint64 Sticks uint64 Iticks uint64 Tracep uint64 Traceflag int32 Holdcnt int32 Siglist int32 Sigmask uint32 Sigignore uint32 Sigcatch uint32 Stat int8 Priority uint8 Usrpri uint8 Nice uint8 Xstat uint16 Acflag uint16 Comm [24]int8 Wmesg [8]uint8 Wchan uint64 Login [32]uint8 Vm_rssize int32 Vm_tsize int32 Vm_dsize int32 Vm_ssize int32 Uvalid int64 Ustart_sec uint64 Ustart_usec uint32 Uutime_sec uint32 Uutime_usec uint32 Ustime_sec uint32 Ustime_usec uint32 Uru_maxrss uint64 Uru_ixrss uint64 Uru_idrss uint64 Uru_isrss uint64 Uru_minflt uint64 Uru_majflt uint64 Uru_nswap uint64 Uru_inblock uint64 Uru_oublock uint64 Uru_msgsnd uint64 Uru_msgrcv uint64 Uru_nsignals uint64 Uru_nvcsw uint64 Uru_nivcsw uint64 Uctime_sec uint32 Uctime_usec uint32 Psflags uint32 Spare int32 Svuid uint32 Svgid uint32 Emul [8]uint8 Rlim_rss_cur uint64 Cpuid uint64 Vm_map_size uint64 Tid int32 Rtableid uint32 Pledge uint64 } ================================================ FILE: core/Clash.Meta/component/memory/memory_openbsd_riscv64.go ================================================ package memory const sizeOfKinfoProc = 0x288 type KinfoProc struct { Forw uint64 Back uint64 Paddr uint64 Addr uint64 Fd uint64 Stats uint64 Limit uint64 Vmspace uint64 Sigacts uint64 Sess uint64 Tsess uint64 Ru uint64 Eflag int32 Exitsig int32 Flag int32 Pid int32 Ppid int32 Sid int32 X_pgid int32 Tpgid int32 Uid uint32 Ruid uint32 Gid uint32 Rgid uint32 Groups [16]uint32 Ngroups int16 Jobc int16 Tdev uint32 Estcpu uint32 Rtime_sec uint32 Rtime_usec uint32 Cpticks int32 Pctcpu uint32 Swtime uint32 Slptime uint32 Schedflags int32 Uticks uint64 Sticks uint64 Iticks uint64 Tracep uint64 Traceflag int32 Holdcnt int32 Siglist int32 Sigmask uint32 Sigignore uint32 Sigcatch uint32 Stat int8 Priority uint8 Usrpri uint8 Nice uint8 Xstat uint16 Spare uint16 Comm [24]int8 Wmesg [8]uint8 Wchan uint64 Login [32]uint8 Vm_rssize int32 Vm_tsize int32 Vm_dsize int32 Vm_ssize int32 Uvalid int64 Ustart_sec uint64 Ustart_usec uint32 Uutime_sec uint32 Uutime_usec uint32 Ustime_sec uint32 Ustime_usec uint32 Uru_maxrss uint64 Uru_ixrss uint64 Uru_idrss uint64 Uru_isrss uint64 Uru_minflt uint64 Uru_majflt uint64 Uru_nswap uint64 Uru_inblock uint64 Uru_oublock uint64 Uru_msgsnd uint64 Uru_msgrcv uint64 Uru_nsignals uint64 Uru_nvcsw uint64 Uru_nivcsw uint64 Uctime_sec uint32 Uctime_usec uint32 Psflags uint32 Acflag uint32 Svuid uint32 Svgid uint32 Emul [8]uint8 Rlim_rss_cur uint64 Cpuid uint64 Vm_map_size uint64 Tid int32 Rtableid uint32 Pledge uint64 Name [24]uint8 } ================================================ FILE: core/Clash.Meta/component/memory/memory_test.go ================================================ package memory import ( "errors" "os" "testing" "github.com/stretchr/testify/require" ) func TestMemoryInfo(t *testing.T) { v, err := GetMemoryInfo(int32(os.Getpid())) if errors.Is(err, ErrNotImplementedError) { t.Skip("not implemented") } require.NoErrorf(t, err, "getting memory info error %v", err) empty := MemoryInfoStat{} if v == nil || *v == empty { t.Errorf("could not get memory info %v", v) } else { t.Logf("memory info {RSS:%s, VMS:%s}", PrettyByteSize(v.RSS), PrettyByteSize(v.VMS)) } } ================================================ FILE: core/Clash.Meta/component/memory/memory_windows.go ================================================ package memory import ( "syscall" "unsafe" "golang.org/x/sys/windows" ) var ( modpsapi = windows.NewLazySystemDLL("psapi.dll") procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo") ) const processQueryInformation = windows.PROCESS_QUERY_LIMITED_INFORMATION type PROCESS_MEMORY_COUNTERS struct { CB uint32 PageFaultCount uint32 PeakWorkingSetSize uint64 WorkingSetSize uint64 QuotaPeakPagedPoolUsage uint64 QuotaPagedPoolUsage uint64 QuotaPeakNonPagedPoolUsage uint64 QuotaNonPagedPoolUsage uint64 PagefileUsage uint64 PeakPagefileUsage uint64 } func getProcessMemoryInfo(h windows.Handle, mem *PROCESS_MEMORY_COUNTERS) (err error) { r1, _, e1 := syscall.Syscall(procGetProcessMemoryInfo.Addr(), 3, uintptr(h), uintptr(unsafe.Pointer(mem)), uintptr(unsafe.Sizeof(*mem))) if r1 == 0 { if e1 != 0 { err = error(e1) } else { err = syscall.EINVAL } } return } func getMemoryInfo(pid int32) (PROCESS_MEMORY_COUNTERS, error) { var mem PROCESS_MEMORY_COUNTERS c, err := windows.OpenProcess(processQueryInformation, false, uint32(pid)) if err != nil { return mem, err } defer windows.CloseHandle(c) if err := getProcessMemoryInfo(c, &mem); err != nil { return mem, err } return mem, err } func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { mem, err := getMemoryInfo(pid) if err != nil { return nil, err } ret := &MemoryInfoStat{ RSS: uint64(mem.WorkingSetSize), VMS: uint64(mem.PagefileUsage), } return ret, nil } ================================================ FILE: core/Clash.Meta/component/mmdb/mmdb.go ================================================ package mmdb import ( "sync" mihomoOnce "github.com/metacubex/mihomo/common/once" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/oschwald/maxminddb-golang" ) type databaseType = uint8 const ( typeMaxmind databaseType = iota typeSing typeMetaV0 ) var ( ipReader IPReader asnReader ASNReader ipOnce sync.Once asnOnce sync.Once ) func LoadFromBytes(buffer []byte) { ipOnce.Do(func() { mmdb, err := maxminddb.FromBytes(buffer) if err != nil { log.Fatalln("Can't load mmdb: %s", err.Error()) } ipReader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": ipReader.databaseType = typeSing case "Meta-geoip0": ipReader.databaseType = typeMetaV0 default: ipReader.databaseType = typeMaxmind } }) } func Verify(path string) bool { instance, err := maxminddb.Open(path) if err == nil { instance.Close() } return err == nil } func IPInstance() IPReader { ipOnce.Do(func() { mmdbPath := C.Path.MMDB() log.Infoln("Load MMDB file: %s", mmdbPath) mmdb, err := maxminddb.Open(mmdbPath) if err != nil { log.Fatalln("Can't load MMDB: %s", err.Error()) } ipReader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": ipReader.databaseType = typeSing case "Meta-geoip0": ipReader.databaseType = typeMetaV0 default: ipReader.databaseType = typeMaxmind } }) return ipReader } func ASNInstance() ASNReader { asnOnce.Do(func() { ASNPath := C.Path.ASN() log.Infoln("Load ASN file: %s", ASNPath) asn, err := maxminddb.Open(ASNPath) if err != nil { log.Fatalln("Can't load ASN: %s", err.Error()) } asnReader = ASNReader{Reader: asn} }) return asnReader } func ReloadIP() { mihomoOnce.Reset(&ipOnce) } func ReloadASN() { mihomoOnce.Reset(&asnOnce) } ================================================ FILE: core/Clash.Meta/component/mmdb/reader.go ================================================ package mmdb import ( "fmt" "net" "strings" "github.com/metacubex/mihomo/log" "github.com/oschwald/maxminddb-golang" ) type geoip2Country struct { Country struct { IsoCode string `maxminddb:"iso_code"` } `maxminddb:"country"` } type IPReader struct { *maxminddb.Reader databaseType } type ASNReader struct { *maxminddb.Reader } type GeoLite2 struct { AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` } type IPInfo struct { ASN string `maxminddb:"asn"` Name string `maxminddb:"name"` } func (r IPReader) LookupCode(ipAddress net.IP) []string { switch r.databaseType { case typeMaxmind: var country geoip2Country _ = r.Lookup(ipAddress, &country) if country.Country.IsoCode == "" { return []string{} } return []string{strings.ToLower(country.Country.IsoCode)} case typeSing: var code string _ = r.Lookup(ipAddress, &code) if code == "" { return []string{} } return []string{code} case typeMetaV0: var record any _ = r.Lookup(ipAddress, &record) switch record := record.(type) { case string: return []string{record} case []any: // lookup returned type of slice is []any result := make([]string, 0, len(record)) for _, item := range record { result = append(result, item.(string)) } return result } return []string{} default: panic(fmt.Sprint("unknown geoip database type:", r.databaseType)) } } func (r ASNReader) LookupASN(ip net.IP) (string, string) { switch r.Metadata.DatabaseType { case "GeoLite2-ASN", "DBIP-ASN-Lite (compat=GeoLite2-ASN)": var result GeoLite2 _ = r.Lookup(ip, &result) return fmt.Sprint(result.AutonomousSystemNumber), result.AutonomousSystemOrganization case "ipinfo generic_asn_free.mmdb": var result IPInfo _ = r.Lookup(ip, &result) return result.ASN[2:], result.Name default: log.Warnln("Unsupported ASN type: %s", r.Metadata.DatabaseType) } return "", "" } ================================================ FILE: core/Clash.Meta/component/mptcp/mptcp_go120.go ================================================ //go:build !go1.21 package mptcp import ( "net" ) const MultipathTCPAvailable = false func SetNetDialer(dialer *net.Dialer, open bool) { } func GetNetDialer(dialer *net.Dialer) bool { return false } func SetNetListenConfig(listenConfig *net.ListenConfig, open bool) { } func GetNetListenConfig(listenConfig *net.ListenConfig) bool { return false } ================================================ FILE: core/Clash.Meta/component/mptcp/mptcp_go121.go ================================================ //go:build go1.21 package mptcp import "net" const MultipathTCPAvailable = true func SetNetDialer(dialer *net.Dialer, open bool) { dialer.SetMultipathTCP(open) } func GetNetDialer(dialer *net.Dialer) bool { return dialer.MultipathTCP() } func SetNetListenConfig(listenConfig *net.ListenConfig, open bool) { listenConfig.SetMultipathTCP(open) } func GetNetListenConfig(listenConfig *net.ListenConfig) bool { return listenConfig.MultipathTCP() } ================================================ FILE: core/Clash.Meta/component/nat/proxy.go ================================================ package nat import ( "net" "github.com/metacubex/mihomo/common/atomic" C "github.com/metacubex/mihomo/constant" ) type writeBackProxy struct { wb atomic.TypedValue[C.WriteBack] } func (w *writeBackProxy) WriteBack(b []byte, addr net.Addr) (n int, err error) { return w.wb.Load().WriteBack(b, addr) } func (w *writeBackProxy) UpdateWriteBack(wb C.WriteBack) { w.wb.Store(wb) } func NewWriteBackProxy(wb C.WriteBack) C.WriteBackProxy { w := &writeBackProxy{} w.UpdateWriteBack(wb) return w } ================================================ FILE: core/Clash.Meta/component/nat/table.go ================================================ package nat import ( "net" "sync" "github.com/metacubex/mihomo/common/xsync" C "github.com/metacubex/mihomo/constant" ) type Table struct { mapping xsync.Map[string, *entry] } type entry struct { PacketSender C.PacketSender LocalUDPConnMap xsync.Map[string, *net.UDPConn] LocalLockMap xsync.Map[string, *sync.Cond] } func (t *Table) GetOrCreate(key string, maker func() C.PacketSender) (C.PacketSender, bool) { item, loaded := t.mapping.LoadOrStoreFn(key, func() *entry { return &entry{ PacketSender: maker(), } }) return item.PacketSender, loaded } func (t *Table) Delete(key string) { t.mapping.Delete(key) } func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn { entry, exist := t.getEntry(lAddr) if !exist { return nil } item, exist := entry.LocalUDPConnMap.Load(rAddr) if !exist { return nil } return item } func (t *Table) AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool { entry, exist := t.getEntry(lAddr) if !exist { return false } entry.LocalUDPConnMap.Store(rAddr, conn) return true } func (t *Table) RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) { entry, exist := t.getEntry(lAddr) if !exist { return } entry.LocalUDPConnMap.Range(f) } func (t *Table) GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool) { entry, loaded := t.getEntry(lAddr) if !loaded { return nil, false } item, loaded := entry.LocalLockMap.LoadOrStoreFn(key, makeLock) return item, loaded } func (t *Table) DeleteForLocalConn(lAddr, key string) { entry, loaded := t.getEntry(lAddr) if !loaded { return } entry.LocalUDPConnMap.Delete(key) } func (t *Table) DeleteLockForLocalConn(lAddr, key string) { entry, loaded := t.getEntry(lAddr) if !loaded { return } entry.LocalLockMap.Delete(key) } func (t *Table) getEntry(key string) (*entry, bool) { return t.mapping.Load(key) } func makeLock() *sync.Cond { return sync.NewCond(&sync.Mutex{}) } // New return *Cache func New() *Table { return &Table{} } ================================================ FILE: core/Clash.Meta/component/pool/pool.go ================================================ package pool import ( "context" "runtime" "time" ) type Factory[T any] func(context.Context) (T, error) type entry[T any] struct { elm T time time.Time } type Option[T any] func(*pool[T]) // WithEvict set the evict callback func WithEvict[T any](cb func(T)) Option[T] { return func(p *pool[T]) { p.evict = cb } } // WithAge defined element max age (millisecond) func WithAge[T any](maxAge int64) Option[T] { return func(p *pool[T]) { p.maxAge = maxAge } } // WithSize defined max size of Pool func WithSize[T any](maxSize int) Option[T] { return func(p *pool[T]) { p.ch = make(chan *entry[T], maxSize) } } // Pool is for GC, see New for detail type Pool[T any] struct { *pool[T] } type pool[T any] struct { ch chan *entry[T] factory Factory[T] evict func(T) maxAge int64 } func (p *pool[T]) GetContext(ctx context.Context) (T, error) { now := time.Now() for { select { case item := <-p.ch: elm := item if p.maxAge != 0 && now.Sub(item.time).Milliseconds() > p.maxAge { if p.evict != nil { p.evict(elm.elm) } continue } return elm.elm, nil default: return p.factory(ctx) } } } func (p *pool[T]) Get() (T, error) { return p.GetContext(context.Background()) } func (p *pool[T]) Put(item T) { e := &entry[T]{ elm: item, time: time.Now(), } select { case p.ch <- e: return default: // pool is full if p.evict != nil { p.evict(item) } return } } func recycle[T any](p *Pool[T]) { for item := range p.pool.ch { if p.pool.evict != nil { p.pool.evict(item.elm) } } } func New[T any](factory Factory[T], options ...Option[T]) *Pool[T] { p := &pool[T]{ ch: make(chan *entry[T], 10), factory: factory, } for _, option := range options { option(p) } P := &Pool[T]{p} runtime.SetFinalizer(P, recycle[T]) return P } ================================================ FILE: core/Clash.Meta/component/pool/pool_test.go ================================================ package pool import ( "context" "testing" "time" "github.com/stretchr/testify/assert" ) func lg() Factory[int] { initial := -1 return func(context.Context) (int, error) { initial++ return initial, nil } } func TestPool_Basic(t *testing.T) { g := lg() pool := New[int](g) elm, _ := pool.Get() assert.Equal(t, 0, elm) pool.Put(elm) elm, _ = pool.Get() assert.Equal(t, 0, elm) elm, _ = pool.Get() assert.Equal(t, 1, elm) } func TestPool_MaxSize(t *testing.T) { g := lg() size := 5 pool := New[int](g, WithSize[int](size)) var items []int for i := 0; i < size; i++ { item, _ := pool.Get() items = append(items, item) } extra, _ := pool.Get() assert.Equal(t, size, extra) for _, item := range items { pool.Put(item) } pool.Put(extra) for _, item := range items { elm, _ := pool.Get() assert.Equal(t, item, elm) } } func TestPool_MaxAge(t *testing.T) { g := lg() pool := New[int](g, WithAge[int](20)) elm, _ := pool.Get() pool.Put(elm) elm, _ = pool.Get() assert.Equal(t, 0, elm) pool.Put(elm) time.Sleep(time.Millisecond * 22) elm, _ = pool.Get() assert.Equal(t, 1, elm) } ================================================ FILE: core/Clash.Meta/component/power/event.go ================================================ package power type Type uint8 const ( SUSPEND Type = iota RESUME RESUMEAUTOMATIC // Because the user is not present, most applications should do nothing. ) func (t Type) String() string { switch t { case SUSPEND: return "SUSPEND" case RESUME: return "RESUME" case RESUMEAUTOMATIC: return "RESUMEAUTOMATIC" default: return "" } } ================================================ FILE: core/Clash.Meta/component/power/event_other.go ================================================ //go:build !windows package power import "errors" func NewEventListener(cb func(Type)) (func(), error) { return nil, errors.New("not support on this platform") } ================================================ FILE: core/Clash.Meta/component/power/event_windows.go ================================================ package power // modify from https://github.com/golang/go/blob/b634f6fdcbebee23b7da709a243f3db217b64776/src/runtime/os_windows.go#L257 import ( "runtime" "unsafe" "golang.org/x/sys/windows" ) var ( libPowrProf = windows.NewLazySystemDLL("powrprof.dll") powerRegisterSuspendResumeNotification = libPowrProf.NewProc("PowerRegisterSuspendResumeNotification") powerUnregisterSuspendResumeNotification = libPowrProf.NewProc("PowerUnregisterSuspendResumeNotification") ) func NewEventListener(cb func(Type)) (func(), error) { if err := powerRegisterSuspendResumeNotification.Find(); err != nil { return nil, err // Running on Windows 7, where we don't need it anyway. } if err := powerUnregisterSuspendResumeNotification.Find(); err != nil { return nil, err // Running on Windows 7, where we don't need it anyway. } // Defines the type of event const ( PBT_APMSUSPEND uint32 = 4 PBT_APMRESUMESUSPEND uint32 = 7 PBT_APMRESUMEAUTOMATIC uint32 = 18 ) const ( _DEVICE_NOTIFY_CALLBACK = 2 ) type _DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS struct { callback uintptr context uintptr } var fn interface{} = func(context uintptr, changeType uint32, setting uintptr) uintptr { switch changeType { case PBT_APMSUSPEND: cb(SUSPEND) case PBT_APMRESUMESUSPEND: cb(RESUME) case PBT_APMRESUMEAUTOMATIC: cb(RESUMEAUTOMATIC) } return 0 } params := _DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS{ callback: windows.NewCallback(fn), } handle := uintptr(0) // DWORD PowerRegisterSuspendResumeNotification( // [in] DWORD Flags, // [in] HANDLE Recipient, // [out] PHPOWERNOTIFY RegistrationHandle //); _, _, err := powerRegisterSuspendResumeNotification.Call( _DEVICE_NOTIFY_CALLBACK, uintptr(unsafe.Pointer(¶ms)), uintptr(unsafe.Pointer(&handle)), ) if err != nil { return nil, err } return func() { // DWORD PowerUnregisterSuspendResumeNotification( // [in, out] HPOWERNOTIFY RegistrationHandle //); _, _, _ = powerUnregisterSuspendResumeNotification.Call( handle, ) runtime.KeepAlive(params) runtime.KeepAlive(handle) }, nil } ================================================ FILE: core/Clash.Meta/component/process/find_process_mode.go ================================================ package process import ( "errors" "strings" ) const ( FindProcessStrict FindProcessMode = iota FindProcessAlways FindProcessOff ) var ( validModes = map[string]FindProcessMode{ FindProcessStrict.String(): FindProcessStrict, FindProcessAlways.String(): FindProcessAlways, FindProcessOff.String(): FindProcessOff, } ) type FindProcessMode int32 // UnmarshalText unserialize FindProcessMode func (m *FindProcessMode) UnmarshalText(data []byte) error { return m.Set(string(data)) } func (m *FindProcessMode) Set(value string) error { mode, exist := validModes[strings.ToLower(value)] if !exist { return errors.New("invalid find process mode") } *m = mode return nil } // MarshalText serialize FindProcessMode func (m FindProcessMode) MarshalText() ([]byte, error) { return []byte(m.String()), nil } func (m FindProcessMode) String() string { switch m { case FindProcessAlways: return "always" case FindProcessOff: return "off" default: return "strict" } } ================================================ FILE: core/Clash.Meta/component/process/process.go ================================================ package process import ( "errors" "net/netip" C "github.com/metacubex/mihomo/constant" ) var ( ErrInvalidNetwork = errors.New("invalid network") ErrPlatformNotSupport = errors.New("not support on this platform") ErrNotFound = errors.New("process not found") ) const ( TCP = "tcp" UDP = "udp" ) func FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, string, error) { return findProcessName(network, srcIP, srcPort) } // PackageNameResolver // never change type traits because it's used in CMFA type PackageNameResolver func(metadata *C.Metadata) (string, error) // DefaultPackageNameResolver // never change type traits because it's used in CMFA var DefaultPackageNameResolver PackageNameResolver func FindPackageName(metadata *C.Metadata) (string, error) { if resolver := DefaultPackageNameResolver; resolver != nil { return resolver(metadata) } return "", ErrPlatformNotSupport } ================================================ FILE: core/Clash.Meta/component/process/process_darwin.go ================================================ package process import ( "encoding/binary" "net/netip" "strconv" "strings" "syscall" "unsafe" "golang.org/x/sys/unix" ) const ( procpidpathinfo = 0xb procpidpathinfosize = 1024 proccallnumpidinfo = 0x2 ) 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) (uint32, string, error) { var spath string switch network { case TCP: spath = "net.inet.tcp.pcblist_n" case UDP: spath = "net.inet.udp.pcblist_n" default: return 0, "", ErrInvalidNetwork } isIPv4 := ip.Is4() value, err := unix.SysctlRaw(spath) if err != nil { return 0, "", err } buf := value itemSize := structSize if network == TCP { // 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 bool ) switch { case flag&0x1 > 0 && isIPv4: // ipv4 srcIP, _ = netip.AddrFromSlice(buf[inp+76 : inp+80]) srcIsIPv4 = true case flag&0x2 > 0 && !isIPv4: // ipv6 srcIP, _ = netip.AddrFromSlice(buf[inp+64 : inp+80]) default: continue } srcIP = srcIP.Unmap() if ip == srcIP { // xsocket_n.so_last_pid pid := readNativeUint32(buf[so+68 : so+72]) pp, err := getExecPathFromPID(pid) return 0, pp, err } // udp packet connection may be not equal with srcIP if network == UDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 { fallbackUDPProcess, _ = getExecPathFromPID(readNativeUint32(buf[so+68 : so+72])) } } if network == UDP && fallbackUDPProcess != "" { return 0, fallbackUDPProcess, nil } return 0, "", ErrNotFound } func getExecPathFromPID(pid uint32) (string, error) { 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: core/Clash.Meta/component/process/process_freebsd_amd64.go ================================================ package process import ( "encoding/binary" "fmt" "net/netip" "strconv" "strings" "sync" "syscall" "unsafe" "github.com/metacubex/mihomo/log" ) // store process name for when dealing with multiple PROCESS-NAME rules var ( defaultSearcher *searcher once sync.Once ) func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { once.Do(func() { if err := initSearcher(); err != nil { log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error()) log.Warnln("All PROCESS-NAME rules will be skipped") return } }) if defaultSearcher == nil { return 0, "", ErrPlatformNotSupport } var spath string isTCP := network == TCP switch network { case TCP: spath = "net.inet.tcp.pcblist" case UDP: spath = "net.inet.udp.pcblist" default: return 0, "", ErrInvalidNetwork } value, err := syscall.Sysctl(spath) if err != nil { return 0, "", err } buf := []byte(value) pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP) if err != nil { return 0, "", err } pp, err := getExecPathFromPID(pid) return 0, pp, err } func getExecPathFromPID(pid uint32) (string, error) { buf := make([]byte, 2048) size := uint64(len(buf)) // CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid mib := [4]uint32{1, 14, 12, pid} _, _, errno := syscall.Syscall6( syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)), 0, 0) if errno != 0 || size == 0 { return "", errno } return string(buf[:size-1]), nil } func readNativeUint32(b []byte) uint32 { return *(*uint32)(unsafe.Pointer(&b[0])) } type searcher struct { // sizeof(struct xinpgen) headSize int // sizeof(struct xtcpcb) tcpItemSize int // sizeof(struct xinpcb) udpItemSize int udpInpOffset int port int ip int vflag int socket int // sizeof(struct xfile) fileItemSize int data int pid int } func (s *searcher) Search(buf []byte, ip netip.Addr, port uint16, isTCP bool) (uint32, error) { var itemSize int var inpOffset int if isTCP { // struct xtcpcb itemSize = s.tcpItemSize inpOffset = 8 } else { // struct xinpcb itemSize = s.udpItemSize inpOffset = s.udpInpOffset } isIPv4 := ip.Is4() // skip the first xinpgen block for i := s.headSize; i+itemSize <= len(buf); i += itemSize { inp := i + inpOffset srcPort := binary.BigEndian.Uint16(buf[inp+s.port : inp+s.port+2]) if port != srcPort { continue } // xinpcb.inp_vflag flag := buf[inp+s.vflag] var srcIP netip.Addr switch { case flag&0x1 > 0 && isIPv4: // ipv4 srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip : inp+s.ip+4]) case flag&0x2 > 0 && !isIPv4: // ipv6 srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip-12 : inp+s.ip+4]) default: continue } srcIP = srcIP.Unmap() if ip != srcIP { continue } // xsocket.xso_so, interpreted as big endian anyway since it's only used for comparison socket := binary.BigEndian.Uint64(buf[inp+s.socket : inp+s.socket+8]) return s.searchSocketPid(socket) } return 0, ErrNotFound } func (s *searcher) searchSocketPid(socket uint64) (uint32, error) { value, err := syscall.Sysctl("kern.file") if err != nil { return 0, err } buf := []byte(value) // struct xfile itemSize := s.fileItemSize for i := 0; i+itemSize <= len(buf); i += itemSize { // xfile.xf_data data := binary.BigEndian.Uint64(buf[i+s.data : i+s.data+8]) if data == socket { // xfile.xf_pid pid := readNativeUint32(buf[i+s.pid : i+s.pid+4]) return pid, nil } } return 0, ErrNotFound } func newSearcher(major int) *searcher { var s *searcher switch major { case 11: s = &searcher{ headSize: 32, tcpItemSize: 1304, udpItemSize: 632, port: 198, ip: 228, vflag: 116, socket: 88, fileItemSize: 80, data: 56, pid: 8, udpInpOffset: 8, } case 12: fallthrough case 13: fallthrough case 14: fallthrough case 15: s = &searcher{ headSize: 64, tcpItemSize: 744, udpItemSize: 400, port: 254, ip: 284, vflag: 392, socket: 16, fileItemSize: 128, data: 56, pid: 8, } } return s } func initSearcher() error { osRelease, err := syscall.Sysctl("kern.osrelease") if err != nil { return err } dot := strings.Index(osRelease, ".") if dot != -1 { osRelease = osRelease[:dot] } major, err := strconv.Atoi(osRelease) if err != nil { return err } defaultSearcher = newSearcher(major) if defaultSearcher == nil { return fmt.Errorf("unsupported freebsd version %d", major) } return nil } ================================================ FILE: core/Clash.Meta/component/process/process_linux.go ================================================ package process import ( "bufio" "bytes" "encoding/binary" "encoding/hex" "fmt" "net/netip" "os" "path" "path/filepath" "runtime" "strconv" "strings" "syscall" "unicode" "unsafe" "github.com/mdlayher/netlink" "golang.org/x/sys/unix" ) const ( SOCK_DIAG_BY_FAMILY = 20 inetDiagRequestSize = int(unsafe.Sizeof(inetDiagRequest{})) inetDiagResponseSize = int(unsafe.Sizeof(inetDiagResponse{})) ) type inetDiagRequest struct { Family byte Protocol byte Ext byte Pad byte States uint32 SrcPort [2]byte DstPort [2]byte Src [16]byte Dst [16]byte If uint32 Cookie [2]uint32 } type inetDiagResponse struct { Family byte State byte Timer byte ReTrans byte SrcPort [2]byte DstPort [2]byte Src [16]byte Dst [16]byte If uint32 Cookie [2]uint32 Expires uint32 RQueue uint32 WQueue uint32 UID uint32 INode uint32 } func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { uid, inode, err := resolveSocketByNetlink(network, ip, srcPort) if runtime.GOOS == "android" { // on Android (especially recent releases), netlink INET_DIAG can fail or return UID 0 / empty process info for some apps // so trying fallback to resolve /proc/net/{tcp,tcp6,udp,udp6} if err != nil { uid, inode, err = resolveSocketByProcFS(network, ip, srcPort) } else if uid == 0 { pUID, pInode, pErr := resolveSocketByProcFS(network, ip, srcPort) if pErr == nil && pUID != 0 { uid, inode, err = pUID, pInode, nil } } } if err != nil { return 0, "", err } pp, err := resolveProcessNameByProcSearch(inode, uid) if runtime.GOOS == "android" { // if inode-based /proc//fd resolution fails but UID is known, // fall back to resolving the process/package name by UID (typical on Android where all app processes share one UID). if err != nil && uid != 0 { pp, err = resolveProcessNameByUID(uid) } } return uid, pp, err } func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { request := &inetDiagRequest{ States: 0xffffffff, Cookie: [2]uint32{0xffffffff, 0xffffffff}, } if ip.Is4() { request.Family = unix.AF_INET } else { request.Family = unix.AF_INET6 } if strings.HasPrefix(network, "tcp") { request.Protocol = unix.IPPROTO_TCP } else if strings.HasPrefix(network, "udp") { request.Protocol = unix.IPPROTO_UDP } else { return 0, 0, ErrInvalidNetwork } copy(request.Src[:], ip.AsSlice()) binary.BigEndian.PutUint16(request.SrcPort[:], uint16(srcPort)) conn, err := netlink.Dial(unix.NETLINK_INET_DIAG, nil) if err != nil { return 0, 0, err } defer conn.Close() message := netlink.Message{ Header: netlink.Header{ Type: SOCK_DIAG_BY_FAMILY, Flags: netlink.Request | netlink.Dump, }, Data: (*(*[inetDiagRequestSize]byte)(unsafe.Pointer(request)))[:], } messages, err := conn.Execute(message) if err != nil { return 0, 0, err } for _, msg := range messages { if len(msg.Data) < inetDiagResponseSize { continue } response := (*inetDiagResponse)(unsafe.Pointer(&msg.Data[0])) return response.UID, response.INode, nil } return 0, 0, ErrNotFound } func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { files, err := os.ReadDir("/proc") if err != nil { return "", err } buffer := make([]byte, unix.PathMax) socket := fmt.Appendf(nil, "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 := filepath.Join("/proc", f.Name()) fdPath := filepath.Join(processPath, "fd") fds, err := os.ReadDir(fdPath) if err != nil { continue } for _, fd := range fds { n, err := unix.Readlink(filepath.Join(fdPath, fd.Name()), buffer) if err != nil { continue } if runtime.GOOS == "android" { if bytes.Equal(buffer[:n], socket) { cmdline, err := os.ReadFile(path.Join(processPath, "cmdline")) if err != nil { return "", err } return splitCmdline(cmdline), nil } } else { if bytes.Equal(buffer[:n], socket) { return os.Readlink(filepath.Join(processPath, "exe")) } } } } return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode) } // resolveProcessNameByUID returns a process name for any process with uid. // On Android all processes of one app share the same UID; used when inode // lookup fails (socket closed / TIME_WAIT). func resolveProcessNameByUID(uid uint32) (string, error) { files, err := os.ReadDir("/proc") if err != nil { return "", err } for _, f := range files { if !f.IsDir() || !isPid(f.Name()) { continue } info, err := f.Info() if err != nil { continue } if info.Sys().(*syscall.Stat_t).Uid != uid { continue } processPath := filepath.Join("/proc", f.Name()) if runtime.GOOS == "android" { cmdline, err := os.ReadFile(path.Join(processPath, "cmdline")) if err != nil { continue } if name := splitCmdline(cmdline); name != "" { return name, nil } } else { if exe, err := os.Readlink(filepath.Join(processPath, "exe")); err == nil { return exe, nil } } } return "", fmt.Errorf("no process found with uid %d", uid) } func splitCmdline(cmdline []byte) string { cmdline = bytes.Trim(cmdline, " ") idx := bytes.IndexFunc(cmdline, func(r rune) bool { return unicode.IsControl(r) || unicode.IsSpace(r) }) if idx == -1 { return filepath.Base(string(cmdline)) } return filepath.Base(string(cmdline[:idx])) } func isPid(s string) bool { return strings.IndexFunc(s, func(r rune) bool { return !unicode.IsDigit(r) }) == -1 } // resolveSocketByProcFS finds UID and inode from /proc/net/{tcp,tcp6,udp,udp6}. // In TUN mode metadata sourceIP is often the gateway (e.g. fake-ip range), not // the socket's real local address; we match by local port first and prefer // exact IP+port when it matches. func resolveSocketByProcFS(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { var proto string switch { case strings.HasPrefix(network, "tcp"): proto = "tcp" case strings.HasPrefix(network, "udp"): proto = "udp" default: return 0, 0, ErrInvalidNetwork } targetPort := uint16(srcPort) unmapped := ip.Unmap() files := []string{"/proc/net/" + proto, "/proc/net/" + proto + "6"} var bestUID, bestInode uint32 found := false for _, path := range files { isV6 := strings.HasSuffix(path, "6") var matchIP netip.Addr if unmapped.Is4() { if isV6 { matchIP = netip.AddrFrom16(unmapped.As16()) } else { matchIP = unmapped } } else { if !isV6 { continue } matchIP = unmapped } uid, inode, exact, err := searchProcNetFileByPort(path, matchIP, targetPort) if err != nil { continue } if exact { return uid, inode, nil } if !found || (bestUID == 0 && uid != 0) { bestUID = uid bestInode = inode found = true } } if found { return bestUID, bestInode, nil } return 0, 0, ErrNotFound } // searchProcNetFileByPort scans /proc/net/* for local_address matching targetPort. // Exact IP+port wins; else port-only (skips inode==0 entries used by TIME_WAIT). func searchProcNetFileByPort(path string, targetIP netip.Addr, targetPort uint16) (uid, inode uint32, exact bool, err error) { f, err := os.Open(path) if err != nil { return 0, 0, false, err } defer f.Close() isV6 := strings.HasSuffix(path, "6") scanner := bufio.NewScanner(f) // skip header scanner.Scan() for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) < 10 { continue } localAddr := fields[1] parts := strings.Split(localAddr, ":") if len(parts) != 2 { continue } portHex := parts[1] port, err := strconv.ParseUint(portHex, 16, 16) if err != nil || uint16(port) != targetPort { continue } inodeStr := fields[9] if inodeStr == "0" { continue // TIME_WAIT entries have inode 0 } inode64, err := strconv.ParseUint(inodeStr, 10, 32) if err != nil { continue } uid64, _ := strconv.ParseUint(fields[7], 10, 32) addrHex := parts[0] if isV6 { addrBytes, err := hex.DecodeString(addrHex) if err != nil || len(addrBytes) != 16 { continue } // IPv6 addresses in /proc/net/tcp6 are in network byte order (big-endian) var addr [16]byte copy(addr[:], addrBytes) parsedIP := netip.AddrFrom16(addr) if parsedIP == targetIP { return uint32(uid64), uint32(inode64), true, nil } } else { addrBytes, err := hex.DecodeString(addrHex) if err != nil || len(addrBytes) != 4 { continue } // IPv4 addresses in /proc/net/tcp are in little-endian order parsedIP := netip.AddrFrom4([4]byte{addrBytes[3], addrBytes[2], addrBytes[1], addrBytes[0]}) if parsedIP == targetIP { return uint32(uid64), uint32(inode64), true, nil } } // port matched but IP didn't - save as best effort if !exact { uid = uint32(uid64) inode = uint32(inode64) exact = false } } return uid, inode, exact, scanner.Err() } ================================================ FILE: core/Clash.Meta/component/process/process_other.go ================================================ //go:build !darwin && !linux && !windows && (!freebsd || !amd64) package process import "net/netip" func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { return 0, "", ErrPlatformNotSupport } func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { return 0, 0, ErrPlatformNotSupport } ================================================ FILE: core/Clash.Meta/component/process/process_windows.go ================================================ package process import ( "fmt" "net/netip" "sync" "syscall" "unsafe" "github.com/metacubex/mihomo/log" "golang.org/x/sys/windows" ) const ( tcpTableFunc = "GetExtendedTcpTable" tcpTablePidConn = 4 udpTableFunc = "GetExtendedUdpTable" udpTablePid = 1 queryProcNameFunc = "QueryFullProcessImageNameW" ) var ( getExTCPTable uintptr getExUDPTable uintptr queryProcName uintptr once sync.Once ) func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { return 0, 0, ErrPlatformNotSupport } func initWin32API() error { h, err := windows.LoadLibrary("iphlpapi.dll") if err != nil { return fmt.Errorf("LoadLibrary iphlpapi.dll failed: %s", err.Error()) } getExTCPTable, err = windows.GetProcAddress(h, tcpTableFunc) if err != nil { return fmt.Errorf("GetProcAddress of %s failed: %s", tcpTableFunc, err.Error()) } getExUDPTable, err = windows.GetProcAddress(h, udpTableFunc) if err != nil { return fmt.Errorf("GetProcAddress of %s failed: %s", udpTableFunc, err.Error()) } h, err = windows.LoadLibrary("kernel32.dll") if err != nil { return fmt.Errorf("LoadLibrary kernel32.dll failed: %s", err.Error()) } queryProcName, err = windows.GetProcAddress(h, queryProcNameFunc) if err != nil { return fmt.Errorf("GetProcAddress of %s failed: %s", queryProcNameFunc, err.Error()) } return nil } func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { once.Do(func() { err := initWin32API() if err != nil { log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error()) log.Warnln("All PROCESS-NAMES rules will be skipped") return } }) family := windows.AF_INET if ip.Is6() { family = windows.AF_INET6 } var class int var fn uintptr switch network { case TCP: fn = getExTCPTable class = tcpTablePidConn case UDP: fn = getExUDPTable class = udpTablePid default: return 0, "", ErrInvalidNetwork } buf, err := getTransportTable(fn, family, class) if err != nil { return 0, "", err } s := newSearcher(family == windows.AF_INET, network == TCP) pid, err := s.Search(buf, ip, uint16(srcPort)) if err != nil { return 0, "", err } pp, err := getExecPathFromPID(pid) return 0, pp, err } type searcher struct { itemSize int port int ip int ipSize int pid int tcpState int } func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) { n := int(readNativeUint32(b[:4])) itemSize := s.itemSize for i := 0; i < n; i++ { row := b[4+itemSize*i : 4+itemSize*(i+1)] if s.tcpState >= 0 { tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4]) // MIB_TCP_STATE_ESTAB, only check established connections for TCP if tcpState != 5 { continue } } // according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian. // this field can be illustrated as follows depends on different machine endianess: // little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB) // big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB) // so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32 srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4]))) if srcPort != port { continue } srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize]) srcIP = srcIP.Unmap() // windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) { continue } pid := readNativeUint32(row[s.pid : s.pid+4]) return pid, nil } return 0, ErrNotFound } func newSearcher(isV4, isTCP bool) *searcher { var itemSize, port, ip, ipSize, pid int tcpState := -1 switch { case isV4 && isTCP: // struct MIB_TCPROW_OWNER_PID itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0 case isV4 && !isTCP: // struct MIB_UDPROW_OWNER_PID itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8 case !isV4 && isTCP: // struct MIB_TCP6ROW_OWNER_PID itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48 case !isV4 && !isTCP: // struct MIB_UDP6ROW_OWNER_PID itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24 } return &searcher{ itemSize: itemSize, port: port, ip: ip, ipSize: ipSize, pid: pid, tcpState: tcpState, } } func getTransportTable(fn uintptr, family int, class int) ([]byte, error) { for size, buf := uint32(8), make([]byte, 8); ; { ptr := unsafe.Pointer(&buf[0]) err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) switch err { case 0: return buf, nil case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER): buf = make([]byte, size) default: return nil, fmt.Errorf("syscall error: %d", err) } } } func readNativeUint32(b []byte) uint32 { return *(*uint32)(unsafe.Pointer(&b[0])) } func getExecPathFromPID(pid uint32) (string, error) { // kernel process starts with a colon in order to distinguish with normal processes switch pid { case 0: // reserved pid for system idle process return ":System Idle Process", nil case 4: // reserved pid for windows kernel image return ":System", nil } h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) if err != nil { return "", err } defer windows.CloseHandle(h) buf := make([]uint16, syscall.MAX_LONG_PATH) size := uint32(len(buf)) r1, _, err := syscall.Syscall6( queryProcName, 4, uintptr(h), uintptr(0), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)), 0, 0) if r1 == 0 { return "", err } return syscall.UTF16ToString(buf[:size]), nil } ================================================ FILE: core/Clash.Meta/component/profile/cachefile/cache.go ================================================ package cachefile import ( "os" "sync" "time" "github.com/metacubex/mihomo/component/profile" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/bbolt" ) var ( initOnce sync.Once fileMode os.FileMode = 0o666 defaultCache *CacheFile bucketSelected = []byte("selected") bucketFakeip = []byte("fakeip") bucketFakeip6 = []byte("fakeip6") bucketETag = []byte("etag") bucketSubscriptionInfo = []byte("subscriptioninfo") bucketStorage = []byte("storage") ) // CacheFile store and update the cache file type CacheFile struct { DB *bbolt.DB } func (c *CacheFile) SetSelected(group, selected string) { if !profile.StoreSelected.Load() { return } else if c.DB == nil { return } err := c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(bucketSelected) if err != nil { return err } return bucket.Put([]byte(group), []byte(selected)) }) if err != nil { log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) return } } func (c *CacheFile) SelectedMap() map[string]string { if !profile.StoreSelected.Load() { return nil } else if c.DB == nil { return nil } mapping := map[string]string{} c.DB.View(func(t *bbolt.Tx) error { bucket := t.Bucket(bucketSelected) if bucket == nil { return nil } c := bucket.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { mapping[string(k)] = string(v) } return nil }) return mapping } func (c *CacheFile) Close() error { return c.DB.Close() } func initCache() { options := bbolt.Options{Timeout: time.Second} db, err := bbolt.Open(C.Path.Cache(), fileMode, &options) switch err { case bbolt.ErrInvalid, bbolt.ErrChecksum, bbolt.ErrVersionMismatch: if err = os.Remove(C.Path.Cache()); err != nil { log.Warnln("[CacheFile] remove invalid cache file error: %s", err.Error()) break } log.Infoln("[CacheFile] remove invalid cache file and create new one") db, err = bbolt.Open(C.Path.Cache(), fileMode, &options) } if err != nil { log.Warnln("[CacheFile] can't open cache file: %s", err.Error()) } defaultCache = &CacheFile{ DB: db, } } // Cache return singleton of CacheFile func Cache() *CacheFile { initOnce.Do(initCache) return defaultCache } ================================================ FILE: core/Clash.Meta/component/profile/cachefile/etag.go ================================================ package cachefile import ( "time" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/log" "github.com/metacubex/bbolt" "github.com/vmihailenco/msgpack/v5" ) type EtagWithHash struct { Hash utils.HashType ETag string Time time.Time } func (c *CacheFile) SetETagWithHash(url string, etagWithHash EtagWithHash) { if c.DB == nil { return } data, err := msgpack.Marshal(etagWithHash) if err != nil { return // maybe panic is better } err = c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(bucketETag) if err != nil { return err } return bucket.Put([]byte(url), data) }) if err != nil { log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) return } } func (c *CacheFile) GetETagWithHash(key string) (etagWithHash EtagWithHash) { if c.DB == nil { return } c.DB.View(func(t *bbolt.Tx) error { if bucket := t.Bucket(bucketETag); bucket != nil { if v := bucket.Get([]byte(key)); v != nil { if err := msgpack.Unmarshal(v, &etagWithHash); err != nil { return err } } } return nil }) return } ================================================ FILE: core/Clash.Meta/component/profile/cachefile/fakeip.go ================================================ package cachefile import ( "net/netip" "github.com/metacubex/mihomo/log" "github.com/metacubex/bbolt" ) type FakeIpStore struct { *CacheFile bucketName []byte } func (c *CacheFile) FakeIpStore() *FakeIpStore { return &FakeIpStore{c, bucketFakeip} } func (c *CacheFile) FakeIpStore6() *FakeIpStore { return &FakeIpStore{c, bucketFakeip6} } func (c *FakeIpStore) GetByHost(host string) (ip netip.Addr, exist bool) { if c.DB == nil { return } c.DB.View(func(t *bbolt.Tx) error { if bucket := t.Bucket(c.bucketName); bucket != nil { if v := bucket.Get([]byte(host)); v != nil { ip, exist = netip.AddrFromSlice(v) } } return nil }) return } func (c *FakeIpStore) PutByHost(host string, ip netip.Addr) { if c.DB == nil { return } err := c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(c.bucketName) if err != nil { return err } return bucket.Put([]byte(host), ip.AsSlice()) }) if err != nil { log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) } } func (c *FakeIpStore) GetByIP(ip netip.Addr) (host string, exist bool) { if c.DB == nil { return } c.DB.View(func(t *bbolt.Tx) error { if bucket := t.Bucket(c.bucketName); bucket != nil { if v := bucket.Get(ip.AsSlice()); v != nil { host, exist = string(v), true } } return nil }) return } func (c *FakeIpStore) PutByIP(ip netip.Addr, host string) { if c.DB == nil { return } err := c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(c.bucketName) if err != nil { return err } return bucket.Put(ip.AsSlice(), []byte(host)) }) if err != nil { log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) } } func (c *FakeIpStore) DelByIP(ip netip.Addr) { if c.DB == nil { return } addr := ip.AsSlice() err := c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(c.bucketName) if err != nil { return err } host := bucket.Get(addr) err = bucket.Delete(addr) if len(host) > 0 { if err = bucket.Delete(host); err != nil { return err } } return err }) if err != nil { log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) } } func (c *FakeIpStore) FlushFakeIP() error { err := c.DB.Batch(func(t *bbolt.Tx) error { bucket := t.Bucket(c.bucketName) if bucket == nil { return nil } return t.DeleteBucket(c.bucketName) }) return err } ================================================ FILE: core/Clash.Meta/component/profile/cachefile/storage.go ================================================ package cachefile import ( "sort" "time" "github.com/metacubex/mihomo/log" "github.com/metacubex/bbolt" "github.com/vmihailenco/msgpack/v5" ) const storageSizeLimit = 1024 * 1024 const storageKeySizeLimit = 64 const maxStorageEntries = storageSizeLimit / storageKeySizeLimit type StorageData struct { Data []byte Time time.Time } func decodeStorageData(v []byte) (StorageData, error) { var storage StorageData if err := msgpack.Unmarshal(v, &storage); err != nil { return StorageData{}, err } return storage, nil } func (c *CacheFile) GetStorage(key string) []byte { if c.DB == nil { return nil } var data []byte decodeFailed := false err := c.DB.View(func(t *bbolt.Tx) error { if bucket := t.Bucket(bucketStorage); bucket != nil { if v := bucket.Get([]byte(key)); v != nil { storage, err := decodeStorageData(v) if err != nil { decodeFailed = true return err } data = storage.Data } } return nil }) if err != nil { log.Warnln("[CacheFile] read cache for key %s failed: %s", key, err.Error()) if decodeFailed { c.DeleteStorage(key) } return nil } return data } func (c *CacheFile) SetStorage(key string, data []byte) { if c.DB == nil { return } if len(key) > storageKeySizeLimit { log.Warnln("[CacheFile] skip storage for key %s: key exceeds %d bytes", key, storageKeySizeLimit) return } if len(data) > storageSizeLimit { log.Warnln("[CacheFile] skip storage for key %s: payload exceeds %d bytes", key, storageSizeLimit) return } keyBytes := []byte(key) payload, err := msgpack.Marshal(StorageData{ Data: data, Time: time.Now(), }) if err != nil { return } err = c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(bucketStorage) if err != nil { return err } type storageEntry struct { Key string Data StorageData } entries := make(map[string]StorageData) usedSize := 0 entryCount := 0 corruptedKeys := make([][]byte, 0) c := bucket.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { storage, err := decodeStorageData(v) if err != nil { log.Warnln("[CacheFile] drop corrupted storage entry %s: %s", string(k), err.Error()) corruptedKeys = append(corruptedKeys, append([]byte(nil), k...)) continue } entryKey := string(k) entries[entryKey] = storage if entryKey != key { usedSize += len(storage.Data) entryCount++ } } for _, k := range corruptedKeys { if err := bucket.Delete(k); err != nil { return err } } evictionQueue := make([]storageEntry, 0, len(entries)) for entryKey, storage := range entries { if entryKey == key { continue } evictionQueue = append(evictionQueue, storageEntry{ Key: entryKey, Data: storage, }) } sort.Slice(evictionQueue, func(i, j int) bool { left := evictionQueue[i] right := evictionQueue[j] if left.Data.Time.Equal(right.Data.Time) { return left.Key < right.Key } return left.Data.Time.Before(right.Data.Time) }) for _, entry := range evictionQueue { if usedSize+len(data) <= storageSizeLimit && entryCount < maxStorageEntries { break } if err := bucket.Delete([]byte(entry.Key)); err != nil { return err } log.Infoln("[CacheFile] evict storage entry %s to make room for %s", entry.Key, key) usedSize -= len(entry.Data.Data) entryCount-- } return bucket.Put(keyBytes, payload) }) if err != nil { log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) } } func (c *CacheFile) DeleteStorage(key string) { if c.DB == nil { return } err := c.DB.Batch(func(t *bbolt.Tx) error { bucket := t.Bucket(bucketStorage) if bucket == nil { return nil } return bucket.Delete([]byte(key)) }) if err != nil { log.Warnln("[CacheFile] delete cache from %s failed: %s", c.DB.Path(), err.Error()) } } ================================================ FILE: core/Clash.Meta/component/profile/cachefile/subscriptioninfo.go ================================================ package cachefile import ( "github.com/metacubex/mihomo/log" "github.com/metacubex/bbolt" ) func (c *CacheFile) SetSubscriptionInfo(name string, userInfo string) { if c.DB == nil { return } err := c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(bucketSubscriptionInfo) if err != nil { return err } return bucket.Put([]byte(name), []byte(userInfo)) }) if err != nil { log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) return } } func (c *CacheFile) GetSubscriptionInfo(name string) (userInfo string) { if c.DB == nil { return } c.DB.View(func(t *bbolt.Tx) error { if bucket := t.Bucket(bucketSubscriptionInfo); bucket != nil { if v := bucket.Get([]byte(name)); v != nil { userInfo = string(v) } } return nil }) return } ================================================ FILE: core/Clash.Meta/component/profile/profile.go ================================================ package profile import ( "github.com/metacubex/mihomo/common/atomic" ) // StoreSelected is a global switch for storing selected proxy to cache var StoreSelected = atomic.NewBool(true) ================================================ FILE: core/Clash.Meta/component/proxydialer/byname.go ================================================ package proxydialer import ( "context" "fmt" "net" "net/netip" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/tunnel" ) type byNameProxyDialer struct { proxyName string } func (d byNameProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { proxies := tunnel.Proxies() proxy, ok := proxies[d.proxyName] if !ok { return nil, fmt.Errorf("proxyName[%s] not found", d.proxyName) } return New(proxy, true).DialContext(ctx, network, address) } func (d byNameProxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { proxies := tunnel.Proxies() proxy, ok := proxies[d.proxyName] if !ok { return nil, fmt.Errorf("proxyName[%s] not found", d.proxyName) } return New(proxy, true).ListenPacket(ctx, network, address, rAddrPort) } func NewByName(proxyName string) C.Dialer { return byNameProxyDialer{proxyName: proxyName} } ================================================ FILE: core/Clash.Meta/component/proxydialer/proxydialer.go ================================================ package proxydialer import ( "context" "net" "net/netip" "strings" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/tunnel/statistic" ) type proxyDialer struct { proxy C.ProxyAdapter statistic bool } func New(proxy C.ProxyAdapter, statistic bool) C.Dialer { return proxyDialer{proxy: proxy, statistic: statistic} } func (p proxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { currentMeta := &C.Metadata{Type: C.INNER} if err := currentMeta.SetRemoteAddress(address); err != nil { return nil, err } if strings.Contains(network, "udp") { // using in wireguard outbound pc, err := p.listenPacket(ctx, currentMeta) if err != nil { return nil, err } if !currentMeta.Resolved() { // should not happen, maybe by a wrongly implemented proxy, but we can handle this (: err = pc.ResolveUDP(ctx, currentMeta) if err != nil { _ = pc.Close() return nil, err } } return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil } conn, err := p.proxy.DialContext(ctx, currentMeta) if err != nil { return nil, err } if p.statistic { conn = statistic.NewTCPTracker(conn, statistic.DefaultManager, currentMeta, nil, 0, 0, false) } return conn, err } func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { currentMeta := &C.Metadata{Type: C.INNER, DstIP: rAddrPort.Addr(), DstPort: rAddrPort.Port()} return p.listenPacket(ctx, currentMeta) } func (p proxyDialer) listenPacket(ctx context.Context, currentMeta *C.Metadata) (C.PacketConn, error) { currentMeta.NetWork = C.UDP pc, err := p.proxy.ListenPacketContext(ctx, currentMeta) if err != nil { return nil, err } if p.statistic { pc = statistic.NewUDPTracker(pc, statistic.DefaultManager, currentMeta, nil, 0, 0, false) } return pc, nil } ================================================ FILE: core/Clash.Meta/component/proxydialer/sing.go ================================================ package proxydialer import ( "context" "net" C "github.com/metacubex/mihomo/constant" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) type SingDialer interface { N.Dialer } type singDialer struct { cDialer C.Dialer } var _ N.Dialer = (*singDialer)(nil) func (d singDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return d.cDialer.DialContext(ctx, network, destination.String()) } func (d singDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return d.cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort()) } func NewSingDialer(cDialer C.Dialer) SingDialer { return singDialer{cDialer: cDialer} } ================================================ FILE: core/Clash.Meta/component/proxydialer/slowdown.go ================================================ package proxydialer import ( "context" "net" "net/netip" "github.com/metacubex/mihomo/component/slowdown" C "github.com/metacubex/mihomo/constant" ) type SlowDownDialer struct { C.Dialer Slowdown *slowdown.SlowDown } func (d SlowDownDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { return slowdown.Do(d.Slowdown, ctx, func() (net.Conn, error) { return d.Dialer.DialContext(ctx, network, address) }) } func (d SlowDownDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { return slowdown.Do(d.Slowdown, ctx, func() (net.PacketConn, error) { return d.Dialer.ListenPacket(ctx, network, address, rAddrPort) }) } func NewSlowDownDialer(d C.Dialer, sd *slowdown.SlowDown) SlowDownDialer { return SlowDownDialer{ Dialer: d, Slowdown: sd, } } ================================================ FILE: core/Clash.Meta/component/proxydialer/slowdown_sing.go ================================================ package proxydialer import ( "context" "net" "github.com/metacubex/mihomo/component/slowdown" M "github.com/metacubex/sing/common/metadata" ) type SlowDownSingDialer struct { SingDialer Slowdown *slowdown.SlowDown } func (d SlowDownSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return slowdown.Do(d.Slowdown, ctx, func() (net.Conn, error) { return d.SingDialer.DialContext(ctx, network, destination) }) } func (d SlowDownSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return slowdown.Do(d.Slowdown, ctx, func() (net.PacketConn, error) { return d.SingDialer.ListenPacket(ctx, destination) }) } func NewSlowDownSingDialer(d SingDialer, sd *slowdown.SlowDown) SlowDownSingDialer { return SlowDownSingDialer{ SingDialer: d, Slowdown: sd, } } ================================================ FILE: core/Clash.Meta/component/resolver/enhancer.go ================================================ package resolver import "net/netip" var DefaultHostMapper Enhancer type Enhancer interface { FakeIPEnabled() bool MappingEnabled() bool IsFakeIP(netip.Addr) bool IsFakeBroadcastIP(netip.Addr) bool IsExistFakeIP(netip.Addr) bool FindHostByIP(netip.Addr) (string, bool) FlushFakeIP() error InsertHostByIP(netip.Addr, string) StoreFakePoolState() } func FakeIPEnabled() bool { if mapper := DefaultHostMapper; mapper != nil { return mapper.FakeIPEnabled() } return false } func MappingEnabled() bool { if mapper := DefaultHostMapper; mapper != nil { return mapper.MappingEnabled() } return false } func IsFakeIP(ip netip.Addr) bool { if mapper := DefaultHostMapper; mapper != nil { return mapper.IsFakeIP(ip) } return false } func IsFakeBroadcastIP(ip netip.Addr) bool { if mapper := DefaultHostMapper; mapper != nil { return mapper.IsFakeBroadcastIP(ip) } return false } func IsExistFakeIP(ip netip.Addr) bool { if mapper := DefaultHostMapper; mapper != nil { return mapper.IsExistFakeIP(ip) } return false } func InsertHostByIP(ip netip.Addr, host string) { if mapper := DefaultHostMapper; mapper != nil { mapper.InsertHostByIP(ip, host) } } func FindHostByIP(ip netip.Addr) (string, bool) { if mapper := DefaultHostMapper; mapper != nil { return mapper.FindHostByIP(ip) } return "", false } func FlushFakeIP() error { if mapper := DefaultHostMapper; mapper != nil { return mapper.FlushFakeIP() } return nil } func StoreFakePoolState() { if mapper := DefaultHostMapper; mapper != nil { mapper.StoreFakePoolState() } } ================================================ FILE: core/Clash.Meta/component/resolver/host.go ================================================ package resolver import ( "errors" "net/netip" "os" "strconv" "strings" _ "unsafe" "github.com/metacubex/mihomo/component/resolver/hosts" "github.com/metacubex/mihomo/component/trie" "github.com/metacubex/randv2" ) var ( DisableSystemHosts, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_HOSTS")) UseSystemHosts bool ) type Hosts struct { *trie.DomainTrie[HostValue] } func NewHosts(hosts *trie.DomainTrie[HostValue]) Hosts { return Hosts{ hosts, } } // Return the search result and whether to match the parameter `isDomain` func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) { if value := h.DomainTrie.Search(domain); value != nil { hostValue := value.Data() for { if isDomain && hostValue.IsDomain { return &hostValue, true } else { if node := h.DomainTrie.Search(hostValue.Domain); node != nil { hostValue = node.Data() } else { break } } } if isDomain == hostValue.IsDomain { return &hostValue, true } return &hostValue, false } if !isDomain && !DisableSystemHosts && UseSystemHosts { addr, _ := hosts.LookupStaticHost(domain) if hostValue, err := NewHostValue(addr); err == nil { return &hostValue, true } } return nil, false } type HostValue struct { IsDomain bool IPs []netip.Addr Domain string } func NewHostValue(value []string) (HostValue, error) { isDomain := true ips := make([]netip.Addr, 0, len(value)) domain := "" switch len(value) { case 0: return HostValue{}, errors.New("value is empty") case 1: host := value[0] if ip, err := netip.ParseAddr(host); err == nil { ips = append(ips, ip.Unmap()) isDomain = false } else { domain = host } default: // > 1 isDomain = false for _, str := range value { if ip, err := netip.ParseAddr(str); err == nil { ips = append(ips, ip.Unmap()) } else { return HostValue{}, err } } } if isDomain { return NewHostValueByDomain(domain) } return NewHostValueByIPs(ips) } func NewHostValueByIPs(ips []netip.Addr) (HostValue, error) { if len(ips) == 0 { return HostValue{}, errors.New("ip list is empty") } return HostValue{ IsDomain: false, IPs: ips, }, nil } func NewHostValueByDomain(domain string) (HostValue, error) { domain = strings.Trim(domain, ".") item := strings.Split(domain, ".") if len(item) < 2 { return HostValue{}, errors.New("invalid domain") } return HostValue{ IsDomain: true, Domain: domain, }, nil } func (hv HostValue) RandIP() (netip.Addr, error) { if hv.IsDomain { return netip.Addr{}, errors.New("value type is error") } return hv.IPs[randv2.IntN(len(hv.IPs))], nil } ================================================ FILE: core/Clash.Meta/component/resolver/hosts/hosts.go ================================================ package hosts // this file copy and modify from golang's std net/hosts.go import ( "errors" "io" "io/fs" "net/netip" "os" "strings" "sync" "time" ) var hostsFilePath = "/etc/hosts" const cacheMaxAge = 5 * time.Second func parseLiteralIP(addr string) string { ip, err := netip.ParseAddr(addr) if err != nil { return "" } return ip.String() } type byName struct { addrs []string canonicalName string } // hosts contains known host entries. var hosts struct { sync.Mutex // Key for the list of literal IP addresses must be a host // name. It would be part of DNS labels, a FQDN or an absolute // FQDN. // For now the key is converted to lower case for convenience. byName map[string]byName // Key for the list of host names must be a literal IP address // including IPv6 address with zone identifier. // We don't support old-classful IP address notation. byAddr map[string][]string expire time.Time path string mtime time.Time size int64 } func readHosts() { now := time.Now() hp := hostsFilePath if now.Before(hosts.expire) && hosts.path == hp && len(hosts.byName) > 0 { return } mtime, size, err := stat(hp) if err == nil && hosts.path == hp && hosts.mtime.Equal(mtime) && hosts.size == size { hosts.expire = now.Add(cacheMaxAge) return } hs := make(map[string]byName) is := make(map[string][]string) file, err := open(hp) if err != nil { if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrPermission) { return } } if file != nil { defer file.close() for line, ok := file.readLine(); ok; line, ok = file.readLine() { if i := strings.IndexByte(line, '#'); i >= 0 { // Discard comments. line = line[0:i] } f := getFields(line) if len(f) < 2 { continue } addr := parseLiteralIP(f[0]) if addr == "" { continue } var canonical string for i := 1; i < len(f); i++ { name := absDomainName(f[i]) h := []byte(f[i]) lowerASCIIBytes(h) key := absDomainName(string(h)) if i == 1 { canonical = key } is[addr] = append(is[addr], name) if v, ok := hs[key]; ok { hs[key] = byName{ addrs: append(v.addrs, addr), canonicalName: v.canonicalName, } continue } hs[key] = byName{ addrs: []string{addr}, canonicalName: canonical, } } } } // Update the data cache. hosts.expire = now.Add(cacheMaxAge) hosts.path = hp hosts.byName = hs hosts.byAddr = is hosts.mtime = mtime hosts.size = size } // LookupStaticHost looks up the addresses and the canonical name for the given host from /etc/hosts. func LookupStaticHost(host string) ([]string, string) { hosts.Lock() defer hosts.Unlock() readHosts() if len(hosts.byName) != 0 { if hasUpperCase(host) { lowerHost := []byte(host) lowerASCIIBytes(lowerHost) host = string(lowerHost) } if byName, ok := hosts.byName[absDomainName(host)]; ok { ipsCp := make([]string, len(byName.addrs)) copy(ipsCp, byName.addrs) return ipsCp, byName.canonicalName } } return nil, "" } // LookupStaticAddr looks up the hosts for the given address from /etc/hosts. func LookupStaticAddr(addr string) []string { hosts.Lock() defer hosts.Unlock() readHosts() addr = parseLiteralIP(addr) if addr == "" { return nil } if len(hosts.byAddr) != 0 { if hosts, ok := hosts.byAddr[addr]; ok { hostsCp := make([]string, len(hosts)) copy(hostsCp, hosts) return hostsCp } } return nil } func stat(name string) (mtime time.Time, size int64, err error) { st, err := os.Stat(name) if err != nil { return time.Time{}, 0, err } return st.ModTime(), st.Size(), nil } type file struct { file *os.File data []byte atEOF bool } func (f *file) close() { f.file.Close() } func (f *file) getLineFromData() (s string, ok bool) { data := f.data i := 0 for i = 0; i < len(data); i++ { if data[i] == '\n' { s = string(data[0:i]) ok = true // move data i++ n := len(data) - i copy(data[0:], data[i:]) f.data = data[0:n] return } } if f.atEOF && len(f.data) > 0 { // EOF, return all we have s = string(data) f.data = f.data[0:0] ok = true } return } func (f *file) readLine() (s string, ok bool) { if s, ok = f.getLineFromData(); ok { return } if len(f.data) < cap(f.data) { ln := len(f.data) n, err := io.ReadFull(f.file, f.data[ln:cap(f.data)]) if n >= 0 { f.data = f.data[0 : ln+n] } if err == io.EOF || err == io.ErrUnexpectedEOF { f.atEOF = true } } s, ok = f.getLineFromData() return } func (f *file) stat() (mtime time.Time, size int64, err error) { st, err := f.file.Stat() if err != nil { return time.Time{}, 0, err } return st.ModTime(), st.Size(), nil } func open(name string) (*file, error) { fd, err := os.Open(name) if err != nil { return nil, err } return &file{fd, make([]byte, 0, 64*1024), false}, nil } func getFields(s string) []string { return splitAtBytes(s, " \r\t\n") } // Count occurrences in s of any bytes in t. func countAnyByte(s string, t string) int { n := 0 for i := 0; i < len(s); i++ { if strings.IndexByte(t, s[i]) >= 0 { n++ } } return n } // Split s at any bytes in t. func splitAtBytes(s string, t string) []string { a := make([]string, 1+countAnyByte(s, t)) n := 0 last := 0 for i := 0; i < len(s); i++ { if strings.IndexByte(t, s[i]) >= 0 { if last < i { a[n] = s[last:i] n++ } last = i + 1 } } if last < len(s) { a[n] = s[last:] n++ } return a[0:n] } // lowerASCIIBytes makes x ASCII lowercase in-place. func lowerASCIIBytes(x []byte) { for i, b := range x { if 'A' <= b && b <= 'Z' { x[i] += 'a' - 'A' } } } // hasUpperCase tells whether the given string contains at least one upper-case. func hasUpperCase(s string) bool { for i := range s { if 'A' <= s[i] && s[i] <= 'Z' { return true } } return false } // absDomainName returns an absolute domain name which ends with a // trailing dot to match pure Go reverse resolver and all other lookup // routines. // See golang.org/issue/12189. // But we don't want to add dots for local names from /etc/hosts. // It's hard to tell so we settle on the heuristic that names without dots // (like "localhost" or "myhost") do not get trailing dots, but any other // names do. func absDomainName(s string) string { if strings.IndexByte(s, '.') != -1 && s[len(s)-1] != '.' { s += "." } return s } ================================================ FILE: core/Clash.Meta/component/resolver/hosts/hosts_windows.go ================================================ package hosts // this file copy and modify from golang's std net/hook_windows.go import ( "golang.org/x/sys/windows" ) func init() { if dir, err := windows.GetSystemDirectory(); err == nil { hostsFilePath = dir + "/Drivers/etc/hosts" } } ================================================ FILE: core/Clash.Meta/component/resolver/ip4p.go ================================================ package resolver import ( "net" "net/netip" "strconv" "github.com/metacubex/mihomo/log" ) var ( ip4PEnable bool ) func GetIP4PEnable() bool { return ip4PEnable } func SetIP4PEnable(enableIP4PConvert bool) { ip4PEnable = enableIP4PConvert } // kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go func LookupIP4P(addr netip.Addr, port string) (netip.Addr, string) { if ip4PEnable { ip := addr.AsSlice() if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && ip[3] == 0x00 { addr = netip.AddrFrom4([4]byte{ip[12], ip[13], ip[14], ip[15]}) port = strconv.Itoa(int(ip[10])<<8 + int(ip[11])) log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr.String(), port)) return addr, port } } return addr, port } ================================================ FILE: core/Clash.Meta/component/resolver/relay.go ================================================ package resolver import ( "context" "encoding/binary" "io" "net" "time" "github.com/metacubex/mihomo/common/pool" D "github.com/miekg/dns" ) const DefaultDnsReadTimeout = time.Second * 10 const DefaultDnsRelayTimeout = time.Second * 5 const SafeDnsPacketSize = 2 * 1024 // safe size which is 1232 from https://dnsflagday.net/2020/, so 2048 is enough func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration) error { buff := pool.Get(pool.UDPBufferSize) defer func() { _ = pool.Put(buff) _ = conn.Close() }() for { if readTimeout > 0 { _ = conn.SetReadDeadline(time.Now().Add(readTimeout)) } length := uint16(0) if err := binary.Read(conn, binary.BigEndian, &length); err != nil { break } if int(length) > len(buff) { break } n, err := io.ReadFull(conn, buff[:length]) if err != nil { break } err = func() error { ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) defer cancel() inData := buff[:n] outBuff := buff[2:] msg, err := relayDnsPacket(ctx, inData, outBuff, 0) if err != nil { return err } if &msg[0] == &outBuff[0] { // msg is still in the buff binary.BigEndian.PutUint16(buff[:2], uint16(len(msg))) outBuff = buff[:2+len(msg)] } else { // buff not big enough (WTF???) newBuff := pool.Get(len(msg) + 2) defer pool.Put(newBuff) binary.BigEndian.PutUint16(newBuff[:2], uint16(len(msg))) copy(newBuff[2:], msg) outBuff = newBuff } _, err = conn.Write(outBuff) if err != nil { return err } return nil }() if err != nil { return err } } return nil } func relayDnsPacket(ctx context.Context, payload []byte, target []byte, maxSize int) ([]byte, error) { msg := &D.Msg{} if err := msg.Unpack(payload); err != nil { return nil, err } r, err := ServeMsg(ctx, msg) if err != nil { m := new(D.Msg) m.SetRcode(msg, D.RcodeServerFailure) return m.PackBuffer(target) } r.SetRcode(msg, r.Rcode) if maxSize > 0 { r.Truncate(maxSize) } r.Compress = true return r.PackBuffer(target) } // RelayDnsPacket will truncate udp message up to SafeDnsPacketSize func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { return relayDnsPacket(ctx, payload, target, SafeDnsPacketSize) } ================================================ FILE: core/Clash.Meta/component/resolver/resolver.go ================================================ package resolver import ( "context" "errors" "fmt" "net/netip" "time" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/trie" "github.com/metacubex/randv2" "github.com/miekg/dns" ) var ( // DefaultResolver aim to resolve ip DefaultResolver Resolver // ProxyServerHostResolver resolve ip for proxies server host, only nil when DefaultResolver is nil ProxyServerHostResolver Resolver // DirectHostResolver resolve ip for direct outbound host, only nil when DefaultResolver is nil DirectHostResolver Resolver // SystemResolver always using system dns, and was init in dns module SystemResolver Resolver // DisableIPv6 means don't resolve ipv6 host // default value is true DisableIPv6 = true // DefaultHosts aim to resolve hosts DefaultHosts = NewHosts(trie.New[HostValue]()) // DefaultDNSTimeout defined the default dns request timeout DefaultDNSTimeout = time.Second * 5 ) var ( ErrIPNotFound = errors.New("couldn't find ip") ErrIPVersion = errors.New("ip version error") ErrIPv6Disabled = errors.New("ipv6 disabled") ) type Resolver interface { LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error) ResolveECH(ctx context.Context, host string) ([]byte, error) ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error) Invalid() bool ClearCache() ResetConnection() } // LookupIPv4WithResolver same as LookupIPv4, but with a resolver func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { if node, ok := DefaultHosts.Search(host, false); ok { if addrs := utils.Filter(node.IPs, func(ip netip.Addr) bool { return ip.Is4() }); len(addrs) > 0 { return addrs, nil } } ip, err := netip.ParseAddr(host) if err == nil { ip = ip.Unmap() if ip.Is4() { return []netip.Addr{ip}, nil } return []netip.Addr{}, ErrIPVersion } if r != nil && r.Invalid() { return r.LookupIPv4(ctx, host) } return SystemResolver.LookupIPv4(ctx, host) } // LookupIPv4 with a host, return ipv4 list func LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) { return LookupIPv4WithResolver(ctx, host, DefaultResolver) } // ResolveIPv4WithResolver same as ResolveIPv4, but with a resolver func ResolveIPv4WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { ips, err := LookupIPv4WithResolver(ctx, host, r) if err != nil { return netip.Addr{}, err } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } return ips[randv2.IntN(len(ips))], nil } // ResolveIPv4 with a host, return ipv4 func ResolveIPv4(ctx context.Context, host string) (netip.Addr, error) { return ResolveIPv4WithResolver(ctx, host, DefaultResolver) } // LookupIPv6WithResolver same as LookupIPv6, but with a resolver func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { if DisableIPv6 { return nil, ErrIPv6Disabled } if node, ok := DefaultHosts.Search(host, false); ok { if addrs := utils.Filter(node.IPs, func(ip netip.Addr) bool { return ip.Is6() }); len(addrs) > 0 { return addrs, nil } } if ip, err := netip.ParseAddr(host); err == nil { ip = ip.Unmap() if ip.Is6() { return []netip.Addr{ip}, nil } return nil, ErrIPVersion } if r != nil && r.Invalid() { return r.LookupIPv6(ctx, host) } return SystemResolver.LookupIPv6(ctx, host) } // LookupIPv6 with a host, return ipv6 list func LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) { return LookupIPv6WithResolver(ctx, host, DefaultResolver) } // ResolveIPv6WithResolver same as ResolveIPv6, but with a resolver func ResolveIPv6WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { ips, err := LookupIPv6WithResolver(ctx, host, r) if err != nil { return netip.Addr{}, err } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } return ips[randv2.IntN(len(ips))], nil } func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { return ResolveIPv6WithResolver(ctx, host, DefaultResolver) } // LookupIPWithResolver same as LookupIP, but with a resolver func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { if node, ok := DefaultHosts.Search(host, false); ok { return node.IPs, nil } if r != nil && r.Invalid() { if DisableIPv6 { return r.LookupIPv4(ctx, host) } return r.LookupIP(ctx, host) } else if DisableIPv6 { return LookupIPv4WithResolver(ctx, host, r) } if ip, err := netip.ParseAddr(host); err == nil { ip = ip.Unmap() return []netip.Addr{ip}, nil } return SystemResolver.LookupIP(ctx, host) } // LookupIP with a host, return ip func LookupIP(ctx context.Context, host string) ([]netip.Addr, error) { return LookupIPWithResolver(ctx, host, DefaultResolver) } // ResolveIPWithResolver same as ResolveIP, but with a resolver func ResolveIPWithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { ips, err := LookupIPWithResolver(ctx, host, r) if err != nil { return netip.Addr{}, err } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } ipv4s, ipv6s := SortationAddr(ips) if len(ipv4s) > 0 { return ipv4s[randv2.IntN(len(ipv4s))], nil } return ipv6s[randv2.IntN(len(ipv6s))], nil } // ResolveIP with a host, return ip and priority return TypeA func ResolveIP(ctx context.Context, host string) (netip.Addr, error) { return ResolveIPWithResolver(ctx, host, DefaultResolver) } // ResolveIPPrefer6WithResolver same as ResolveIP, but with a resolver func ResolveIPPrefer6WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { ips, err := LookupIPWithResolver(ctx, host, r) if err != nil { return netip.Addr{}, err } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } ipv4s, ipv6s := SortationAddr(ips) if len(ipv6s) > 0 { return ipv6s[randv2.IntN(len(ipv6s))], nil } return ipv4s[randv2.IntN(len(ipv4s))], nil } // ResolveIPPrefer6 with a host, return ip and priority return TypeAAAA func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) { return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver) } func ResolveECHWithResolver(ctx context.Context, host string, r Resolver) ([]byte, error) { if r != nil && r.Invalid() { return r.ResolveECH(ctx, host) } return SystemResolver.ResolveECH(ctx, host) } func ResolveECH(ctx context.Context, host string) ([]byte, error) { return ResolveECHWithResolver(ctx, host, DefaultResolver) } func ClearCache() { if DefaultResolver != nil { go DefaultResolver.ClearCache() } go SystemResolver.ClearCache() // SystemResolver unneeded check nil } func ResetConnection() { if DefaultResolver != nil { go DefaultResolver.ResetConnection() } go SystemResolver.ResetConnection() // SystemResolver unneeded check nil } func SortationAddr(ips []netip.Addr) (ipv4s, ipv6s []netip.Addr) { for _, v := range ips { if v.Unmap().Is4() { ipv4s = append(ipv4s, v) } else { ipv6s = append(ipv6s, v) } } return } ================================================ FILE: core/Clash.Meta/component/resolver/service.go ================================================ package resolver import ( "context" D "github.com/miekg/dns" ) var DefaultService Service type Service interface { ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) } // ServeMsg with a dns.Msg, return resolve dns.Msg func ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { if server := DefaultService; server != nil { return server.ServeMsg(ctx, msg) } return nil, ErrIPNotFound } ================================================ FILE: core/Clash.Meta/component/resolver/system.go ================================================ package resolver import "sync" var blacklist struct { Map map[string]struct{} Mutex sync.Mutex } func init() { blacklist.Map = make(map[string]struct{}) } func AddSystemDnsBlacklist(names ...string) { blacklist.Mutex.Lock() defer blacklist.Mutex.Unlock() for _, name := range names { blacklist.Map[name] = struct{}{} } } func RemoveSystemDnsBlacklist(names ...string) { blacklist.Mutex.Lock() defer blacklist.Mutex.Unlock() for _, name := range names { delete(blacklist.Map, name) } } func IsSystemDnsBlacklisted(names ...string) bool { blacklist.Mutex.Lock() defer blacklist.Mutex.Unlock() for _, name := range names { if _, ok := blacklist.Map[name]; ok { return true } } return false } ================================================ FILE: core/Clash.Meta/component/resource/fetcher.go ================================================ package resource import ( "context" "os" "sync" "time" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/slowdown" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" "github.com/metacubex/fswatch" "github.com/samber/lo" ) type Parser[V any] func([]byte) (V, error) type Fetcher[V any] struct { ctx context.Context ctxCancel context.CancelFunc resourceType string name string vehicle P.Vehicle updatedAt time.Time hash utils.HashType parser Parser[V] interval time.Duration onUpdate func(V) watcher *fswatch.Watcher loadBufMutex sync.Mutex backoff slowdown.Backoff } func (f *Fetcher[V]) Name() string { return f.name } func (f *Fetcher[V]) Vehicle() P.Vehicle { return f.vehicle } func (f *Fetcher[V]) VehicleType() P.VehicleType { return f.vehicle.Type() } func (f *Fetcher[V]) UpdatedAt() time.Time { return f.updatedAt } func (f *Fetcher[V]) Initial() (V, error) { if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil { // local file exists, use it first buf, err := os.ReadFile(f.vehicle.Path()) modTime := stat.ModTime() contents, _, err := f.loadBuf(buf, utils.MakeHash(buf), false) f.updatedAt = modTime // reset updatedAt to file's modTime if err == nil { err = f.startPullLoop(time.Since(modTime) > f.interval) if err != nil { return lo.Empty[V](), err } return contents, nil } } // parse local file error, fallback to remote contents, _, updateErr := f.Update() // start the pull loop even if f.Update() failed err := f.startPullLoop(false) if err != nil { return lo.Empty[V](), err } if updateErr != nil { return lo.Empty[V](), updateErr } return contents, nil } func (f *Fetcher[V]) Update() (V, bool, error) { buf, hash, err := f.vehicle.Read(f.ctx, f.hash) if err != nil { f.backoff.AddAttempt() // add a failed attempt to backoff return lo.Empty[V](), false, err } return f.loadBuf(buf, hash, f.vehicle.Type() != P.File) } func (f *Fetcher[V]) SideUpdate(buf []byte) (V, bool, error) { return f.loadBuf(buf, utils.MakeHash(buf), true) } func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (V, bool, error) { f.loadBufMutex.Lock() defer f.loadBufMutex.Unlock() now := time.Now() if f.hash.Equal(hash) { if updateFile { _ = os.Chtimes(f.vehicle.Path(), now, now) } f.updatedAt = now f.backoff.Reset() // no error, reset backoff return lo.Empty[V](), true, nil } if buf == nil { // f.hash has been changed between f.vehicle.Read but should not happen (cause by concurrent) return lo.Empty[V](), true, nil } contents, err := f.parser(buf) if err != nil { f.backoff.AddAttempt() // add a failed attempt to backoff return lo.Empty[V](), false, err } f.backoff.Reset() // no error, reset backoff if updateFile { if err = f.vehicle.Write(buf); err != nil { return lo.Empty[V](), false, err } } f.updatedAt = now f.hash = hash if f.onUpdate != nil { f.onUpdate(contents) } return contents, false, nil } func (f *Fetcher[V]) Close() error { f.ctxCancel() if f.watcher != nil { _ = f.watcher.Close() } return nil } func (f *Fetcher[V]) pullLoop(forceUpdate bool) { initialInterval := f.interval - time.Since(f.updatedAt) if initialInterval > f.interval { initialInterval = f.interval } if forceUpdate { // Delay 10 seconds initialInterval = 10 * time.Second } if attempt := f.backoff.Attempt(); attempt > 0 { // f.Update() was failed, decrease the interval from backoff to achieve fast retry if duration := f.backoff.ForAttempt(attempt); duration < initialInterval { initialInterval = duration } } timer := time.NewTimer(initialInterval) defer timer.Stop() for { select { case <-timer.C: if forceUpdate { log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name()) forceUpdate = false } f.updateWithLog() interval := f.interval if attempt := f.backoff.Attempt(); attempt > 0 { // f.Update() was failed, decrease the interval from backoff to achieve fast retry if duration := f.backoff.ForAttempt(attempt); duration < interval { interval = duration } } timer.Reset(interval) case <-f.ctx.Done(): return } } } func (f *Fetcher[V]) startPullLoop(forceUpdate bool) (err error) { // pull contents automatically if f.vehicle.Type() == P.File { f.watcher, err = fswatch.NewWatcher(fswatch.Options{ Path: []string{f.vehicle.Path()}, Callback: f.updateCallback, }) if err != nil { return err } err = f.watcher.Start() if err != nil { return err } } else if f.interval > 0 { go f.pullLoop(forceUpdate) } return } func (f *Fetcher[V]) updateCallback(path string) { f.updateWithLog() } func (f *Fetcher[V]) updateWithLog() { _, same, err := f.Update() if err != nil { log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error()) return } if same { log.Debugln("[Provider] %s's content doesn't change", f.Name()) return } log.Infoln("[Provider] %s's content update", f.Name()) return } func NewFetcher[V any](name string, interval time.Duration, vehicle P.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] { ctx, cancel := context.WithCancel(context.Background()) minBackoff := 10 * time.Second if interval < minBackoff { minBackoff = interval } return &Fetcher[V]{ ctx: ctx, ctxCancel: cancel, name: name, vehicle: vehicle, parser: parser, onUpdate: onUpdate, interval: interval, backoff: slowdown.Backoff{ Factor: 2, Jitter: false, Min: minBackoff, Max: interval, }, } } ================================================ FILE: core/Clash.Meta/component/resource/vehicle.go ================================================ package resource import ( "context" "errors" "io" "os" "path/filepath" "time" "github.com/metacubex/mihomo/common/utils" mihomoHttp "github.com/metacubex/mihomo/component/http" "github.com/metacubex/mihomo/component/profile/cachefile" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/http" ) const ( DefaultHttpTimeout = time.Second * 20 fileMode os.FileMode = 0o666 dirMode os.FileMode = 0o755 ) var ( etag = false ) func ETag() bool { return etag } func SetETag(b bool) { etag = b } func safeWrite(path string, buf []byte) error { dir := filepath.Dir(path) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, dirMode); err != nil { return err } } return os.WriteFile(path, buf, fileMode) } type FileVehicle struct { path string } func (f *FileVehicle) Type() P.VehicleType { return P.File } func (f *FileVehicle) Path() string { return f.path } func (f *FileVehicle) Url() string { return "file://" + f.path } func (f *FileVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) { buf, err = os.ReadFile(f.path) if err != nil { return } hash = utils.MakeHash(buf) return } func (f *FileVehicle) Proxy() string { return "" } func (f *FileVehicle) Write(buf []byte) error { return safeWrite(f.path, buf) } func NewFileVehicle(path string) *FileVehicle { return &FileVehicle{path: path} } type HTTPVehicle struct { url string path string proxy string header http.Header timeout time.Duration sizeLimit int64 inRead func(response *http.Response) provider P.ProxyProvider } func (h *HTTPVehicle) Url() string { return h.url } func (h *HTTPVehicle) Type() P.VehicleType { return P.HTTP } func (h *HTTPVehicle) Path() string { return h.path } func (h *HTTPVehicle) Proxy() string { return h.proxy } func (h *HTTPVehicle) Write(buf []byte) error { return safeWrite(h.path, buf) } func (h *HTTPVehicle) SetInRead(fn func(response *http.Response)) { h.inRead = fn } func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) { ctx, cancel := context.WithTimeout(ctx, h.timeout) defer cancel() header := h.header setIfNoneMatch := false if etag && oldHash.IsValid() { etagWithHash := cachefile.Cache().GetETagWithHash(h.url) if oldHash.Equal(etagWithHash.Hash) && etagWithHash.ETag != "" { if header == nil { header = http.Header{} } else { header = header.Clone() } header.Set("If-None-Match", etagWithHash.ETag) setIfNoneMatch = true } } resp, err := mihomoHttp.HttpRequest(ctx, h.url, http.MethodGet, header, nil, mihomoHttp.WithSpecialProxy(h.proxy)) if err != nil { return } defer resp.Body.Close() if h.inRead != nil { h.inRead(resp) } if resp.StatusCode < 200 || resp.StatusCode > 299 { if setIfNoneMatch && resp.StatusCode == http.StatusNotModified { return nil, oldHash, nil } err = errors.New(resp.Status) return } var reader io.Reader = resp.Body if h.sizeLimit > 0 { reader = io.LimitReader(reader, h.sizeLimit) } buf, err = io.ReadAll(reader) if err != nil { return } hash = utils.MakeHash(buf) if etag { cachefile.Cache().SetETagWithHash(h.url, cachefile.EtagWithHash{ Hash: hash, ETag: resp.Header.Get("ETag"), Time: time.Now(), }) } return } func NewHTTPVehicle(url string, path string, proxy string, header http.Header, timeout time.Duration, sizeLimit int64) *HTTPVehicle { return &HTTPVehicle{ url: url, path: path, proxy: proxy, header: header, timeout: timeout, sizeLimit: sizeLimit, } } ================================================ FILE: core/Clash.Meta/component/slowdown/backoff.go ================================================ // modify from https://github.com/jpillora/backoff/blob/v1.0.0/backoff.go package slowdown import ( "math" "sync/atomic" "time" "github.com/metacubex/randv2" ) // Backoff is a time.Duration counter, starting at Min. After every call to // the Duration method the current timing is multiplied by Factor, but it // never exceeds Max. // // Backoff is not generally concurrent-safe, but the ForAttempt method can // be used concurrently. type Backoff struct { attempt atomic.Uint64 // Factor is the multiplying factor for each increment step Factor float64 // Jitter eases contention by randomizing backoff steps Jitter bool // Min and Max are the minimum and maximum values of the counter Min, Max time.Duration } // Duration returns the duration for the current attempt before incrementing // the attempt counter. See ForAttempt. func (b *Backoff) Duration() time.Duration { d := b.ForAttempt(float64(b.attempt.Add(1) - 1)) return d } const maxInt64 = float64(math.MaxInt64 - 512) // ForAttempt returns the duration for a specific attempt. This is useful if // you have a large number of independent Backoffs, but don't want use // unnecessary memory storing the Backoff parameters per Backoff. The first // attempt should be 0. // // ForAttempt is concurrent-safe. func (b *Backoff) ForAttempt(attempt float64) time.Duration { // Zero-values are nonsensical, so we use // them to apply defaults min := b.Min if min <= 0 { min = 100 * time.Millisecond } max := b.Max if max <= 0 { max = 10 * time.Second } if min >= max { // short-circuit return max } factor := b.Factor if factor <= 0 { factor = 2 } //calculate this duration minf := float64(min) durf := minf * math.Pow(factor, attempt) if b.Jitter { durf = randv2.Float64()*(durf-minf) + minf } //ensure float64 wont overflow int64 if durf > maxInt64 { return max } dur := time.Duration(durf) //keep within bounds if dur < min { return min } if dur > max { return max } return dur } // Reset restarts the current attempt counter at zero. func (b *Backoff) Reset() { b.attempt.Store(0) } // Attempt returns the current attempt counter value. func (b *Backoff) Attempt() float64 { return float64(b.attempt.Load()) } // AddAttempt adds one to the attempt counter. func (b *Backoff) AddAttempt() { b.attempt.Add(1) } // Copy returns a backoff with equals constraints as the original func (b *Backoff) Copy() *Backoff { return &Backoff{ Factor: b.Factor, Jitter: b.Jitter, Min: b.Min, Max: b.Max, } } ================================================ FILE: core/Clash.Meta/component/slowdown/slowdown.go ================================================ package slowdown import ( "context" "sync/atomic" "time" ) type SlowDown struct { errTimes atomic.Int64 backoff Backoff } func (s *SlowDown) Wait(ctx context.Context) (err error) { timer := time.NewTimer(s.backoff.Duration()) defer timer.Stop() select { case <-timer.C: case <-ctx.Done(): err = ctx.Err() } return } func New() *SlowDown { return &SlowDown{ backoff: Backoff{ Min: 10 * time.Millisecond, Max: 1 * time.Second, Factor: 2, Jitter: true, }, } } func Do[T any](s *SlowDown, ctx context.Context, fn func() (T, error)) (t T, err error) { if s.errTimes.Load() > 10 { err = s.Wait(ctx) if err != nil { return } } t, err = fn() if err != nil { s.errTimes.Add(1) return } s.errTimes.Store(0) s.backoff.Reset() return } ================================================ FILE: core/Clash.Meta/component/sniffer/base_sniffer.go ================================================ package sniffer import ( "errors" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/sniffer" ) type SnifferConfig struct { OverrideDest bool Ports utils.IntRanges[uint16] } type BaseSniffer struct { ports utils.IntRanges[uint16] supportNetworkType constant.NetWork } // Protocol implements sniffer.Sniffer func (*BaseSniffer) Protocol() string { return "unknown" } // SniffData implements sniffer.Sniffer func (*BaseSniffer) SniffData(bytes []byte) (string, error) { return "", errors.New("TODO") } // SupportNetwork implements sniffer.Sniffer func (bs *BaseSniffer) SupportNetwork() constant.NetWork { return bs.supportNetworkType } // SupportPort implements sniffer.Sniffer func (bs *BaseSniffer) SupportPort(port uint16) bool { return bs.ports.Check(port) } func NewBaseSniffer(ports utils.IntRanges[uint16], networkType constant.NetWork) *BaseSniffer { return &BaseSniffer{ ports: ports, supportNetworkType: networkType, } } var _ sniffer.Sniffer = (*BaseSniffer)(nil) ================================================ FILE: core/Clash.Meta/component/sniffer/dispatcher.go ================================================ package sniffer import ( "errors" "net" "net/netip" "time" "github.com/metacubex/sing/common/metadata" "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/sniffer" "github.com/metacubex/mihomo/log" ) var ( ErrorUnsupportedSniffer = errors.New("unsupported sniffer") ErrorSniffFailed = errors.New("all sniffer failed") ErrNoClue = errors.New("not enough information for making a decision") ) type Dispatcher struct { enable bool sniffers map[sniffer.Sniffer]SnifferConfig forceDomain []C.DomainMatcher skipSrcAddress []C.IpMatcher skipDstAddress []C.IpMatcher skipDomain []C.DomainMatcher skipList *lru.LruCache[netip.AddrPort, uint8] forceDnsMapping bool parsePureIp bool } func (sd *Dispatcher) shouldOverride(metadata *C.Metadata) bool { for _, matcher := range sd.skipDstAddress { if matcher.MatchIp(metadata.DstIP) { return false } } for _, matcher := range sd.skipSrcAddress { if matcher.MatchIp(metadata.SrcIP) { return false } } if metadata.Host == "" && sd.parsePureIp { return true } if metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping { return true } return sd.forceSniff(metadata) } func (sd *Dispatcher) forceSniff(metadata *C.Metadata) bool { for _, matcher := range sd.forceDomain { if matcher.MatchDomain(metadata.Host) { return true } } return false } // UDPSniff is called when a UDP NAT is created and passed the first initialization packet. // It may return a wrapped packetSender if the sniffer process needs to wait for multiple packets. // This function must be non-blocking, and any blocking operations should be done in the wrapped packetSender. func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSender) C.PacketSender { metadata := packet.Metadata() if sd.shouldOverride(metadata) { for current, config := range sd.sniffers { if current.SupportNetwork() == C.UDP || current.SupportNetwork() == C.ALLNet { inWhitelist := current.SupportPort(metadata.DstPort) overrideDest := config.OverrideDest if inWhitelist { replaceDomain := func(metadata *C.Metadata, host string) { if sd.domainCanReplace(host) { replaceDomain(metadata, host, overrideDest) } else { log.Debugln("[Sniffer] Skip sni[%s]", host) } } if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok { return wrapable.WrapperSender(packetSender, replaceDomain) } host, err := current.SniffData(packet.Data()) if err != nil { continue } replaceDomain(metadata, host) return packetSender } } } } return packetSender } // TCPSniff returns true if the connection is sniffed to have a domain func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool { if sd.shouldOverride(metadata) { inWhitelist := false overrideDest := false for sniffer, config := range sd.sniffers { if sniffer.SupportNetwork() == C.TCP || sniffer.SupportNetwork() == C.ALLNet { inWhitelist = sniffer.SupportPort(metadata.DstPort) if inWhitelist { overrideDest = config.OverrideDest break } } } if !inWhitelist { return false } forceSniffer := sd.forceSniff(metadata) dst := metadata.AddrPort() if !forceSniffer { if count, ok := sd.skipList.Get(dst); ok && count > 5 { log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) return false } } host, err := sd.sniffDomain(conn, metadata) if err != nil { if !forceSniffer { sd.cacheSniffFailed(metadata) } log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) return false } if !sd.domainCanReplace(host) { log.Debugln("[Sniffer] Skip sni[%s]", host) return false } sd.skipList.Delete(dst) replaceDomain(metadata, host, overrideDest) return true } return false } func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) { metadata.SniffHost = host if overrideDest { log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]", metadata.NetWork, metadata.SourceDetail(), metadata.RemoteAddress(), metadata.Host, host) metadata.Host = host metadata.DstIP = netip.Addr{} } metadata.DNSMode = C.DNSNormal } func (sd *Dispatcher) domainCanReplace(host string) bool { if host == "." || !metadata.IsDomainName(host) { return false } for _, matcher := range sd.skipDomain { if matcher.MatchDomain(host) { return false } } return true } func (sd *Dispatcher) Enable() bool { return sd != nil && sd.enable } func (sd *Dispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (string, error) { //defer func(start time.Time) { // log.Debugln("[Sniffer] [%s] Sniffing took %s", metadata.DstIP, time.Since(start)) //}(time.Now()) for s := range sd.sniffers { if s.SupportNetwork() == C.TCP && s.SupportPort(metadata.DstPort) { _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) _, err := conn.Peek(1) _ = conn.SetReadDeadline(time.Time{}) if err != nil { _, ok := err.(*net.OpError) if ok { sd.cacheSniffFailed(metadata) log.Errorln("[Sniffer] [%s] [%s] may not have any sent data, Consider adding skip", metadata.DstIP, s.Protocol()) _ = conn.Close() } return "", err } bufferedLen := conn.Buffered() bytes, err := conn.Peek(bufferedLen) if err != nil { log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err) continue } host, err := s.SniffData(bytes) var e *errNeedAtLeastData if errors.As(err, &e) { //log.Debugln("[Sniffer] [%s] [%s] %v, got length: %d", metadata.DstIP, s.Protocol(), e, len(bytes)) _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) bytes, err = conn.Peek(e.length) _ = conn.SetReadDeadline(time.Time{}) //log.Debugln("[Sniffer] [%s] [%s] try again, got length: %d", metadata.DstIP, s.Protocol(), len(bytes)) if err != nil { log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err) continue } host, err = s.SniffData(bytes) } if err != nil { //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, error: %v", metadata.DstIP, s.Protocol(), err) continue } _, err = netip.ParseAddr(host) if err == nil { //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, got host [%s]", metadata.DstIP, s.Protocol(), host) continue } //log.Debugln("[Sniffer] [%s] [%s] Sniffed [%s]", metadata.DstIP, s.Protocol(), host) return host, nil } } return "", ErrorSniffFailed } func (sd *Dispatcher) cacheSniffFailed(metadata *C.Metadata) { dst := metadata.AddrPort() sd.skipList.Compute(dst, func(oldValue uint8, loaded bool) (newValue uint8, delete bool) { if oldValue <= 5 { oldValue++ } return oldValue, false }) } type Config struct { Enable bool Sniffers map[sniffer.Type]SnifferConfig ForceDomain []C.DomainMatcher SkipSrcAddress []C.IpMatcher SkipDstAddress []C.IpMatcher SkipDomain []C.DomainMatcher ForceDnsMapping bool ParsePureIp bool } func NewDispatcher(snifferConfig *Config) (*Dispatcher, error) { dispatcher := Dispatcher{ enable: snifferConfig.Enable, forceDomain: snifferConfig.ForceDomain, skipSrcAddress: snifferConfig.SkipSrcAddress, skipDstAddress: snifferConfig.SkipDstAddress, skipDomain: snifferConfig.SkipDomain, skipList: lru.New(lru.WithSize[netip.AddrPort, uint8](128), lru.WithAge[netip.AddrPort, uint8](600)), forceDnsMapping: snifferConfig.ForceDnsMapping, parsePureIp: snifferConfig.ParsePureIp, sniffers: make(map[sniffer.Sniffer]SnifferConfig, len(snifferConfig.Sniffers)), } for snifferName, config := range snifferConfig.Sniffers { s, err := NewSniffer(snifferName, config) if err != nil { log.Errorln("Sniffer name[%s] is error", snifferName) return &Dispatcher{enable: false}, err } dispatcher.sniffers[s] = config } return &dispatcher, nil } func NewSniffer(name sniffer.Type, snifferConfig SnifferConfig) (sniffer.Sniffer, error) { switch name { case sniffer.TLS: return NewTLSSniffer(snifferConfig) case sniffer.HTTP: return NewHTTPSniffer(snifferConfig) case sniffer.QUIC: return NewQuicSniffer(snifferConfig) default: return nil, ErrorUnsupportedSniffer } } ================================================ FILE: core/Clash.Meta/component/sniffer/http_sniffer.go ================================================ package sniffer import ( "bytes" "errors" "fmt" "net" "strings" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/sniffer" ) var ( // refer to https://pkg.go.dev/net/http@master#pkg-constants methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect", "patch", "trace"} errNotHTTPMethod = errors.New("not an HTTP method") ) type version byte const ( HTTP1 version = iota HTTP2 ) type HTTPSniffer struct { *BaseSniffer version version host string } var _ sniffer.Sniffer = (*HTTPSniffer)(nil) func NewHTTPSniffer(snifferConfig SnifferConfig) (*HTTPSniffer, error) { ports := snifferConfig.Ports if len(ports) == 0 { ports = utils.IntRanges[uint16]{utils.NewRange[uint16](80, 80)} } return &HTTPSniffer{ BaseSniffer: NewBaseSniffer(ports, C.TCP), }, nil } func (http *HTTPSniffer) Protocol() string { switch http.version { case HTTP1: return "http1" case HTTP2: return "http2" default: return "unknown" } } func (http *HTTPSniffer) SupportNetwork() C.NetWork { return C.TCP } func (http *HTTPSniffer) SniffData(bytes []byte) (string, error) { domain, err := SniffHTTP(bytes) if err == nil { return *domain, nil } else { return "", err } } func beginWithHTTPMethod(b []byte) error { for _, m := range &methods { if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) { return nil } if len(b) < len(m) { return ErrNoClue } } return errNotHTTPMethod } func SniffHTTP(b []byte) (*string, error) { if err := beginWithHTTPMethod(b); err != nil { return nil, err } _ = &HTTPSniffer{ version: HTTP1, } headers := bytes.Split(b, []byte{'\n'}) for i := 1; i < len(headers); i++ { header := headers[i] if len(header) == 0 { break } parts := bytes.SplitN(header, []byte{':'}, 2) if len(parts) != 2 { continue } key := strings.ToLower(string(parts[0])) if key == "host" { rawHost := strings.ToLower(string(bytes.TrimSpace(parts[1]))) host, _, err := net.SplitHostPort(rawHost) if err != nil { if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") { return parseHost(rawHost) } else { return nil, err } } if net.ParseIP(host) != nil { return nil, fmt.Errorf("host is ip") } return &host, nil } } return nil, ErrNoClue } func parseHost(host string) (*string, error) { if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { if net.ParseIP(host[1:len(host)-1]) != nil { return nil, fmt.Errorf("host is ip") } } if net.ParseIP(host) != nil { return nil, fmt.Errorf("host is ip") } return &host, nil } ================================================ FILE: core/Clash.Meta/component/sniffer/quic_sniffer.go ================================================ package sniffer import ( "crypto" "crypto/aes" "crypto/cipher" "encoding/binary" "errors" "io" "sync" "time" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/sniffer" "github.com/metacubex/quic-go/quicvarint" "golang.org/x/crypto/hkdf" ) // Modified from https://github.com/v2fly/v2ray-core/blob/master/common/protocol/quic/sniff.go const ( versionDraft29 uint32 = 0xff00001d version1 uint32 = 0x1 quicPacketTypeInitial = 0x00 quicPacketType0RTT = 0x01 // Timeout before quic sniffer all packets quicWaitConn = time.Second * 3 // maxCryptoStreamOffset is the maximum offset allowed on any of the crypto streams. // This limits the size of the ClientHello and Certificates that can be received. maxCryptoStreamOffset = 16 * (1 << 10) ) var ( quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} errNotQuic = errors.New("not QUIC") errNotQuicInitial = errors.New("not QUIC initial packet") ) var _ sniffer.Sniffer = (*QuicSniffer)(nil) var _ sniffer.MultiPacketSniffer = (*QuicSniffer)(nil) type QuicSniffer struct { *BaseSniffer } func NewQuicSniffer(snifferConfig SnifferConfig) (*QuicSniffer, error) { ports := snifferConfig.Ports if len(ports) == 0 { ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)} } return &QuicSniffer{ BaseSniffer: NewBaseSniffer(ports, C.UDP), }, nil } func (sniffer *QuicSniffer) Protocol() string { return "quic" } func (sniffer *QuicSniffer) SupportNetwork() C.NetWork { return C.UDP } func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) { return "", ErrorUnsupportedSniffer } func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, replaceDomain sniffer.ReplaceDomain) constant.PacketSender { return &quicPacketSender{ PacketSender: packetSender, replaceDomain: replaceDomain, chClose: make(chan struct{}), } } var _ constant.PacketSender = (*quicPacketSender)(nil) type quicPacketSender struct { lock sync.RWMutex ranges utils.IntRanges[uint64] buffer []byte result *string replaceDomain sniffer.ReplaceDomain constant.PacketSender chClose chan struct{} closed bool } // Send will send PacketAdapter nonblocking // the implement must call UDPPacket.Drop() inside Send func (q *quicPacketSender) Send(current constant.PacketAdapter) { defer q.PacketSender.Send(current) q.lock.RLock() if q.closed { q.lock.RUnlock() return } q.lock.RUnlock() err := q.readQuicData(current.Data()) if err != nil { q.close() return } } // DoSniff wait sniffer recv all fragments and update the domain func (q *quicPacketSender) DoSniff(metadata *constant.Metadata) error { select { case <-q.chClose: q.lock.RLock() if q.result != nil { host := *q.result q.replaceDomain(metadata, host) } q.lock.RUnlock() break case <-time.After(quicWaitConn): q.close() } return q.PacketSender.DoSniff(metadata) } // Close stop the Process loop func (q *quicPacketSender) Close() { q.PacketSender.Close() q.close() } func (q *quicPacketSender) close() { q.lock.Lock() q.closeLocked() q.lock.Unlock() } func (q *quicPacketSender) closeLocked() { if !q.closed { close(q.chClose) q.closed = true if q.buffer != nil { _ = pool.Put(q.buffer) q.buffer = nil } q.ranges = nil } } func (q *quicPacketSender) readQuicData(b []byte) error { buffer := buf.As(b) typeByte, err := buffer.ReadByte() if err != nil { return errNotQuic } isLongHeader := typeByte&0x80 > 0 if !isLongHeader || typeByte&0x40 == 0 { return errNotQuicInitial } vb, err := buffer.ReadBytes(4) if err != nil { return errNotQuic } versionNumber := binary.BigEndian.Uint32(vb) if versionNumber != 0 && typeByte&0x40 == 0 { return errNotQuic } else if versionNumber != versionDraft29 && versionNumber != version1 { return errNotQuic } connIdLen, err := buffer.ReadByte() if err != nil || connIdLen == 0 { return errNotQuic } destConnID := make([]byte, int(connIdLen)) if _, err := io.ReadFull(buffer, destConnID); err != nil { return errNotQuic } packetType := (typeByte & 0x30) >> 4 if packetType != quicPacketTypeInitial { return nil } if l, err := buffer.ReadByte(); err != nil { return errNotQuic } else if _, err := buffer.ReadBytes(int(l)); err != nil { return errNotQuic } tokenLen, err := quicvarint.Read(buffer) if err != nil || tokenLen > uint64(len(b)) { return errNotQuic } if _, err = buffer.ReadBytes(int(tokenLen)); err != nil { return errNotQuic } packetLen, err := quicvarint.Read(buffer) if err != nil { return errNotQuic } hdrLen := len(b) - buffer.Len() var salt []byte if versionNumber == version1 { salt = quicSalt } else { salt = quicSaltOld } initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) hpKey := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16) block, err := aes.NewCipher(hpKey) if err != nil { return err } cache := buf.NewPacket() defer cache.Release() mask := cache.Extend(block.BlockSize()) block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16]) firstByte := b[0] // Encrypt/decrypt first byte. if isLongHeader { // Long header: 4 bits masked // High 4 bits are not protected. firstByte ^= mask[0] & 0x0f } else { // Short header: 5 bits masked // High 3 bits are not protected. firstByte ^= mask[0] & 0x1f } packetNumberLength := int(firstByte&0x3 + 1) // max = 4 (64-bit sequence number) extHdrLen := hdrLen + packetNumberLength // copy to avoid modify origin data extHdr := cache.Extend(extHdrLen) copy(extHdr, b) extHdr[0] = firstByte packetNumber := extHdr[hdrLen:extHdrLen] // Encrypt/decrypt packet number. for i := range packetNumber { packetNumber[i] ^= mask[1+i] } if int(packetLen)+hdrLen > len(b) || extHdrLen > len(b) { return errNotQuic } data := b[extHdrLen : int(packetLen)+hdrLen] key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) aesCipher, err := aes.NewCipher(key) if err != nil { return err } aead, err := cipher.NewGCM(aesCipher) if err != nil { return err } // We only decrypt once, so we do not need to XOR it back. // https://github.com/quic-go/qtls-go1-20/blob/e132a0e6cb45e20ac0b705454849a11d09ba5a54/cipher_suites.go#L496 for i, b := range packetNumber { iv[len(iv)-len(packetNumber)+i] ^= b } dst := cache.Extend(len(data)) decrypted, err := aead.Open(dst[:0], iv, data, extHdr) if err != nil { return err } buffer = buf.As(decrypted) for i := 0; !buffer.IsEmpty(); i++ { q.lock.RLock() if q.closed { q.lock.RUnlock() // close() was called, just return return nil } q.lock.RUnlock() frameType := byte(0x0) // Default to PADDING frame for frameType == 0x0 && !buffer.IsEmpty() { frameType, _ = buffer.ReadByte() } switch frameType { case 0x00: // PADDING frame case 0x01: // PING frame case 0x02, 0x03: // ACK frame if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged return io.ErrUnexpectedEOF } if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay return io.ErrUnexpectedEOF } ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count if err != nil { return io.ErrUnexpectedEOF } if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range return io.ErrUnexpectedEOF } for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap return io.ErrUnexpectedEOF } if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length return io.ErrUnexpectedEOF } } if frameType == 0x03 { if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count return io.ErrUnexpectedEOF } if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count return io.ErrUnexpectedEOF } if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count return io.ErrUnexpectedEOF } } case 0x06: // CRYPTO frame, we will use this frame offset, err := quicvarint.Read(buffer) // Field: Offset if err != nil { return io.ErrUnexpectedEOF } length, err := quicvarint.Read(buffer) // Field: Length if err != nil || length > uint64(buffer.Len()) { return io.ErrUnexpectedEOF } end := offset + length if end > maxCryptoStreamOffset { return io.ErrShortBuffer } q.lock.Lock() if q.closed { q.lock.Unlock() // close() was called, just return return nil } if q.buffer == nil { q.buffer = pool.Get(maxCryptoStreamOffset)[:end] } else if end > uint64(len(q.buffer)) { q.buffer = q.buffer[:end] } target := q.buffer[offset:end] if _, err := buffer.Read(target); err != nil { // Field: Crypto Data q.lock.Unlock() return io.ErrUnexpectedEOF } q.ranges = append(q.ranges, utils.NewRange(offset, end)) q.ranges = q.ranges.Merge() q.lock.Unlock() case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code return io.ErrUnexpectedEOF } if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type return io.ErrUnexpectedEOF } length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length if err != nil { return io.ErrUnexpectedEOF } if _, err := buffer.ReadBytes(int(length)); err != nil { // Field: Reason Phrase return io.ErrUnexpectedEOF } default: // Only above frame types are permitted in initial packet. // See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8 return errNotQuicInitial } } return q.tryAssemble() } func (q *quicPacketSender) tryAssemble() error { q.lock.RLock() if q.closed { q.lock.RUnlock() // close() was called, just return return nil } if len(q.ranges) != 1 || q.ranges[0].Start() != 0 || q.ranges[0].End() != uint64(len(q.buffer)) { q.lock.RUnlock() // incomplete fragment, just return return nil } if len(q.buffer) <= 4 || // Handshake Type (1) + uint24 Length (3) + ClientHello body // maxCryptoStreamOffset is in the valid range of uint16 so just ignore the q.buffer[1] int(binary.BigEndian.Uint16([]byte{q.buffer[2], q.buffer[3]})+4) != len(q.buffer) { q.lock.RUnlock() // end of segment not reached, just return return nil } domain, err := ReadClientHello(q.buffer) q.lock.RUnlock() if err != nil { return err } q.lock.Lock() q.result = domain q.closeLocked() q.lock.Unlock() return nil } 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 } ================================================ FILE: core/Clash.Meta/component/sniffer/sniff_test.go ================================================ package sniffer import ( "bytes" "encoding/hex" "net" "net/netip" "testing" "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/assert" ) type fakeSender struct { constant.PacketSender } var _ constant.PacketSender = (*fakeSender)(nil) func (e *fakeSender) Send(packet constant.PacketAdapter) { // Ensure that the wrapper's Send can correctly handle the situation where the packet is directly discarded. packet.Drop() } func (e *fakeSender) DoSniff(metadata *constant.Metadata) error { return nil } type fakeUDPPacket struct { data []byte data2 []byte // backup } func (s *fakeUDPPacket) InAddr() net.Addr { return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) } func (s *fakeUDPPacket) LocalAddr() net.Addr { return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) } func (s *fakeUDPPacket) Data() []byte { return s.data } func (s *fakeUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { return 0, net.ErrClosed } func (s *fakeUDPPacket) Drop() { for i := range s.data { if s.data[i] != s.data2[i] { // ensure input data not changed panic("data has been changed!") } s.data[i] = 0 // forcing data to become illegal } s.data = nil } var _ constant.UDPPacket = (*fakeUDPPacket)(nil) func asPacket(data string) constant.PacketAdapter { pktData, _ := hex.DecodeString(data) meta := &constant.Metadata{} pkt := &fakeUDPPacket{data: pktData, data2: bytes.Clone(pktData)} pktAdp := constant.NewPacketAdapter(pkt, meta) return pktAdp } const fakeHost = "fake.host.com" func testQuicSniffer(data []string, async bool) (string, string, error) { q, err := NewQuicSniffer(SnifferConfig{}) if err != nil { return "", "", err } resultCh := make(chan *constant.Metadata, 1) emptySender := &fakeSender{} sender := q.WrapperSender(emptySender, func(metadata *constant.Metadata, host string) { replaceDomain(metadata, host, true) }) go func() { meta := constant.Metadata{Host: fakeHost} err := sender.DoSniff(&meta) if err != nil { panic(err) } resultCh <- &meta }() for _, d := range data { if async { go sender.Send(asPacket(d)) } else { sender.Send(asPacket(d)) } } meta := <-resultCh return meta.SniffHost, meta.Host, nil } func TestQuicHeaders(t *testing.T) { cases := []struct { input []string domain string invalid bool }{ //Normal domain quic sniff { input: []string{"cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b"}, domain: "www.google.com", }, { input: []string{"c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738"}, domain: "cloudflare-dns.com", }, // Fragmented quic sniff { input: []string{ "c70000000108afb466a232f7f9f2000044d0168a15a021477ecb9731ed77784d42301462e2d59b0395adc1fa6b569d428583f100860d6b6ae29b6c1b8c0f9c0d9081475ff801f34a9e0677adf685f02b1169fe86c683fb51934915ff43921a73b98fb0b734406f8dd90ce6060d75e923b0d3c738291b421bf16de27ed4785d727ce589f5d0957c413c81d6ee75052e3ab50fe53f1abbb24a138a52e1412683992ad769e65ed301a736914843543e2a3e11eb395726d4fcc9283f8607b38685069f63d05ab8bf38aa24d4073a1e68fa1b6087cec44d7fa628342e9d88a0d20b381014cdd1a07b9d913a3bbcad0cfbddd0560617cf26054138075eb86e06db1e68781541587302e6dda86cae779f9848fcefcc33626f8953bfe4dc293d23e74c87020e79e9ffd58ee345382bd4d1d6e5a3389b0a977124708d05e3c305545857041734dc7092901ab54604b3750b3139dd3b8f2bd94cda89d85be3756fda6f0cfb6f66af3d2e36a7808ff7bce271a0272f8dbc88193ede31613433985cd35c7bd9b627d434e7b2e94b38402b8f1b5619a903572dcf4c2b864c6ee66657c9ec81e03fbe765037f83b2229171888ba08651fc78a1b50c7cc52f6dfe8273723e08932b1a16a6b717a80b5520cf3f40e46f9d9c350eaa914bf99dd4ab700cfdae21437daf695916d4f3121235e4913e0657d8cdcf4afd8f2c7ef977a2dfe49f46fef46c8fa6932e745311d4a6eb3124d5e0a204b9e3227e86a55e662f7002d4f4a72cba8c77c3adc3eff076dfb9195cf68455cecbbfc9b5444d9c4a4775bba68d57ff52edac6ce6ff4efbf6466579bf68308f2ba9a59b2c09506064091a86af621e9dae52366a90599db0d64a23944bc48966b6d3ab8e20f4afb5b0e94370d26a89a9c4207b454554e58ac74f62ffb3eb2686eaa596b9610322a5ce8eeb42f2ead1c71b11b51bc4f1800eb549a2bb529ca4a0d165ae461e45b556b2365e9459d531489d59d0dfa544a76c5c00b0a01270741d4061a331c32fd6cd0e68bbce49137b852e215c9db52f3e430416d8979520e5270be324f3d93132358c0eac35a4618ea7aad997dbbd8e99d4ea577271b935e3fe928f90abd94593806d272a565a414686b8e56c28e34b77671de6a696b09414380bc658c69a309d3225ba8493e9076dac776c845ce11a7ccd6cae58fba5434014250f3e211058b2efe3424b991d679a02ba949b086ba12144c7df3e049b5d026f386e4ae712c9b0b4b02730dd6862ed4e72730224cb6ec9101c5cbb7ee4fc30d497bb1dbf74ffdd49d8cae6c7c9a364ede453d9ae25edf27a2153ab285f3e3be66b2968d67a56480f1f74c4fe61dc69db3451f5b113d7ca02e5afa8627f579c07a9b1814853fb8fdaf0c0f220f89725c757f5617ba4e43cb4f3ad9ce18f48f23d10f9e8950b0fd737070655730532896d93df8768860ebf941365d0634db399feab1f8a88bad28d25e689c5a57321debb8d1435130e90a699e17fa5255f2063f09659a432e9ab5f89eeabe12756bfc5e02fcae2b78a9d0f570934b8d4af8f4afbd57549176f465a0cea485dd89c95a8ae915b4b99548a4c939710c16908f968368baf5f547cfee07f3cbb6142041d6e6084aac253a0d3aeac628cfe76f87b94c3806cb14a912ce8e4981e316511d5ede36f526805d6c3fab5b72d9d91f4eacd26e28cb181ec66611818f5c206ddd52488707a940dc12144ae825d25929bc32b718f46e471fdb30762d299b45c84f6310a72b60", "c20000000108afb466a232f7f9f2000044d00582e8683e329a63e5bf4dc93e93e325ff661e74b9cabefdfbf6065c7ab203c8a629534e87e5f2d4c0f463352904642358b8f137e99802c3a26cf22235782a777769ecd134c6b4d0dce6aa10b485c45ccdcf6deb805342e99ef97e2777aee0b2a44073843fccc2f8eb837031f76a8e968cb01c13c1268af095f54f860958e4062a84e2527bcc9b25a7791650a844de1b0c4b2476282a0e00c9de9d39a41914d1e797a88a8997b96b25a4c194762912b2ddee0e01a365f1afa1e82ea266c14ae94e47c90b5679e2cd00e63ee5a834505ca33463751bac22f3b87afc80099335dc7bfd12b7df224a23ced3d2e25b58a04c4b5cb089ca187abc54d782973c7bb157cc515c7508431ff5bdc227871da58b9ca8a9a576960f38edb384112b08e4c70672a6f23d17d9d901342e56c12370deaaafcb22810eb352f1a6d9377e96bdc1ad4dd397dbc6a227b70f204c1a4e9a4db2705763b82ec4df1fab11420aae547155c6b49abceeed997ff01b7d24e369c65f7edf18665d067c7d2bda5ec8623281fce8c77d893cb8a42053756713e910894a58ef5bf3d9f3a41071026660dd7cd05e1640767ec68f78e22c1716700ca9c0f076f90a65cffc394c10a32071c6532d07b59414181070d08c9c84e3d13842718d51bf90dd36ab1b3f708df7eeb3939dc8553787308983c3e9ba971e7d447788477a7140196c2f717b9ba4f5da92d73316dd11c1d1830b4200f26f733a6c65ec1cc21549b485e3a43dc7a2b68e95466a53544082a20d9a43387a7ccbfd353f7e590b7047f13bfc0d91923c2d75dad4f8091ea96502f98e83e5c30e52e4cd5c670f6c2248ce37cd6ee8b3970531fbf0c53c5fa9a0d73200442b755c91fa4f70524ffe8a36063b6709d3aa9f6b53eb0aaecc57a8c8c9a7ac5e57e03e9cfb290b67dd8222a245ff5439914147e2799fd1cd2ca2cb22fda299443b81e8024adc59d098058432fa4bde376b8e59075f6b86427b4ef6cd7c83b5c08add0c3d3543aee8d672c41cb287c1f0a17f1bc30f62a57490afb2d9f401bf302fd473ddbaf63f6883221579743d6aa1f386b8b2f5db06d7d6c36be81f29fafd14b82e863d744f116ce2be4921631f1fb2797289fffa9ee16a3e537ddfa52350546bc544459c0c9d66fdcbd41612cfc0e2744f50927983a3224291c1ae51608fbc00f40c60ec72573a7e128c3415b0d9a7db52de8ff763dd66e2eeb03ef2e67838c9e68cfddae4b86a3f34a69e0a473b5a73ab627282648df7912c11a4bf033ade185a8f438036b99b960aa6213c800abbbd751248a7ae600357ab888433125d49c5643705ecb8c86f2980050edd7e3c579ad6fcae9bbe2c8d8b38004426f35eadb543a3bef42355acb1b94c21d7eae7b6ed422ca0d58fa03b227b035628871465ed6509254c8a3bf43dfadbb247ecbc52d80d65e9c03c4bc7bc35a829502bde3868af9c33737cd88d70f7427790313eed4ed1938955c5dd360212ef700f274efcc8c26ea94c4e2e0937d475c5c4909edfb66714d15d12e153e5586725ce0c47e8a1506bb197366754ca8960508f22fe7b83a5eaa40f05f3cb87464dc6b848080c0e0cecf2dae82bfa42cc6f52694478dc3d00ab0e1ed696b98e26c7fd34d2efd969f83e284c28ce3f27b178f4691c772011f61722266153142dd0d526393e6c6848d201115b256e65f12b911a983bc2f96a5b4b99f63f0b58485a521553a3e1d4498ac5d4ee70c3f9", }, domain: "quic.nginx.org", }, { input: []string{ "c00000000108e63b9140d034563d000044d066e1913892ec1d84c179dfa9596e0ce930171a134a09446a888d9e579a6f7bd77df6deda715b028d64f7866603c6deb468d60ecc6488b5e5ee2e2daa1840b76ead998023593c9ebc4178ec89cb198d3c79a867e27177a74ee5f3db74ea194e36e328047ffc3890192665a6feba09ba1e224967fa9575dc7b094e1c29c7f3be9961ba62e3e063f674a09786b7611138e1edaee32cd1d47839e840a74f25ed786463fc48bf3d38a4c793178ab7cbf5a3eb974415b9f9ef7861dfc73460594332f5545c7b7037043afdfc1aa62ac3dfb76ec2c6ae8ebd351f7483992c762d6483b3e2c1454c8ed939ce43f858ccca22d9149cc9da16af86a010be7f3248cf19fa442e94d625ec7f7144b01ac9afb8fb8c595d4cd12fcd2b2d9986371ae65f6f216bed152b79d2782d60f1f01e06b359f88900c4bb3f987f3ce336854a5beaaa616813af4e5f9bd82dca0af6886b544fff0261807bbd8cf90213299f5802b98edc27a6606be8e2bbc18fa7519eac260dcda139f164796a082908459c31aa964a5d3f6fed8944ad61bda126991468f3b7627f2470179619864f234a395ea3bd4f7ba4c0cdf9f5f0dd95d7d59476f2d2a36521c13886265a2fbbd4345e8d1d1e7b5d01a58fb11de23730b087e2b702200155a1ebd50db5751d279438822ac158173533140998a3056893bf470ac84720cb37a4a3205fa88267abc56520bcddacee06011d929c3a114314822d8ccf7cfef89f2fcf0a4fef800afbfca4a62ee848f22066f68c7d3c5c9a24402d422fc2fd5da6d3b470b0ea253f12a883705f7f78bd67006ade4f1c8a3e8fa052656b5b40dacd8062228871cc3bfb1a9c38472b0a720c3c750430edbcbdcecd46b144dfcaa009fee06770238d0270e80671e8ee5f5df18b86dfe8df2f121245c0710ccaefecbeda0ba3db945c768624dc38f21a4ac53741f4e58a5052f3d667fc466b69905f05d0843cfcb830163fae18dd1eb0ce62a59420db9c44958a0eca9ba4258c8060a9956343f155da6c55b2060427d07d9e311729d2971439c7541ae2babfce25a3f5f361fde86c39ef6c04e4e3cf7dd70c9cc0758ec5db3f0cb368e2447080af51c8a5fa6b84ec3175d2d3e6d877b6953e433b4e94b52e1a5f2a1ca37124c27e47f9de5d4c74644181cd37f3f3863ca529c0847bba91c246dadba94b4566b08eaa06a0db4d58b8cb0c8d3070533306a3089891b24a7c4e11b3aa50d5628fc1d136388e8bfbc420a6f12701333ccdc95dec25d09ce25fa4b654260965b91f05b1542c2ee02008d01de4419f14d6749c4bcfcc45a332ba0772def720ea3c8d207802418137b733e779eb406dace0b4b5f5e5e14c787f3e044e6d8160f90fc3c65bcc7f3449205b63294fbc11e9bb92c007d1cb59183eafbf76be9680224cb442806500d71870777d087bf864890848f4a79424c02304f2a6ee2b07f9257f4a2f185ee21239625e246cf680e74b85d292cca44261c6cee6da39bfac3882d28fe547a500f79519ffcd3f54ff5a905c99f22a5e8142c903c41adbe1eb9770b6cf554688529091b126ed2168a23bb191c2b89728e31773623bc58bcb9baebc2c664c79d6ffee7e4404e039723eb05e7f7835c87212431a0131603fcc3fe090cc2fda8239b8f42188b35f98d7fff949b3044544b3bb962ae236a664d76d0c751d9c9ed1271715d240f111febdf7045502f2afd7de8aaaac650511e7bc7716a5b6622ae925abb7", "c90000000108e63b9140d034563d000044d00b2498988864d8b7f59a00d26165f5ae638fc9b1c12d546ffd86212ccd85f654259cb8b8c9d753c696ddad7ee4847bf3b3c10063606cf3972f75e17ae23e73b6a3029f23541f674256d19677665cdd0b8ac15c3f60984bc14ff5dc7a9ae37395516204f2020965713fccaf35cb0a5823085cd6211d681dc6b39be9db46cbfef154a2b9049ed202e9088961b0b710e94bd73259b0967e4d6b8cdfd5b72774fee2f2ceb16bcafa010f247c43b0a9ca25578e7d45bfda7edb82e91f8e1c0a2cfa990223bf97ece42862d3f329521fe2d12493b717f174f966d173102e5cca10943d5b612101d65d0dd48b44416f9ac1eac4575558ecaaa39c47ade2dee6e25fd219d799b499143b47a5bf449701b939c1dde111349cd0d63efd2ff74fbd3573ed40abfdb2310e2740da40fc50c7a137a3f32c3a26b3d407f80e669fe7f9a3542fdd412a9cb53f845d9c1af0814377bf92e30f05ee387fb8675807a6de083c85d3d7860601c8170923c53e5773ee388b68e510a28cd7009c485bd4cb861eddfdd265de042e5a018d20cb810614e2bb17b0f52d6bf620a6f173e0b41951e1b83ffb29e3b3b3c5d9fff13acd3b409021195201d003e281d8cda7b0f02c273e17b1f9b9e8cec4296d65a1c4923b78a2e4273cb42e4e159980472e440078e542eeddcc5a9bfefa5a72871fbcd9ebb74fef20a50215bf75cfd8572d5ab9ac5945e8d6ca35884caf0af0446ee9aab0a1cc3a452ec79c9de786119e63bb3a75fce0ae29c15a0c320fff87e87cc23a05e75b4f4b30b75c6aa036c4b6657f8200ea014185b31ee7fcd00d1eaf40973f347fae227f89d41794fa57ac1ed1efda3ba840ef27852cf33a9dc9e2d77b56af9ced9e75707837aa8c5395cdc15134ba132de87152ce53d506c53284dab912bbc276542504cc94afaca71a5173ff13ea6cb45b47dde9965428ba5d8eb968cc2a5729c2f9b8f1c1de208943a2cd565196e040dcc415d769ceb6300c7909d7e32bbbe83c4cbf4d49f6e34fe56b651838628f3a0001e99f39cafe45c98e455aff8d98f89942a862f7505b9f7fe3f64dacf8c574affacf91c2c05f094127acaa5187f9dfa188f67db421243a02e583942138c2edf45fec4c6b6a8a791da9055be247e9b252e9f7c1330e76f9cb3aa5feebb21f871315b5fb90a1df0b8056513b74daeb6ac995f85c64150ad115a14830d145e5f4e6638c26987b676a1dd19a9775df29ab442ce6143b0fbf8f8d4618084896e34812ed59d63041e2b4ccf6c959a6c849813dd926082bb7b1adedf69246547f335552bcdbae7e466ac31e07e442530ad114abebc6f58015b786e7f35644307fa7ad3d9248c56c8ff472735c6911da1843fe53821b8f5180f8844db4a9f7a826a919fd93c4db4d25861054929260dcdc46d085827c46d60f1097424a6ef250f5aaf3235c80230eda4eb580ce93e1ac8aac422a7aa1241562af601981b84b74949f1c476705c8030eb5d447b2414f9716ff3fd606cd750030b94345c016078bdcb97b7ebc24f661fbd08802f32df18d6a2aa85bfe2e9b8dc76b121c44ae9f29e4413051b527e99fde29720724337476c0eff325cb6220a290a9eb852151c84836729d6a223032e2c638857d9e7f469b84d7d650c45e56e763aee73f902e82b055425c4568725e2d4efd7fde8b02906bda48af86bf47ea27ff00f4528494b74be9bbff001cc841449a184a4e00d64e51a72660a2c21f704f", }, domain: "chat.openai.com", }, // Fragmented quic and 0-rtt packet sniff { input: []string{ "de0000000108c2751a596bd51c6e004041948ab7d9d493e9e1e9902a7734534fb9eaddc70ca7f821d1b58a406b23ba9db1d03266ae74765b03fac21c284fd50cb0a3d1ca71d8c3cabef5553dd1cb748ac662", "c50000000108c2751a596bd51c6e000044d0538af4ba75e226a6fc7f43e7f1f59610973b8a6670bb8338ca7ef7d90f81aa59f179dae5f8f6dbd24ec6fe576b28f6ce6cd46f26de143b8c99cdadaecf2041948a61bd5a8591486e10022fd100aa20e6423b4f4ca5773edb1aba79b73d6150ee185e66da60e658b2a698098462122b6b80c7fbc5542b0b8e9532898c1f31aa2ef55cbdf036d74c3069abbb261660f048d950b00b7db279ec2bc39912102679ddbffb53f1b1921f137fce43e164af86c72908532f4cdc48eb462a9d9e9cdd6d3c3faaf8aa8aea312dcac5d6aa75b1ade4af6901576649da7e3efd4199b92107d7acee8bbf06734b2484957c3d8cbb1f3fc0ccd56c55223628ed8ea514ffd101bac370c97b28c7da81175ab0508c0002d458cf41f7159dfce22b447c1ec502c186b782c1854718b7fc0fc39e5c09aee31113fc4c5003803fc27ca48850c08a54dbbfdef6ea9a6a138cac0ecd045cfd5607cb6c99c39c0cb21778857f97416b78fa7c6ac8ae3fa2ef2adb3b85fe3fdba70ef9265bb3d54e56ec68b8887d54d02d4a571a6b793ae4df8ff171c881a554b5c5a7848351d446ab94c90ee9c600f03b785fee6300450a4ffc2a55d417952e15449a491296d463ac6942bce4ca93c99440396bc8984073ec028b11ad412e97e26f9248031dc4b1a6ae385803bb578fa1a3b3a58a8ef19c6c511f17b28a275e8c40e51fca8f410a4a1879b5d8749a44a6a9f97c0c9df25318cc28fd0cc61eea78ddc603a17e74eb542c8c08cdbaafa3b44566db4d67e8d1429332375cd30cdaead9594c46d8ce91bce9813c3ca23f55ec2f4dd3ff141471bc3df590367bc65e4830018ff7d845ec4987d11e471d114c48acd1ae9b7670341a34077ae59ea6c3bfc4675cf419d37db48a98a5573b69867039731f537098b46415a193f50b2c85bf9e5da45d6757c5c366e21f04ea62d64b81c28be5148d89e53535414067cf609e59686b7fd135f5cb473e57f6c82dbb291308a1065e0f755935d77517adecee55e72cf37ecaab1b5c0c6e0c7463a014e7e439757913f6e43abb6af775d21ab6e43cbdbcd1935a000cf8025ebc11378d86d6f72d51bf2dfe4be1db5d3b0fcacd13e1b9fbaac6e9153c3d1f4e876f2fa9c3cfc84fd0910b778105b66be70827b1830b7b3c9633af5d83ad527efd81498cbbdd112873cc5ced573e6579acfe817b62280c2122b582b591d52b96cac047bff91192a5cfc001d15c811e055dcb1c9710dc892258ed1ab5152af2cfc57a0b93205dd41fd82b86090b4281b1493a8828ebc96bbd603b888cbca4a15799a5f3eaef93655d5609948080ca57c696d0ffc9a07665bdb063b547bb5a862c3b058c9efb2e7b79cf405fd83efacaa4b8e3a1fd126270587119756562c03d69a9cb67550369030a0204e531cb8df91ab2dfa2e4106c590c59b1b13c447843937929a574d3ea1785db0d52b4b2eeefd1a07c69729bec7c2813c9eb1249f706b3cc14a3d489d6b42a641dfd9e91aa70c7d3222e154af2d7fc1a8f48e5ba11739ae128d1f32ff929aaf4b249df5ea23f7847301e36ffda02342cdf1bd9dfd1979cbd8de32eb8b1eb8c415ddd267efe53f54678d9fc32435b34b00ee2256d8b6190e30a280df5bc48cf9fd669a52469954deccb0f1da37371d513ea57f31ead22a34f9379c7931fd18286d9fde6ecfaac8ca2a9be79d688c5401c65407543c066532f6621f256551c4a98a86b543c576ed0f3254daa4915", "cc0000000108c2751a596bd51c6e000044d07b624bf3d95fb3b7299b67dd836fbbbeb05a51650f9b2da3b2695070a0d19ab0d5334cc04de7ea7494fbe6c438f4e84fa56a3f246132468b5b4f1ba0fcc0251cf278338e15fdd715d5bfed18c1f98ca3cd3cc7b6f904aeeea2914a8b998dd3ae7df694c49c1742dccbf4c3472ddfe2e447959655459c11f18bddd9481eb597b887fb3f90a7d0f05224a144f87a5fdad502ea1e46c1c9f4b4154bafa4542c026296040228703bcd020202acceb772b596bf788341cceca864c8907037c39739e511b04e8ba956efa0fb5cb151ac90eb5817444f6488d593325ad4466058ba45214b965c5738f33d5591624584559ba18e89913b868619d498072e3aa1f333f5d6e3d1db88b28adf7d9350c3c383c1eda894f36bf1bb2a58c7a5e5c8b20597b71a099e46bbd3d8894877e43b0183919185b4e9f059472203979d3334c535fc4eaedebebc79bd1e423184765047a50e6dcc76ba2b23ad23511cae2edd2ad8e7f7f302226dbf6c0e4dbc8c08cda26340b9abfef1ef3333cd511295f14c87197d7890576b4076dd9686047854e67733599d96a99194aecf7b927cae2e5fa4568afc71e748dabd3bd71e6c3984f45b06a068a7c9c3a1ca7b5c245a9bb2cc7e2726e833e283430a25b6ccad55bc5b7644b44f99fedeff3c3bbf995a0387cf1e45a5684e5d1c01350d0cd2d615ffb6d1011d80ad16b75925efcbee483e4e2c0e2386e9e1b35b5a107ed97058adb60e323342989559856faeaafe5149bdcd60c113230f9923b2f654c95f986944a014198686f9c2275053c05080e3bf9fab7d46302948b152e2f2fb1ecbe71b412016b3f25ae512ad45cd096d5f284a0c2808b5eab03b4b9b2dff4d81bf234e75e30d480f39a5f9737563e31a19b14d1038296915af33e0ac0dc18e9c871e539e8772d525e5fa19afc582b1c00ae573af39fe293e16d182bbe57af5bee1c0939862ffb62e3d52a60aeb71e4db2a4a1708e75afa5f37d72cd6c0e036abcb4eb8db6515fbbdf98be95d0a6d261a9445797a8f38c3579a2f04c9f5b74dfd1ffaba2c6aa05959704b9b8cb0db30bcc360711c5afef0d1e7c2b076466dcaab104c70f3cc0cda33d7a47462c3fa3d7e34a99b2d8ff3fe5cafd27ede28b9e09b547cf955b97b0d0d4ec126957601c6982d176252be422df3366118895ca25fe27a96c9c234d484fe98634fb9e970e0d2b096f2ced5d56603505990a65363726c828aed2df0f112e0c44f058424ff5c25ae60aa2cb5fdfa289e8ebb63908365aa4e4609eae87e567f1e86d92c43992e6d505f55226fe3533f9fc9c9facff9dae02a3e3c97ca54191bebab93881c0e89b9de5bb4acd5c6fed5b1e7978803f693bfdbc125b4d08fd34fdc6aaf02444c4b06010b0eb2f15d86850a7aa5af05af438f6b7345fad4315f631bc5b017c7482e7af725a09844472f48e4de79b15284932a7e99a46ae72b187ee3faaa0f31a36726056e86eb706bc8eac04b68a3302307a157c91639f30bafc2d180670625673310a9a45a171063011e59c57c8eb67353a8ea344a87853e7b600c2b49a7a1b60a2904c0ea55951af6430667ecdfa6e90a8d2d0ed9857ba5b876cf78af190d5013d16208d2b30d02cf2c23e6ad1466f76c30d11034d5d2eca113e2764b2fb6298fc4940c16d971e28e3e6e5d0e8eea1ccb9b4b89741ed675861fc3680457ee08547f4efcf68bb6247313f8218ae3ec372e51ba8786ecae115dcff241e0", }, domain: "fonts.gstatic.com", }, // Test sniffer packet out of order { input: []string{ "c50000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a448875483ec1f071622121a49c456dc3ce16bae5f61f84ceaef9e8b71db56479845b764507dd9416e8c44b8c93406a230945eb8e484471c1b6207c9afd944fa0fee555a5c966f27ccffb4bfed37fe3936f2c84e9852c0d46c7e2e94b897fcef18c4b0b83d966aef75c0af4240325a24668bc017e0d3f69680ea5b2f59bd0b964062bc40190be86aef3ed0716a18a67057f309faaf3a040222812142a399deb72ebb330d03d59961e2ca10cf78d40886dd094368a881db261068920968f6adf7a7b1266faf8842e71840a29859e877c66e3ebc47d7fe3ee586b6512d9b0e1bea82b302647706473e68dc8209f4e9ca19f1dd25fe386e62c21d9c741e75cb8b11606739ba3de6d6325ee3a9cd1bb2b9613746140ccdaaf936eefdaa1ca7ad73d684e5d82b1ba1dd3356ca0c881f6eee72c02c8b78d02a8217a8fd972e463c77374d0fcbb761459e3ab0bb5492e516d7d4304c19c16a4bed11ea7f4e75616a26a7c81b04ffa580cce04d59825b8ed929578f9219e64bdbc6352ae6e4150a993fc3cc27ce4d66c62893866b9053bb737ac40364094b53d91e8b325b9dab5f537af04f10bf8db644897b0b03b42b1bd6c3aedfe018a6e4f6533183649f4ef6a6300383430f86e802fb4e51976d056a3c40c3b53c847b8308cfbe54dc2d20b8cdc870c73f5fc22c376c35d9a85348ca6a2288ae03dda6b97f0f502f35219e19cff3a810143289cb1f0715f8785028f887bf02c656c9cc372bdc419290f05957ad3dee82b56db352db65aca58e6fa0bd2f753160dd9e7214968c0496be1ab49f978a9252e49266939fedf542760abd653dd38b1659bcf452c753cb89e8235bcf732afcff8f524331be9b6f4a5081c81255e68c358b3444fb1d57bf5659d86b6674544fe2826ca81ee52f93a17b3291826678e488c3074c259223845e4083a413af7fc93d9992823620a8d29d321438a760293e36c4232216207060dd3ee5c4036250ede71ca9cbe335a1e068eb3ff6c10a7f1c8204750d6d0f3145014949a7b4e88a723566ee5446f960a95d9f81cc45155443da561d85a3a311df8172a1c4eb118bf27ec4b3cc4573b1ab421d96d41cc1e5557797ca68f701fe75c474527144d30b9bb00a117637f88896b0b2dcb9bb29ba144ec384b5a085e82e7387e0560a4621423c306b041ad42e84928ce23bc2a7f995ef5c21616de43be8a1657847489b32c8e364846389e7c8cae99530c499f3662a2ae7090e54958ba940b5d3eaf1333ebcecc7f06f29f68ffd97defe65017519c29d355ecb0a4b47ab08dbad8cb0cc5c86de65dfa703110c60a0c55281925018fb4ef49fe5d0132dcc86602c2ab9921a8f3451480d3e931f01c2f9a81873435bb83860128aa78dcc950fb13e416d90ea969aa92763f9caefa0fe3ef4ea82e3af4a3e717fabcc589fe8cb9bfba6810ddf7def8c1445fc0048fb07be043a628e9c920bb72c04d3b9472caafc6c14bffb854a1ba2170dda919322a6d79eab92e3a88888a224093946b87840033fe41941f780f569eaf1fdac55e36b74514d72d09823d71f48f5d5f0ceb7b6d69c5da0e0408c1b13c265d4775db6a0f952ae72bd5c277b22c4be2f2728451ce31e921c856000d20da0489103bad6a6ab4", "c60000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a44884e9e716461869ca408431e1ba92740c598aa74e9cd45706f28942f9cc64dbfd7c292cd33e82b50ae0e2e08dc478c19886718cde33e56c38517f8834d64904bf4fb1d30650caedecb9567ea8ef50157c287a2741e98a00f8e7e19e76bbb0143ac7862a49393f17ec66aa0e2c02123ffb5abcc96ccf92cd542c8f571bd7a4382ff81432d11f83796959696c38f2029db6c6a536a9ea24b74c848b95882562d74739ac95f5a069d48e8756d1a9750c7ebc23d4ee22d617b29b415b7458b3bb8106c22de3a9ace9ec689e6e00471aa33e570f7481d15911d7cf46a429cee1a416558c5e78360795d905ff1e0c81d18fcf4954131fa5b9289ed2291e122cdffd666c66209aa2cab01730739249ce293b3ba3abb31683c108bdfd51f54593f47411077e948f01105bd9bfff1578d235674e96a8b9cfdde119edaa960b84e70fd681312514151de1d5939c79abdfe4953e22be5ad3e6e242d0ce9b3f2e589ce3c768f610d4d3a32e33225d8a5ce2ad74a9b40859cdd9ea99f14fa2a7018e4b6aa6e46a0d73d46d161ec5d3b30bd55078e23987865551a605a33472931428ce222040d20c07d1ebe970e576d9d54ae688a3fe9388adda3da4d011a7cbb604f1f19d2ef1be7ef4713bfa84d4d69ffa606a08b61a1ebb99aacc4e19d0c5034642da1ce2d7d5abecc8adbbc6d7f72ff2da4ce5228ff8626509b38e17b31717c0b7821558b021ba81502d54da7e778d4526367109333383e7c67d5d5bde86bd4001fa13a703ff9259e1c2268ca8f4ed2e6c022a7466e2178bc725f59792803ba28c629e3df7696c416dc294b510920077b2d2b258fdc3506c36c42d37796c8fdb20ba797ee68fdc410325a355f6c1189aa9fc9ee220d42186677e3955cd3c844ce505cd601f04201cc390e923db2ea6407fa2fb4ca7f3f82d0a82d52697ff5ba5d4633bb0d655d7ee3348b89c9cb42870cdfe7c0c162babab4208a9a54700c5785d4134e9e33361480e3512ac8b556e11775536e90ee1270a4cb4d6bf2faa72d7e1f23ceb4fc3aded0e423b6be6a55bc25e5a99163b4f5f72ec4a24fe96f68c739d1848c92c4236a5a637d19871456b8dae671ea6ae5c16ed4fc257612a0821e6dc1cbe2ef4963a1436925dcc4e6ce528fa75e41f7721b379fae8ca09e6fb51d0c3e3ae6c19b98860ab9f74013146c6d375656dd1f530abfa64670a510390e9a54bb9a4ad19977491377c8cd743597bc156ee3f58cfcafa5a547b20852749e66fa8838c100ebde039ea25c8ec32b0c6325b793797546a095e79b9388d8e67dc6b4b3892f93ecd13e64ba4b2ad26fc810fedc374b831921531344c581927da9ba822bd625584d98c7582759ae40f01e14277a0a13d30c2c12536df698330d8aa6a3613a42c493c42692b468b4a2cc6bb6dd45684ee6115848110bf517074efd93bf212c071013f4359f140cfed17bbe10328f2026cb8ada16427122d3fc8a933119a1e3e4cfe2b95cbc73af5044cb099cf34247228972495488ebaa4696280d17665c421be5f1727c5d5b013d8aac0e9943bbbb7fbc2162a4000a306dffe3bc4425cf272f1ebb63c8e4998f867fa6b05d71a8642e29392244d4e2e2351bc149d665efe1b9519cb1b15005393f938d", }, domain: "ogads-pa.clients6.google.com", }, { input: []string{ "cf0000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f9944884c88b10f198e849dddbc1e9f9bac61f98f67f27d5452da6e2bda1f5210a145b1f1416ad2fc15e60aa00444362630650bcd0ee47999b689a40100dcacf40a4c3d74fa6293d4a5cb0487d8c76787c04dd2b47ec7718df5a2dc6942069062617b3d40a95360802957419433436c9065bda5a6156291d909a079b6d3819941368d7e17a2e97e36be829bb421b44545af47e37d7815ee1f200ca28ffd361d955ebe0484fb234a7e8a7c68ad824fd14d517fa7b35f878beebaf3dd22bd9f7a39cb7e0fd8369cdd28c05a06323be7af0b2d69ed2a2f4ea9f25d000de71bf5bd6765a20ddf81d976cff2321f1a4584ad6c4b7e9a42a6d4aa3a02b59f7d994a8e4a3070a4646e51fdf354448420ebfd0aa9118d010d019cc168f2fe5a9ff0c42e6091676be11f28a372ea97d008a1a02efd58149106cfdec7ef86f5416c4b1a408d8efba6c8d4742d781374ff0a1a8ac183bffa1345dc8e3a7cce04f66cc865f434decb912dc9e8e811eb59b80d3e39d5788639ae7c5ede73a935edb47d907725656be0522195bd2c099b0241f36664fad1543e4ae43862252662707fb424a8f5f9486b8e3779ac24bac457671ad664475d1fc9eb1de3c46f624b559742b3477953552e44f20cc1725a11ab915423fdce7cfbc8dafebc0c43d1ac3d3373ca2f0210924433c46e5fcface47a65579efaa1999d52b2632f69c33c3c63537c01be68fb679f9229f8f68c5caaa23dc4c61d3c45dee90affed984dbbfb06b2659447400b4dcbf6e574719e8d49fe0dacea9509182a42f6463138d8693a3b8d797d3bb6b0b02648829d666341373939ac41a57e90fdc2469623b6e2d772199d7c806d5998f439603c0de8413f9d29f79323ec5410b409ab8c95547ab50bb921fe0c407b7aaaf663389bdea5ba56c023dc4622d6dd9cacd8f318a6a0297d041cc6ed455d906be50dc85a25ecb32f4a565432fec9f359833be1c6a6b7b4bd119d3c4b29932eeec8d140dd467ab4d969bd23e9d2a95b92835587f32428f957b6785b8206a4834e00a3013e0b6a5855f16207268bbdf311572c54d2e6ff9c659cd02c258f494c3b168ea170c69138b63e0dde487b72576e87657befa44548b0b4e1e5a837dbbe66a559cd1df8f2151ba513930243fd2b7705bd29b183dff966224d87ffabb74017d634ab2e4b368052504a7f6bc1c62d39a29dc2dcfba683bee2039e376ff391abbd13a0b89512fd8f6a4e66051dfa04e0e1a3cb4bd56a9b17e27651873bf2ed50f65cf1cc608afaf06fe7e6238347adb66f01d1f0b9b51f0078615553cb8ff8d6786b87e19dbc44000025693c4b34cfd695601a680efdc1e7465a981b0f028cbd3dbb938789f240e39223290e34ec303ff5c78a4a637ac04dad60d744f82e96c3c9e8ed6cb0248ac73b5b3a92007edfc1277c3cc6fa1d0045c1c371820f06bedaf046dd999665cc4745ddf8934084ae02e9238acae6dea330b5798e046138f5b15011875eae72d6eb6689e56e0ac5c5d9e25dc4fc1874cf37265e68ce5b8630b84ad8dab7704474f0bfd08ac295b3a508284fb6ff201f0aee6388d0e1d5cdaaf4c20429874792109f5b8e2f3eae6c397e46a510ed829a6746e523481465f64be4e145c83d6fa6951229d3", "c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f994488828e6082edd53e228164d8862067483762ea9523c90d565b9e4b185b7805eaf8220664264e82a95164ab6cab4fb3f5e795e246e7205aca236b3c94dde0ff4fa66ce0924d654829d59c3eb690470b20c5011c739102257e9c2247dee67c0b98190d0015154d31041aadb026b8d3a828c861a15dccdab0cec8cc99b8d6c2acddbb93ab66253e87ac39016507dba42e8fa9f5d22c7f27645a02361842a59ebb2eafefd0f3b92bd9692a96b93875defcfe2796243be8861c59ce5ab03f1d65d308ae456cb9656da1f01026ef0807cc9930021b29d69b36881c3e7d70fc68799ca81922008db93c9ca4a365ee191e214d9829481fd430194ad4583a0ab2e920c25244d7d64662872b3b69ab413ccf0dfb6bf2ea9a9b93e04ed19f8a0e146613ca9d511179f80aeab40d573590d38a7c10840e3f8b9ac1bd23b0826aecabf6d1cdb2aef02deb982c2029dd6d8bc21da6c262c8116b7b383ce8c9eec69da3e16c044dd96ae08a98595d128e89dd55e6dc8eb08b8d51327278027137f60a0e1b42878f98ca898587474f6d509c3a58ff4dd7f8b10905c200cf3170bdec725ee14a1ac8ebd1022509d3e499f5e72168eac43264d7246daa0bfc81a216ca97730b7e043cad8d8a9af5c443a5d15e9a88d82b6750c740eecfd63561712a185c69532b1a18b23513d7cf871f14d164ec544f22b6a8cc77d6fae5fc6e47eb64f08617098d229da78a378d6a0864684a7978f650c7922c907f97b0ebf2be29cc834ffe995c9636b310f4a8c2c5623c3b7b533518193d226923f111da1a0e8055b9053ad7f7504d194fbc3ae2b41cef30aa099624d5e229ffb56d5883a5a09163d22455cac52e37ee0ed5367b7c3bbcd4818a46b9b363b592c53c780eeae2c8b80a1d60d296614c998a9774f76453a58bc55d1c26bb10dc321c159858d7ba2f7855ba01aadf3585632c097e5471591dcc24d87e9b76509c10e2710310e4869de710ce0f484d326be751f8e9f765a685312423f1801aefb28dfe0c8f286432356d06857101a67a432497c5849111db2792fa0ee4ffff49a9124c152bcff82da1951258f989681e4f1338357f2c9f82333f6051b188f640bf200a0a75be1d35d2301e8d3813f7ba1926a28a0df05c21413cc0c4090c1e4ba4877dca8e129876c72ab3a801b4093320f5f685120680541d97889eea5dfcaf07a7ecb00c0ba0ae193969a4cddcbd753609a5304ea88783358ab0ae005c6af27bb58b2c4282186461ea50540845e2e2a2f4efced88c8ab9cd9fb4a226a265714c77ce7b79d1a40bd00b24cbba498dafde6bbad91686cf2e13e75669234bc342218887ba910ac81680122ddd36466e7e8a983d5a0fc18a6e9a386762c32132be08abe5554e334ec7d88734cfad9a378553b71222c55f6aa114392e015dfa2bb6cb4ad241c6bca82fdd0a00eb8d6b4afac61268130dea2807a97e4c0adc0e2be39abccbe64dca5c480e09c4bebb8b598e4f60afb0e92dce710859013b1ffa9c78fbf380160f31b1e72340dea86d353ff0e95884b72e2c2c10f6eff5f36c588ee845b7bc97c3b6bec4aa879dd0eb56b838b7bc2ec6e66a5b5517908197a67566dce7df421a8daedc98848c70d1d2c39b2f3538e6f17800bee3d3", "df0000000108277148f2b916666000403a52317841946860def0d7829c06fec03ffe8b97f84e10116fafd93d1d2d39bbfda0d148778c21bb1e1667eb789b1ff70c2e3d557ed9c31570d20d", "d00000000108277148f2b91666600044cd5e860e3fa7ac4769ec75d9b7d20f19e69265939a42afd3c4248a7f5210358a044f42869567e72a05642e96ddffa67bf24ec2d966d860c6accdee01d6917c8c43d4d089d8bb63ff848b617c13fbeefafcbb049ab0822a9ca7716c95af84d019b755b145dfe43c218555d1a7e047deda7d8db352a386b2b6d03f2e7f4510f47ff4ab199348dfa81c86bea5d09d7c7af4ef3f04e99fe4e6c21d53c4335407e27913129152033f17580f97d0345c8487a7ad329dc5c97b298ec7b80fee7813f1d6f94945a44ff662a69453c2dc7ac5e8a1cb90400e63818632d7f9654f140a61280df183b3d9f9b824e53d82f2c14ea3de89befdc79b84a4a3eb659a41db25622add94f2ad4b0d5977f1091aae0a4b83c7b41bca61c6c8d807ef02a8ce6240b76d442559a8b338b39418d27e99aff38840fc79a20995af65b3bbe1e3177074079a47578c51655a4016363364fd2c108d384e602deebd022da3c814549cd57d73c5bfc20e279045e2ad436fbd7e7c9e1985f0ec2f422e310e7aa8cfc48e637f9ac61d06d6482cb40b4376ff3c7abff3c3c26634689ae16d704bab1343d6413fc7b6c076eb0454eda2e0d1e077db40c922ebba6b0b1fa814e3ba76d8d6c4289abbd655f0cf5968eb2aba7131680b44da8910056a76647a6dfea95f27364a7ce694b8fbe19ebcb2a47e7350d33a36f7f5ca67af5e934f449125f4aae870a5b23b4370680ee02b194784d5d188ecdf58ae5454221406bde0ddd3e50d3363a564d6ca9fe0fb57d4df8716cb430cf553be573aa690e5645075ec74edd38cf23215bd50bcda0639dfbbe08dd6c476249e35da819ea6ccef808911b0eef6efaa4947244472795bc071d7154ed87e4a43575b3d61a551fcfccfb7ca3edaee9324f33f54dc9809747e59e24e79f256e8e72f01b8647f71c4b9dc260715fd9d83d3dbd9e124c432c04b3398e74efa3869fe129e368c15b6ca234a243fcac675adcc1db247e3f8485ac4a78f4a1ce2db3b437a1960b02f0c227901d165dcc05abdb3929a80dff2eaf72816185d4af4e28eea05430b736ddd2962e03ec64fa48649dd610e0e221c48f781b45cd9963c176126110e662369874e6a55f28039a23484c5c53714fc2d2030b48f1c895102ca9ad8acae1ec4eb0ae8d8bde31cd74fc515930078d22ad07dc3c7221ddbc4027c746207fa038b31080714091459c9a66ba4f5912d8d3905d3a9a47e4d8829a8110c96c0c9c81291c7985073808814109364df15b04520dc07e8d67cafcda71f0ca59423df5fadae92417a8661b3cdbbf6b1059780fb8b43eb4dcdacf731bb8db26294f978f6be7506b87d17a95367cdb83000565a4986e66dd60d0851f9b593d68790f8097434f62ea7a7396017c3c84754845d3a97f028cf8697d929a2826451653ccf84aba4d2f40fa530b258c13f08c6523c3c02d9669fd46b6a51f20ad323857d767150e3530a66bf88976dbadf99aeea549254c07e11e14085979b60f3b7e1728a4a2d7a35b0377c6501ae7d1d4bba338fb51a17ca8f7e698bd70cd01e8f30edf3e83591a2eb0038811e347bfcfab159b0d1ff6153e0f9ee4c129cfb7687e30b82eed74130c466eee06506dde50805b58c2acccd4cf4b2cc86c52fa2af602a8a7064eb9d90e1c568373b19e43ef4e7c1e4e1c9a58ccedf80a02a46ed64e68e72d4e75c7436e2bc0ba59f95a00456e5680af9e6cf4bf3a6d302ddaf8847cfd5ea606797", "d30000000108277148f2b91666600043d24b66b2531ed9f9c13b07b2654186b0410a608592fdf728479734933197ec06a1cde860f36b3170fb2a9c85c62a7867ba6520dcb2d0ab2f6a484d9ebf8237d7a6f3c1fb16c1e0458ccf20e6d1b298a7530cea42636166027d92812915e76fbcc436a5e414147672dd7b0d19ff24513800e63cd86984f1c93ef1430bb848d37830eed61675d7c9999b92c6e5796d384554c74dd5a163de341ab309d6b0cb028aa08e56d79c60980d4a49a1c095456ca119fc3f04e496c93a084d017f60c6e031d6e9ad2e4fa699bb4b0c92fdcb44131129db0d30ce9efb740d3db0339127d9bdd1d4f677b1cb532a33647851ba9bb20bd8d6aa593271a85c3a9dc9835065663e61faa8dc6af209a0caf183d0fda3d4839d40edd5659dd053778642db8fba21f1f793e45c5c517e68bbef8543e3a727743c7bf87d047d441d13226b9021fac56904872774cf6768dc91db8ea489a244500e9e527acdc0088437357acf9397b014e66fef2db1248f9c6a578af07d7a02b1356fee02e27b8207e57633fa7bfd87ccd382e368c14b946aea780fcbe696d6e4fa3aa589184e104177db2fc3d91d4af120d9da3bdad021d003796b8261b590d8113f995dc1db4fac1c62cb68370d41cc87c982815017ae2143d5a469b742d019e5556d813877fec9d021cb37f80e5987d9f743c2b39093a34f6654164a8185a5caefbbef8ea17f62f6801a3fd89fae333c878cec9b25d10dfee2abca65d7c909ad2e4f11736d13b1642df4c5a0761f8f29f35f37def9ed327f4a9d8e53269fa6c7cedd0f4fd67d6cde81934e291d9fca695cc9745890cb54503e29e09f4a30f80e2f574bbbeedb7d20481c583d8362d22b2dbec09494095a043cdae283e86f905d8807f7b7c0f06ce968487bbca1e20b87245b68f24537a7c7e768c838f1bf26650afdabec2c0bb9736b345473f279c9b73ecf0d2c4aea49330ecfef0949ef7cb81861b05950ec0772db856365b136ba75d5509c01d7a970c84ebc77d8d5c3ceae1ef5f3079afc7d78965ffa3bc4c64ef1b4718ffb488a571528c83b615c43022616bb4c494c838b556df5ede711a688b0315c1ce6e2892247df582b7c3f2b06cac0bd8d670e2b581f074750596ba162189060b8af3dfc650ba3b45932edc4f94f08741d3072bfd1ef8159b27a7f3673a4fc504304c12116e3c2d7636c663c9fa1b2f5571be88769f33ccb94a09abd9c5a7dc8a8c2031bb2bc256b84aeb68a9abf7673151cec41b48bdd74f395a46acf30dae43e060e596bb2e739274210701ee9bb6cc3ff81ace751e375a01f17b3c5cc5f1234c488d69611bb27f6e3ee17e3c3843ebe4a280d6aa8ef017058a872810a437f85331adb3cb8d382650897b1b1589ee6", "dd0000000108277148f2b9166660004053972df1beb451f73eb070e33ed63f681eb9b7e1e03f20baff3f54157598c7dd90a0de49850a3ccd6eb1b1cfc9dc6d3ba9ed1c0a19c69bf433da300d3cecc4ef151c44a721d680e3e3aaaf3eefec23091c5fde22", "c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f99448825ee873013b006b2a6c87c7581c7117bfeb4ec3d68405a68d9488f6d58474dd16539677e869812ead055e70d655a660062e17083995c0dbecd565c79800b11c8ca0c351ecffa61e707d62d443b3810bc60d4ef87aa99b979ff55ee1ea46b65436c15534e5315113138aed6daa9f04d3050d77a7e379c83b948d3797177c1793e59b2555423bd52595d93e293ea8ffa3c428c6dbba4e202d76933caf6a5609b0a4aa6cf4fd2aadb6505382381ef2d5b33efc43eba24c84b7805baf2ddab44a50180e5e6f2a31f9ea8089aef562d3b578a799d61befec99c016fadec3363f68a1be4ca1e13e8bdd2809a1dacc41134663e22f21978167c5ee8ef49652ae152fc6c1bcf52109cd3076cdd599cb43261941de7aed148d7d3e956cd615549a9647496f43f998daad4c841cc40ce1501fbfc152b957c94be558f6743061e312d746137db2ae6a44e181587dbf6b0d9508cef4aefd99ea5d3369898bd4c3df5e95ac89eaaba54019ffe0402b8f567c91b9371e80c621c67d3c831331acc063892bbd8a81cfc0498e78474b11e8c05dd8f540c449505342ae95f6281940aae973db35b8e31ff801f6bc8975f592538881ae9cc4cedcbdb39a784a9fe962a1f12be51c11b91d4dedf649bb5672dec8e03db97b0d69fce36edfbecb6836644bad1ab8e6d4e13644d9c3476db0e8a8eb4b5a5c32f7a5604c8e19700c53602839478531579cb4c4bb5cc969cf482f325dd837629318baf128920d9978e23296d7016e6c05c954f95881b4f9f7e43bcea393951e91af0e4a671400dc435bd2a1616c60618df2476d0ece060dbbda11e751e256956a0dbcd7e4a8d6d85a3319f22a2c5f26dad50e82f70f3dd91feff19c775aa60499a3b7daa57e344c07c3787e99d53303488801d2b17cdbfdee61ea3fc473f6c146f06eb60d70594a59e0ed79cee6ca4a5f78b037637ddab69fb8522c0f7bf37aa7f59cc7fa659e759db69966455944975cd22a1a1355f35a589a4978c8f3272e1c4f6793288a00ab879299aa6ad02d966e3dc67cee0c808b1a046458cff9bdac25a4071eb10038a6389a0ef7233003641bd4ee1efad0e9b2f693396a89ca0db3c05b6abfed3b246eb1b23a6b77e8b486f26d9c3dde9dd6f3637a8115940ed2ca762ca6320609f61c37ffc9c3f2f7a0f27edc9891c2eeac49ba258a0d09c35c4fe1dc52d4d9319aa9b1a271a5d8d2d3a75fef4d59fb04679ba526aecbd19d73f72fee537630444326e2543ce564c669bf378499738385dda9ac63521a1b91f580d0737a7326009f0ff0dcb05aa8b86222c934d9ddb4628e30b6e12ae370154ab39c605431b4c40683592afcfd6fccf35df9fe5850442595d24be3d9f4298bf3d541f09e7e71f552c88eed9642df46953622d5aea05b5060325304ec81c0447ac95b90f9da4359e3286938f06aea3d45030cb836be15b1c65e3edf44cbcfe2f01ef8d7209c69d7c81334c866ebee50e418a28336cea1982069b4df090eab81303761d1af337e083f1e0ad1440a02ef1eefb03506c39d2377807e335ee64bdb76527f786223cee5233299eda9fcb1d38f19c34480f790a328b0735f80908e3aa70086df828d56b6c79516f71a24c9d94f60335f86e9d29c0c5d3872b", "dd0000000108277148f2b916666000406672db10ab41db38c01f7021709bac4d1659d872623eb5852b12b494535d13779a88d37e9685da572f6b2de35793a519a457493456ac4ee242933cf92d783f783656899c31832274bf1c26d24720d9d8ecfec598e19c58a478d2991dfc1cda3000f7bd7bd17e80", "d60000000108277148f2b916666000404ed98b1b4ac35c0c0ef18c88adf08a6701ccb0876ea75aac8c128349936fa3cb6728e4e58de8673dd7dc8457b092957f26bc8194233bb81c7e78127844f9b833f196dc46c5cb4064c773f3c6e0bc73", }, domain: "www.google.com", }, // invalid packet { input: []string{"00000000000000000000"}, invalid: true, }, } for _, test := range cases { data, host, err := testQuicSniffer(test.input, true) assert.NoError(t, err) assert.Equal(t, test.domain, data) if test.invalid { assert.Equal(t, fakeHost, host) } else { assert.Equal(t, test.domain, host) } data, host, err = testQuicSniffer(test.input, false) assert.NoError(t, err) assert.Equal(t, test.domain, data) if test.invalid { assert.Equal(t, fakeHost, host) } else { assert.Equal(t, test.domain, host) } } } func TestTLSHeaders(t *testing.T) { cases := []struct { input []byte domain string err bool }{ { input: []byte{ 0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00, 0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe, 0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4, 0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36, 0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43, 0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0xaa, 0xaa, 0x00, 0x01, 0x00, }, domain: "c.s-microsoft.com", err: false, }, { input: []byte{ 0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00, 0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca, 0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5, 0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e, 0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca, 0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00, 0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74, 0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85, 0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea, 0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30, 0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74, 0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00, 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a, 0x00, 0x01, 0x00, }, domain: "www07.clicktale.net", err: false, }, { input: []byte{ 0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1, 0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6, 0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d, 0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84, 0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08, 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e, 0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08, 0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c, 0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01, 0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72, }, domain: "dogfish", err: false, }, { input: []byte{ 0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00, 0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee, 0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14, 0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62, 0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45, 0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 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, 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00, 0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30, 0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, 0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02, 0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f, 0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e, 0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f, 0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d, 0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36, }, domain: "10.42.0.243", err: false, }, } for _, test := range cases { input := bytes.Clone(test.input) domain, err := SniffTLS(test.input) if test.err { if err == nil { t.Errorf("Exepct error but nil in test %v", test) } } else { if err != nil { t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) } if *domain != test.domain { t.Error("expect domain ", test.domain, " but got ", domain) } } assert.Equal(t, input, test.input) } } ================================================ FILE: core/Clash.Meta/component/sniffer/tls_sniffer.go ================================================ package sniffer import ( "encoding/binary" "errors" "fmt" "strings" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/sniffer" ) var ( errNotTLS = errors.New("not TLS header") errNotClientHello = errors.New("not client hello") ) type errNeedAtLeastData struct { length int err error } func (e *errNeedAtLeastData) Error() string { return fmt.Sprintf("%v, need at least length: %d", e.err, e.length) } func (e *errNeedAtLeastData) Unwrap() error { return e.err } var _ sniffer.Sniffer = (*TLSSniffer)(nil) type TLSSniffer struct { *BaseSniffer } func NewTLSSniffer(snifferConfig SnifferConfig) (*TLSSniffer, error) { ports := snifferConfig.Ports if len(ports) == 0 { ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)} } return &TLSSniffer{ BaseSniffer: NewBaseSniffer(ports, C.TCP), }, nil } func (tls *TLSSniffer) Protocol() string { return "tls" } func (tls *TLSSniffer) SupportNetwork() C.NetWork { return C.TCP } func (tls *TLSSniffer) SniffData(bytes []byte) (string, error) { domain, err := SniffTLS(bytes) if err == nil { return *domain, nil } else { return "", err } } func IsValidTLSVersion(major, minor byte) bool { return major == 3 } // ReadClientHello returns server name (if any) from TLS client hello message. // https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300 func ReadClientHello(data []byte) (*string, error) { if len(data) < 42 { return nil, ErrNoClue } sessionIDLen := int(data[38]) if sessionIDLen > 32 || len(data) < 39+sessionIDLen { return nil, ErrNoClue } data = data[39+sessionIDLen:] if len(data) < 2 { return nil, ErrNoClue } // cipherSuiteLen is the number of bytes of cipher suite numbers. Since // they are uint16s, the number must be even. cipherSuiteLen := int(data[0])<<8 | int(data[1]) if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { return nil, errNotClientHello } data = data[2+cipherSuiteLen:] if len(data) < 1 { return nil, ErrNoClue } compressionMethodsLen := int(data[0]) if len(data) < 1+compressionMethodsLen { return nil, ErrNoClue } data = data[1+compressionMethodsLen:] if len(data) == 0 { return nil, errNotClientHello } if len(data) < 2 { return nil, errNotClientHello } extensionsLength := int(data[0])<<8 | int(data[1]) data = data[2:] if extensionsLength != len(data) { return nil, errNotClientHello } for len(data) != 0 { if len(data) < 4 { return nil, errNotClientHello } extension := uint16(data[0])<<8 | uint16(data[1]) length := int(data[2])<<8 | int(data[3]) data = data[4:] if len(data) < length { return nil, errNotClientHello } if extension == 0x00 { /* extensionServerName */ d := data[:length] if len(d) < 2 { return nil, errNotClientHello } namesLen := int(d[0])<<8 | int(d[1]) d = d[2:] if len(d) != namesLen { return nil, errNotClientHello } for len(d) > 0 { if len(d) < 3 { return nil, errNotClientHello } nameType := d[0] nameLen := int(d[1])<<8 | int(d[2]) d = d[3:] if len(d) < nameLen { return nil, errNotClientHello } if nameType == 0 { serverName := string(d[:nameLen]) // An SNI value may not include a // trailing dot. See // https://tools.ietf.org/html/rfc6066#section-3. if strings.HasSuffix(serverName, ".") { return nil, errNotClientHello } return &serverName, nil } d = d[nameLen:] } } data = data[length:] } return nil, errNotTLS } func SniffTLS(b []byte) (*string, error) { if len(b) < 5 { return nil, ErrNoClue } if b[0] != 0x16 /* TLS Handshake */ { return nil, errNotTLS } if !IsValidTLSVersion(b[1], b[2]) { return nil, errNotTLS } headerLen := int(binary.BigEndian.Uint16(b[3:5])) if 5+headerLen > len(b) { return nil, &errNeedAtLeastData{ length: 5 + headerLen, err: ErrNoClue, } } domain, err := ReadClientHello(b[5 : 5+headerLen]) if err == nil { return domain, nil } return nil, err } ================================================ FILE: core/Clash.Meta/component/tls/httpserver.go ================================================ package tls import ( "context" "net" "runtime/debug" "time" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/log" "github.com/metacubex/http" ) func extractTlsHandshakeTimeoutFromServer(s *http.Server) time.Duration { var ret time.Duration for _, v := range [...]time.Duration{ s.ReadHeaderTimeout, s.ReadTimeout, s.WriteTimeout, } { if v <= 0 { continue } if ret == 0 || v < ret { ret = v } } return ret } // NewListenerForHttps returns a net.Listener for (*http.Server).Serve() // the "func (c *conn) serve(ctx context.Context)" in http\server.go // only do tls handshake and check NegotiatedProtocol with std's *tls.Conn // so we do the same logic to let http2 (not h2c) work fine func NewListenerForHttps(l net.Listener, httpServer *http.Server, tlsConfig *Config) net.Listener { http2Server := &http.Http2Server{} _ = http.Http2ConfigureServer(httpServer, http2Server) return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) { c := Server(conn, tlsConfig) tlsTO := extractTlsHandshakeTimeoutFromServer(httpServer) if tlsTO > 0 { dl := time.Now().Add(tlsTO) _ = conn.SetReadDeadline(dl) _ = conn.SetWriteDeadline(dl) } err := c.HandshakeContext(ctx) if err != nil { return nil, err } // Restore Conn-level deadlines. if tlsTO > 0 { _ = conn.SetReadDeadline(time.Time{}) _ = conn.SetWriteDeadline(time.Time{}) } if c.ConnectionState().NegotiatedProtocol == http.Http2NextProtoTLS { http2Server.ServeConn(c, &http.Http2ServeConnOpts{BaseConfig: httpServer}) return nil, net.ErrClosed } return c, nil }, func(a any) { stack := debug.Stack() log.Errorln("https server panic: %s\n%s", a, stack) }) } ================================================ FILE: core/Clash.Meta/component/tls/reality.go ================================================ package tls import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/ecdh" "crypto/ed25519" "crypto/hmac" "crypto/sha256" "crypto/sha512" "crypto/x509" "encoding/binary" "errors" "net" "strings" "time" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/http" "github.com/metacubex/randv2" utls "github.com/metacubex/utls" "golang.org/x/crypto/hkdf" ) const RealityMaxShortIDLen = 8 type RealityConfig struct { PublicKey *ecdh.PublicKey ShortID [RealityMaxShortIDLen]byte SupportX25519MLKEM768 bool } func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, serverName string, realityConfig *RealityConfig) (net.Conn, error) { for retry := 0; ; retry++ { verifier := &realityVerifier{ serverName: serverName, } uConfig := &utls.Config{ Time: ntp.Now, ServerName: serverName, InsecureSkipVerify: true, SessionTicketsDisabled: true, VerifyConnection: verifier.VerifyConnection, } uConn := utls.UClient(conn, uConfig, fingerprint) verifier.UConn = uConn err := uConn.BuildHandshakeState() if err != nil { return nil, err } if !realityConfig.SupportX25519MLKEM768 { // for X25519MLKEM768 does not work properly with the old reality server err = BuildRemovedX25519MLKEM768HandshakeState(uConn) if err != nil { return nil, err } } hello := uConn.HandshakeState.Hello rawSessionID := hello.Raw[39 : 39+32] // the location of session ID for i := range rawSessionID { // https://github.com/golang/go/issues/5373 rawSessionID[i] = 0 } binary.BigEndian.PutUint64(hello.SessionId, uint64(ntp.Now().Unix())) copy(hello.SessionId[8:], realityConfig.ShortID[:]) hello.SessionId[0] = 1 hello.SessionId[1] = 8 hello.SessionId[2] = 2 //log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16]) keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys if keyShareKeys == nil { // WTF??? if retry > 2 { return nil, errors.New("nil keyShareKeys") } continue // retry } ecdheKey := keyShareKeys.Ecdhe if ecdheKey == nil { ecdheKey = keyShareKeys.MlkemEcdhe } if ecdheKey == nil { // WTF??? if retry > 2 { return nil, errors.New("nil ecdheKey") } continue // retry } authKey, err := ecdheKey.ECDH(realityConfig.PublicKey) if err != nil { return nil, err } if authKey == nil { return nil, errors.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) aeadCipher, _ := cipher.NewGCM(aesBlock) aeadCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) copy(hello.Raw[39:], hello.SessionId) //log.Debugln("REALITY hello.sessionId: %v", hello.SessionId) //log.Debugln("REALITY uConn.AuthKey: %v", authKey) err = uConn.HandshakeContext(ctx) if err != nil { return nil, err } log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher) if !verifier.verified { go realityClientFallback(uConn, uConfig.ServerName, fingerprint) return nil, errors.New("REALITY authentication failed") } return uConn, nil } } func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) { defer uConn.Close() // use h2c mode to disallow the net/http fallback to http1.1 protocols := new(http.Protocols) protocols.SetUnencryptedHTTP2(true) client := http.Client{ Transport: &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return uConn, nil }, Protocols: protocols, }, } request, err := http.NewRequest("GET", "https://"+serverName, nil) if err != nil { return } request.Header.Set("User-Agent", fingerprint.Client) request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", randv2.IntN(32)+30)}) response, err := client.Do(request) if err != nil { return } //_, _ = io.Copy(io.Discard, response.Body) time.Sleep(time.Duration(5+randv2.IntN(10)) * time.Second) response.Body.Close() client.CloseIdleConnections() } type realityVerifier struct { *utls.UConn serverName string authKey []byte verified bool } func (c *realityVerifier) VerifyConnection(state utls.ConnectionState) error { log.Debugln("REALITY localAddr: %v is using X25519MLKEM768 for TLS' communication: %v", c.RemoteAddr(), c.HandshakeState.ServerHello.ServerShare.Group == utls.X25519MLKEM768) certs := state.PeerCertificates 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(), CurrentTime: ntp.Now(), } for _, cert := range certs[1:] { opts.Intermediates.AddCert(cert) } if _, err := certs[0].Verify(opts); err != nil { return err } return nil } ================================================ FILE: core/Clash.Meta/component/tls/utls.go ================================================ package tls import ( "context" "net" "reflect" "unsafe" "github.com/metacubex/mihomo/common/once" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/log" "github.com/metacubex/tls" utls "github.com/metacubex/utls" "github.com/mroth/weightedrand/v2" "golang.org/x/exp/slices" ) type Conn = utls.Conn type UConn = utls.UConn type UClientHelloID = utls.ClientHelloID const VersionTLS12 = utls.VersionTLS12 const VersionTLS13 = utls.VersionTLS13 func Client(c net.Conn, config *utls.Config) *Conn { return utls.Client(c, config) } func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn { return utls.UClient(c, config, fingerprint) } func Server(c net.Conn, config *utls.Config) *Conn { return utls.Server(c, config) } func NewListener(inner net.Listener, config *Config) net.Listener { return utls.NewListener(inner, config) } func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) { if len(clientFingerprint) == 0 { clientFingerprint = globalFingerprint } if len(clientFingerprint) == 0 || clientFingerprint == "none" { return UClientHelloID{}, false } if clientFingerprint == "random" { fingerprint := randomFingerprint() log.Debugln("use initial random HelloID:%s", fingerprint.Client) return fingerprint, true } if fingerprint, ok := fingerprints[clientFingerprint]; ok { log.Debugln("use specified fingerprint:%s", fingerprint.Client) return fingerprint, true } else { log.Warnln("wrong clientFingerprint:%s", clientFingerprint) return UClientHelloID{}, false } } var randomFingerprint = once.OnceValue(func() UClientHelloID { chooser, _ := weightedrand.NewChooser( weightedrand.NewChoice("chrome", 6), weightedrand.NewChoice("safari", 3), weightedrand.NewChoice("ios", 2), weightedrand.NewChoice("firefox", 1), ) initClient := chooser.Pick() log.Debugln("initial random HelloID:%s", initClient) fingerprint, ok := fingerprints[initClient] if !ok { log.Warnln("error in initial random HelloID:%s", initClient) } return fingerprint }) var fingerprints = map[string]UClientHelloID{ "chrome": utls.HelloChrome_Auto, "firefox": utls.HelloFirefox_Auto, "safari": utls.HelloSafari_Auto, "ios": utls.HelloIOS_Auto, "android": utls.HelloAndroid_11_OkHttp, "edge": utls.HelloEdge_Auto, "360": utls.Hello360_Auto, "qq": utls.HelloQQ_Auto, "random": {}, // classical fingerprints without X25519MLKEM768 "chrome120": utls.HelloChrome_120, "firefox120": utls.HelloFirefox_120, "safari16": utls.HelloSafari_16_0, // deprecated fingerprints should not be used "chrome_psk": utls.HelloChrome_100_PSK, "chrome_psk_shuffle": utls.HelloChrome_106_Shuffle, "chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf, "chrome_pq": utls.HelloChrome_115_PQ, "chrome_pq_psk": utls.HelloChrome_115_PQ_PSK, "randomized": utls.HelloRandomized, } func init() { weights := utls.DefaultWeights weights.TLSVersMax_Set_VersionTLS13 = 1 weights.FirstKeyShare_Set_CurveP256 = 0 randomized := utls.HelloRandomized randomized.Seed, _ = utls.NewPRNGSeed() randomized.Weights = &weights fingerprints["randomized"] = randomized } type Certificate = utls.Certificate func UCertificate(it tls.Certificate) utls.Certificate { return utls.Certificate{ Certificate: it.Certificate, PrivateKey: it.PrivateKey, SupportedSignatureAlgorithms: utils.Map(it.SupportedSignatureAlgorithms, func(it tls.SignatureScheme) utls.SignatureScheme { return utls.SignatureScheme(it) }), OCSPStaple: it.OCSPStaple, SignedCertificateTimestamps: it.SignedCertificateTimestamps, Leaf: it.Leaf, } } type EncryptedClientHelloKey = utls.EncryptedClientHelloKey func UEncryptedClientHelloKey(it tls.EncryptedClientHelloKey) utls.EncryptedClientHelloKey { return utls.EncryptedClientHelloKey{ Config: it.Config, PrivateKey: it.PrivateKey, SendAsRetry: it.SendAsRetry, } } type ConnectionState = utls.ConnectionState type Config = utls.Config var tlsCertificateRequestInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.CertificateRequestInfo)(nil)).Elem().FieldByName("ctx")).Offset var tlsClientHelloInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.ClientHelloInfo)(nil)).Elem().FieldByName("ctx")).Offset var tlsConnectionStateEkmOffset = utils.MustOK(reflect.TypeOf((*tls.ConnectionState)(nil)).Elem().FieldByName("ekm")).Offset var utlsConnectionStateEkmOffset = utils.MustOK(reflect.TypeOf((*utls.ConnectionState)(nil)).Elem().FieldByName("ekm")).Offset func tlsConnectionState(state utls.ConnectionState) (tlsState tls.ConnectionState) { tlsState = tls.ConnectionState{ Version: state.Version, HandshakeComplete: state.HandshakeComplete, DidResume: state.DidResume, CipherSuite: state.CipherSuite, //CurveID: state.CurveID, NegotiatedProtocol: state.NegotiatedProtocol, NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, ServerName: state.ServerName, PeerCertificates: state.PeerCertificates, VerifiedChains: state.VerifiedChains, SignedCertificateTimestamps: state.SignedCertificateTimestamps, OCSPResponse: state.OCSPResponse, TLSUnique: state.TLSUnique, ECHAccepted: state.ECHAccepted, //HelloRetryRequest: state.HelloRetryRequest, } // The layout of map, chan, and func types is equivalent to *T. // state.ekm is a func(label string, context []byte, length int) ([]byte, error) *(*unsafe.Pointer)(unsafe.Add(unsafe.Pointer(&tlsState), tlsConnectionStateEkmOffset)) = *(*unsafe.Pointer)(unsafe.Add(unsafe.Pointer(&state), utlsConnectionStateEkmOffset)) return } func UConfig(config *tls.Config) *utls.Config { cfg := &utls.Config{ Rand: config.Rand, Time: config.Time, Certificates: utils.Map(config.Certificates, UCertificate), VerifyPeerCertificate: config.VerifyPeerCertificate, RootCAs: config.RootCAs, NextProtos: config.NextProtos, ServerName: config.ServerName, ClientAuth: utls.ClientAuthType(config.ClientAuth), ClientCAs: config.ClientCAs, InsecureSkipVerify: config.InsecureSkipVerify, CipherSuites: config.CipherSuites, MinVersion: config.MinVersion, MaxVersion: config.MaxVersion, CurvePreferences: utils.Map(config.CurvePreferences, func(it tls.CurveID) utls.CurveID { return utls.CurveID(it) }), SessionTicketsDisabled: config.SessionTicketsDisabled, Renegotiation: utls.RenegotiationSupport(config.Renegotiation), KeyLogWriter: config.KeyLogWriter, } if config.GetClientCertificate != nil { cfg.GetClientCertificate = func(info *utls.CertificateRequestInfo) (*utls.Certificate, error) { tlsInfo := &tls.CertificateRequestInfo{ AcceptableCAs: info.AcceptableCAs, SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme { return tls.SignatureScheme(it) }), Version: info.Version, } *(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsCertificateRequestInfoCtxOffset)) = info.Context() // for tlsInfo.ctx cert, err := config.GetClientCertificate(tlsInfo) if err != nil { return nil, err } uCert := UCertificate(*cert) return &uCert, err } } if config.GetCertificate != nil { cfg.GetCertificate = func(info *utls.ClientHelloInfo) (*utls.Certificate, error) { tlsInfo := &tls.ClientHelloInfo{ CipherSuites: info.CipherSuites, ServerName: info.ServerName, SupportedCurves: utils.Map(info.SupportedCurves, func(it utls.CurveID) tls.CurveID { return tls.CurveID(it) }), SupportedPoints: info.SupportedPoints, SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme { return tls.SignatureScheme(it) }), SupportedProtos: info.SupportedProtos, SupportedVersions: info.SupportedVersions, Extensions: info.Extensions, Conn: info.Conn, //HelloRetryRequest: info.HelloRetryRequest, } *(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsClientHelloInfoCtxOffset)) = info.Context() // for tlsInfo.ctx cert, err := config.GetCertificate(tlsInfo) if err != nil { return nil, err } uCert := UCertificate(*cert) return &uCert, err } } if config.VerifyConnection != nil { cfg.VerifyConnection = func(state utls.ConnectionState) error { return config.VerifyConnection(tlsConnectionState(state)) } } config.EncryptedClientHelloConfigList = cfg.EncryptedClientHelloConfigList if config.EncryptedClientHelloRejectionVerify != nil { cfg.EncryptedClientHelloRejectionVerify = func(state utls.ConnectionState) error { return config.EncryptedClientHelloRejectionVerify(tlsConnectionState(state)) } } //cfg.GetEncryptedClientHelloKeys = cfg.EncryptedClientHelloKeys = utils.Map(config.EncryptedClientHelloKeys, UEncryptedClientHelloKey) return cfg } // BuildWebsocketHandshakeState it will only send http/1.1 in its ALPN. // Copy from https://github.com/XTLS/Xray-core/blob/main/transport/internet/tls/tls.go func BuildWebsocketHandshakeState(c *UConn) error { // Build the handshake state. This will apply every variable of the TLS of the // fingerprint in the UConn if err := c.BuildHandshakeState(); err != nil { return err } // Iterate over extensions and check for utls.ALPNExtension hasALPNExtension := false for _, extension := range c.Extensions { if alpn, ok := extension.(*utls.ALPNExtension); ok { hasALPNExtension = true alpn.AlpnProtocols = []string{"http/1.1"} break } } if !hasALPNExtension { // Append extension if doesn't exists c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}) } // Rebuild the client hello if err := c.BuildHandshakeState(); err != nil { return err } return nil } func BuildRemovedX25519MLKEM768HandshakeState(c *UConn) error { // Build the handshake state. This will apply every variable of the TLS of the // fingerprint in the UConn if err := c.BuildHandshakeState(); err != nil { return err } // Iterate over extensions and check for _, extension := range c.Extensions { if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool { return curveID == utls.X25519MLKEM768 }) } if ks, ok := extension.(*utls.KeyShareExtension); ok { ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool { return share.Group == utls.X25519MLKEM768 }) } } // Rebuild the client hello if err := c.BuildHandshakeState(); err != nil { return err } return nil } func GetTLSConnectionState(conn net.Conn) (tlsState tls.ConnectionState) { switch tlsConn := conn.(type) { case interface{ ConnectionState() tls.ConnectionState }: state := tlsConn.ConnectionState() return state case interface{ ConnectionState() utls.ConnectionState }: state := tlsConn.ConnectionState() return tlsConnectionState(state) } return } var globalFingerprint string func SetGlobalFingerprint(fingerprint string) { globalFingerprint = fingerprint } func GetGlobalFingerprint() string { return globalFingerprint } ================================================ FILE: core/Clash.Meta/component/trie/domain.go ================================================ package trie import ( "errors" "strings" "unicode" "unicode/utf8" ) const ( wildcard = "*" dotWildcard = "" complexWildcard = "+" domainStep = "." ) // ErrInvalidDomain means insert domain is invalid var ErrInvalidDomain = errors.New("invalid domain") // DomainTrie contains the main logic for adding and searching nodes for domain segments. // support wildcard domain (e.g *.google.com) type DomainTrie[T any] struct { root *Node[T] } func ValidAndSplitDomain(domain string) ([]string, bool) { if domain != "" && domain[len(domain)-1] == '.' { return nil, false } if domain != "" { if r, _ := utf8.DecodeRuneInString(domain); unicode.IsSpace(r) { return nil, false } if r, _ := utf8.DecodeLastRuneInString(domain); unicode.IsSpace(r) { return nil, false } } domain = strings.ToLower(domain) parts := strings.Split(domain, domainStep) if len(parts) == 1 { if parts[0] == "" { return nil, false } return parts, true } for _, part := range parts[1:] { if part == "" { return nil, false } } return parts, true } // Insert adds a node to the trie. // Support // 1. www.example.com // 2. *.example.com // 3. subdomain.*.example.com // 4. .example.com // 5. +.example.com func (t *DomainTrie[T]) Insert(domain string, data T) error { parts, valid := ValidAndSplitDomain(domain) if !valid { return ErrInvalidDomain } if parts[0] == complexWildcard { t.insert(parts[1:], data) parts[0] = dotWildcard t.insert(parts, data) } else { t.insert(parts, data) } return nil } func (t *DomainTrie[T]) insert(parts []string, data T) { node := t.root // reverse storage domain part to save space for i := len(parts) - 1; i >= 0; i-- { part := parts[i] node = node.getOrNewChild(part) } node.setData(data) } // Search is the most important part of the Trie. // Priority as: // 1. static part // 2. wildcard domain // 2. dot wildcard domain func (t *DomainTrie[T]) Search(domain string) *Node[T] { parts, valid := ValidAndSplitDomain(domain) if !valid || parts[0] == "" { return nil } n := t.search(t.root, parts) if n.isEmpty() { return nil } return n } func (t *DomainTrie[T]) search(node *Node[T], parts []string) *Node[T] { if len(parts) == 0 { return node } if c := node.getChild(parts[len(parts)-1]); c != nil { if n := t.search(c, parts[:len(parts)-1]); !n.isEmpty() { return n } } if c := node.getChild(wildcard); c != nil { if n := t.search(c, parts[:len(parts)-1]); !n.isEmpty() { return n } } return node.getChild(dotWildcard) } func (t *DomainTrie[T]) Optimize() { t.root.optimize() } func (t *DomainTrie[T]) Foreach(fn func(domain string, data T) bool) { for key, data := range t.root.getChildren() { recursion([]string{key}, data, fn) if !data.isEmpty() { if !fn(joinDomain([]string{key}), data.data) { return } } } } func (t *DomainTrie[T]) IsEmpty() bool { if t == nil || t.root == nil { return true } return len(t.root.getChildren()) == 0 } func recursion[T any](items []string, node *Node[T], fn func(domain string, data T) bool) bool { for key, data := range node.getChildren() { newItems := append([]string{key}, items...) if !data.isEmpty() { domain := joinDomain(newItems) if domain[0] == domainStepByte { domain = complexWildcard + domain } if !fn(domain, data.Data()) { return false } } if !recursion(newItems, data, fn) { return false } } return true } func joinDomain(items []string) string { return strings.Join(items, domainStep) } // New returns a new, empty Trie. func New[T any]() *DomainTrie[T] { return &DomainTrie[T]{root: newNode[T]()} } ================================================ FILE: core/Clash.Meta/component/trie/domain_set.go ================================================ package trie // Package succinct provides several succinct data types. // Modify from https://github.com/openacid/succinct/blob/d4684c35d123f7528b14e03c24327231723db704/sskv.go import ( "sort" "strings" "github.com/metacubex/mihomo/common/utils" "github.com/openacid/low/bitmap" ) const ( complexWildcardByte = byte('+') wildcardByte = byte('*') domainStepByte = byte('.') ) type DomainSet struct { leaves, labelBitmap []uint64 labels []byte ranks, selects []int32 } type qElt struct{ s, e, col int } // NewDomainSet creates a new *DomainSet struct, from a DomainTrie. func (t *DomainTrie[T]) NewDomainSet() *DomainSet { reserveDomains := make([]string, 0) t.Foreach(func(domain string, data T) bool { reserveDomains = append(reserveDomains, utils.Reverse(domain)) return true }) // ensure that the same prefix is continuous // and according to the ascending sequence of length sort.Strings(reserveDomains) keys := reserveDomains if len(keys) == 0 { return nil } ss := &DomainSet{} lIdx := 0 queue := []qElt{{0, len(keys), 0}} for i := 0; i < len(queue); i++ { elt := queue[i] if elt.col == len(keys[elt.s]) { elt.s++ // a leaf node setBit(&ss.leaves, i, 1) } for j := elt.s; j < elt.e; { frm := j for ; j < elt.e && keys[j][elt.col] == keys[frm][elt.col]; j++ { } queue = append(queue, qElt{frm, j, elt.col + 1}) ss.labels = append(ss.labels, keys[frm][elt.col]) setBit(&ss.labelBitmap, lIdx, 0) lIdx++ } setBit(&ss.labelBitmap, lIdx, 1) lIdx++ } ss.init() return ss } // Has query for a key and return whether it presents in the DomainSet. func (ss *DomainSet) Has(key string) bool { if ss == nil { return false } key = utils.Reverse(key) key = strings.ToLower(key) // no more labels in this node // skip character matching // go to next level nodeId, bmIdx := 0, 0 type wildcardCursor struct { bmIdx, index int } stack := make([]wildcardCursor, 0) for i := 0; i < len(key); i++ { RESTART: c := key[i] for ; ; bmIdx++ { if getBit(ss.labelBitmap, bmIdx) != 0 { if len(stack) > 0 { cursor := stack[len(stack)-1] stack = stack[0 : len(stack)-1] // back wildcard and find next node nextNodeId := countZeros(ss.labelBitmap, ss.ranks, cursor.bmIdx+1) nextBmIdx := selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nextNodeId-1) + 1 j := cursor.index for ; j < len(key) && key[j] != domainStepByte; j++ { } if j == len(key) { if getBit(ss.leaves, nextNodeId) != 0 { return true } else { goto RESTART } } for ; nextBmIdx-nextNodeId < len(ss.labels); nextBmIdx++ { if ss.labels[nextBmIdx-nextNodeId] == domainStepByte { bmIdx = nextBmIdx nodeId = nextNodeId i = j goto RESTART } } } return false } // handle wildcard for domain if ss.labels[bmIdx-nodeId] == complexWildcardByte { return true } else if ss.labels[bmIdx-nodeId] == wildcardByte { cursor := wildcardCursor{} cursor.bmIdx = bmIdx cursor.index = i stack = append(stack, cursor) } else if ss.labels[bmIdx-nodeId] == c { break } } nodeId = countZeros(ss.labelBitmap, ss.ranks, bmIdx+1) bmIdx = selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nodeId-1) + 1 } return getBit(ss.leaves, nodeId) != 0 } func (ss *DomainSet) keys(f func(key string) bool) { var currentKey []byte var traverse func(int, int) bool traverse = func(nodeId, bmIdx int) bool { if getBit(ss.leaves, nodeId) != 0 { if !f(string(currentKey)) { return false } } for ; ; bmIdx++ { if getBit(ss.labelBitmap, bmIdx) != 0 { return true } nextLabel := ss.labels[bmIdx-nodeId] currentKey = append(currentKey, nextLabel) nextNodeId := countZeros(ss.labelBitmap, ss.ranks, bmIdx+1) nextBmIdx := selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nextNodeId-1) + 1 if !traverse(nextNodeId, nextBmIdx) { return false } currentKey = currentKey[:len(currentKey)-1] } } traverse(0, 0) return } func (ss *DomainSet) Foreach(f func(key string) bool) { ss.keys(func(key string) bool { return f(utils.Reverse(key)) }) } // MatchDomain implements C.DomainMatcher func (ss *DomainSet) MatchDomain(domain string) bool { return ss.Has(domain) } func setBit(bm *[]uint64, i int, v int) { for i>>6 >= len(*bm) { *bm = append(*bm, 0) } (*bm)[i>>6] |= uint64(v) << uint(i&63) } func getBit(bm []uint64, i int) uint64 { return bm[i>>6] & (1 << uint(i&63)) } // init builds pre-calculated cache to speed up rank() and select() func (ss *DomainSet) init() { ss.selects, ss.ranks = bitmap.IndexSelect32R64(ss.labelBitmap) } // countZeros counts the number of "0" in a bitmap before the i-th bit(excluding // the i-th bit) on behalf of rank index. // E.g.: // // countZeros("010010", 4) == 3 // // 012345 func countZeros(bm []uint64, ranks []int32, i int) int { a, _ := bitmap.Rank64(bm, ranks, int32(i)) return i - int(a) } // selectIthOne returns the index of the i-th "1" in a bitmap, on behalf of rank // and select indexes. // E.g.: // // selectIthOne("010010", 1) == 4 // // 012345 func selectIthOne(bm []uint64, ranks, selects []int32, i int) int { a, _ := bitmap.Select32R64(bm, selects, ranks, int32(i)) return int(a) } ================================================ FILE: core/Clash.Meta/component/trie/domain_set_bin.go ================================================ package trie import ( "encoding/binary" "errors" "io" ) func (ss *DomainSet) WriteBin(w io.Writer) (err error) { // version _, err = w.Write([]byte{1}) if err != nil { return err } // leaves err = binary.Write(w, binary.BigEndian, int64(len(ss.leaves))) if err != nil { return err } for _, d := range ss.leaves { err = binary.Write(w, binary.BigEndian, d) if err != nil { return err } } // labelBitmap err = binary.Write(w, binary.BigEndian, int64(len(ss.labelBitmap))) if err != nil { return err } for _, d := range ss.labelBitmap { err = binary.Write(w, binary.BigEndian, d) if err != nil { return err } } // labels err = binary.Write(w, binary.BigEndian, int64(len(ss.labels))) if err != nil { return err } _, err = w.Write(ss.labels) if err != nil { return err } return nil } func ReadDomainSetBin(r io.Reader) (ds *DomainSet, err error) { // version version := make([]byte, 1) _, err = io.ReadFull(r, version) if err != nil { return nil, err } if version[0] != 1 { return nil, errors.New("version is invalid") } ds = &DomainSet{} var length int64 // leaves err = binary.Read(r, binary.BigEndian, &length) if err != nil { return nil, err } if length < 1 { return nil, errors.New("length is invalid") } ds.leaves = make([]uint64, length) for i := int64(0); i < length; i++ { err = binary.Read(r, binary.BigEndian, &ds.leaves[i]) if err != nil { return nil, err } } // labelBitmap err = binary.Read(r, binary.BigEndian, &length) if err != nil { return nil, err } if length < 1 { return nil, errors.New("length is invalid") } ds.labelBitmap = make([]uint64, length) for i := int64(0); i < length; i++ { err = binary.Read(r, binary.BigEndian, &ds.labelBitmap[i]) if err != nil { return nil, err } } // labels err = binary.Read(r, binary.BigEndian, &length) if err != nil { return nil, err } if length < 1 { return nil, errors.New("length is invalid") } ds.labels = make([]byte, length) _, err = io.ReadFull(r, ds.labels) if err != nil { return nil, err } ds.init() return ds, nil } ================================================ FILE: core/Clash.Meta/component/trie/domain_set_test.go ================================================ package trie_test import ( "golang.org/x/exp/slices" "testing" "github.com/metacubex/mihomo/component/trie" "github.com/stretchr/testify/assert" ) func testDump(t *testing.T, tree *trie.DomainTrie[struct{}], set *trie.DomainSet) { var dataSrc []string tree.Foreach(func(domain string, data struct{}) bool { dataSrc = append(dataSrc, domain) return true }) slices.Sort(dataSrc) var dataSet []string set.Foreach(func(key string) bool { dataSet = append(dataSet, key) return true }) slices.Sort(dataSet) assert.Equal(t, dataSrc, dataSet) } func TestDomainSet(t *testing.T) { tree := trie.New[struct{}]() domainSet := []string{ "baidu.com", "google.com", "www.google.com", "test.a.net", "test.a.oc", "Mijia Cloud", ".qq.com", "+.cn", } for _, domain := range domainSet { assert.NoError(t, tree.Insert(domain, struct{}{})) } assert.False(t, tree.IsEmpty()) set := tree.NewDomainSet() assert.NotNil(t, set) assert.True(t, set.Has("test.cn")) assert.True(t, set.Has("cn")) assert.True(t, set.Has("Mijia Cloud")) assert.True(t, set.Has("test.a.net")) assert.True(t, set.Has("www.qq.com")) assert.True(t, set.Has("google.com")) assert.False(t, set.Has("qq.com")) assert.False(t, set.Has("www.baidu.com")) testDump(t, tree, set) } func TestDomainSetComplexWildcard(t *testing.T) { tree := trie.New[struct{}]() domainSet := []string{ "+.baidu.com", "+.a.baidu.com", "www.baidu.com", "+.bb.baidu.com", "test.a.net", "test.a.oc", "www.qq.com", } for _, domain := range domainSet { assert.NoError(t, tree.Insert(domain, struct{}{})) } assert.False(t, tree.IsEmpty()) set := tree.NewDomainSet() assert.NotNil(t, set) assert.False(t, set.Has("google.com")) assert.True(t, set.Has("www.baidu.com")) assert.True(t, set.Has("test.test.baidu.com")) testDump(t, tree, set) } func TestDomainSetWildcard(t *testing.T) { tree := trie.New[struct{}]() domainSet := []string{ "*.*.*.baidu.com", "www.baidu.*", "stun.*.*", "*.*.qq.com", "test.*.baidu.com", "*.apple.com", } for _, domain := range domainSet { assert.NoError(t, tree.Insert(domain, struct{}{})) } assert.False(t, tree.IsEmpty()) set := tree.NewDomainSet() assert.NotNil(t, set) assert.True(t, set.Has("www.baidu.com")) assert.True(t, set.Has("test.test.baidu.com")) assert.True(t, set.Has("test.test.qq.com")) assert.True(t, set.Has("stun.ab.cd")) assert.False(t, set.Has("test.baidu.com")) assert.False(t, set.Has("www.google.com")) assert.False(t, set.Has("a.www.google.com")) assert.False(t, set.Has("test.qq.com")) assert.False(t, set.Has("test.test.test.qq.com")) testDump(t, tree, set) } ================================================ FILE: core/Clash.Meta/component/trie/domain_test.go ================================================ package trie_test import ( "net/netip" "testing" "github.com/metacubex/mihomo/component/trie" "github.com/stretchr/testify/assert" ) var localIP = netip.AddrFrom4([4]byte{127, 0, 0, 1}) func TestTrie_Basic(t *testing.T) { tree := trie.New[netip.Addr]() domains := []string{ "example.com", "google.com", "localhost", } for _, domain := range domains { assert.NoError(t, tree.Insert(domain, localIP)) } node := tree.Search("example.com") assert.NotNil(t, node) assert.True(t, node.Data() == localIP) assert.NotNil(t, tree.Insert("", localIP)) assert.Nil(t, tree.Search("")) assert.NotNil(t, tree.Search("localhost")) assert.Nil(t, tree.Search("www.google.com")) } func TestTrie_Wildcard(t *testing.T) { tree := trie.New[netip.Addr]() domains := []string{ "*.example.com", "sub.*.example.com", "*.dev", ".org", ".example.net", ".apple.*", "+.foo.com", "+.stun.*.*", "+.stun.*.*.*", "+.stun.*.*.*.*", "stun.l.google.com", } for _, domain := range domains { assert.NoError(t, tree.Insert(domain, localIP)) } assert.NotNil(t, tree.Search("sub.example.com")) assert.NotNil(t, tree.Search("sub.foo.example.com")) assert.NotNil(t, tree.Search("test.org")) assert.NotNil(t, tree.Search("test.example.net")) assert.NotNil(t, tree.Search("test.apple.com")) assert.NotNil(t, tree.Search("test.foo.com")) assert.NotNil(t, tree.Search("foo.com")) assert.NotNil(t, tree.Search("global.stun.website.com")) assert.Nil(t, tree.Search("foo.sub.example.com")) assert.Nil(t, tree.Search("foo.example.dev")) assert.Nil(t, tree.Search("example.com")) } func TestTrie_Priority(t *testing.T) { tree := trie.New[int]() domains := []string{ ".dev", "example.dev", "*.example.dev", "test.example.dev", } assertFn := func(domain string, data int) { node := tree.Search(domain) assert.NotNil(t, node) assert.Equal(t, data, node.Data()) } for idx, domain := range domains { assert.NoError(t, tree.Insert(domain, idx+1)) } assertFn("test.dev", 1) assertFn("foo.bar.dev", 1) assertFn("example.dev", 2) assertFn("foo.example.dev", 3) assertFn("test.example.dev", 4) } func TestTrie_Boundary(t *testing.T) { tree := trie.New[netip.Addr]() assert.NoError(t, tree.Insert("*.dev", localIP)) assert.NotNil(t, tree.Insert(".", localIP)) assert.NotNil(t, tree.Insert("..dev", localIP)) assert.Nil(t, tree.Search("dev")) } func TestTrie_WildcardBoundary(t *testing.T) { tree := trie.New[netip.Addr]() assert.NoError(t, tree.Insert("+.*", localIP)) assert.NoError(t, tree.Insert("stun.*.*.*", localIP)) assert.NotNil(t, tree.Search("example.com")) } func TestTrie_Foreach(t *testing.T) { tree := trie.New[netip.Addr]() domainList := []string{ "google.com", "stun.*.*.*", "test.*.google.com", "+.baidu.com", "*.baidu.com", "*.*.baidu.com", } for _, domain := range domainList { assert.NoError(t, tree.Insert(domain, localIP)) } count := 0 tree.Foreach(func(domain string, data netip.Addr) bool { count++ return true }) assert.Equal(t, 7, count) } func TestTrie_Space(t *testing.T) { validDomain := func(domain string) bool { _, ok := trie.ValidAndSplitDomain(domain) return ok } assert.True(t, validDomain("google.com")) assert.False(t, validDomain(" google.com")) assert.False(t, validDomain(" google.com ")) assert.True(t, validDomain("Mijia Cloud")) } ================================================ FILE: core/Clash.Meta/component/trie/ipcidr_node.go ================================================ package trie import "errors" var ( ErrorOverMaxValue = errors.New("the value don't over max value") ) type IpCidrNode struct { Mark bool child map[uint32]*IpCidrNode maxValue uint32 } func NewIpCidrNode(mark bool, maxValue uint32) *IpCidrNode { ipCidrNode := &IpCidrNode{ Mark: mark, child: map[uint32]*IpCidrNode{}, maxValue: maxValue, } return ipCidrNode } func (n *IpCidrNode) addChild(value uint32) error { if value > n.maxValue { return ErrorOverMaxValue } n.child[value] = NewIpCidrNode(false, n.maxValue) return nil } func (n *IpCidrNode) hasChild(value uint32) bool { return n.getChild(value) != nil } func (n *IpCidrNode) getChild(value uint32) *IpCidrNode { if value <= n.maxValue { return n.child[value] } return nil } ================================================ FILE: core/Clash.Meta/component/trie/ipcidr_trie.go ================================================ package trie import ( "net" "github.com/metacubex/mihomo/log" ) type IPV6 bool const ( ipv4GroupMaxValue = 0xFF ipv6GroupMaxValue = 0xFFFF ) type IpCidrTrie struct { ipv4Trie *IpCidrNode ipv6Trie *IpCidrNode } func NewIpCidrTrie() *IpCidrTrie { return &IpCidrTrie{ ipv4Trie: NewIpCidrNode(false, ipv4GroupMaxValue), ipv6Trie: NewIpCidrNode(false, ipv6GroupMaxValue), } } func (trie *IpCidrTrie) AddIpCidr(ipCidr *net.IPNet) error { subIpCidr, subCidr, isIpv4, err := ipCidrToSubIpCidr(ipCidr) if err != nil { return err } for _, sub := range subIpCidr { addIpCidr(trie, isIpv4, sub, subCidr/8) } return nil } func (trie *IpCidrTrie) AddIpCidrForString(ipCidr string) error { _, ipNet, err := net.ParseCIDR(ipCidr) if err != nil { return err } return trie.AddIpCidr(ipNet) } func (trie *IpCidrTrie) IsContain(ip net.IP) bool { if ip == nil { return false } isIpv4 := len(ip) == net.IPv4len var groupValues []uint32 var ipCidrNode *IpCidrNode if isIpv4 { ipCidrNode = trie.ipv4Trie for _, group := range ip { groupValues = append(groupValues, uint32(group)) } } else { ipCidrNode = trie.ipv6Trie for i := 0; i < len(ip); i += 2 { groupValues = append(groupValues, getIpv6GroupValue(ip[i], ip[i+1])) } } return search(ipCidrNode, groupValues) != nil } func (trie *IpCidrTrie) IsContainForString(ipString string) bool { ip := net.ParseIP(ipString) // deal with 4in6 actualIp := ip.To4() if actualIp == nil { actualIp = ip } return trie.IsContain(actualIp) } func ipCidrToSubIpCidr(ipNet *net.IPNet) ([]net.IP, int, bool, error) { maskSize, _ := ipNet.Mask.Size() var ( ipList []net.IP newMaskSize int isIpv4 bool err error ) isIpv4 = len(ipNet.IP) == net.IPv4len ipList, newMaskSize, err = subIpCidr(ipNet.IP, maskSize, isIpv4) return ipList, newMaskSize, isIpv4, err } func subIpCidr(ip net.IP, maskSize int, isIpv4 bool) ([]net.IP, int, error) { var subIpCidrList []net.IP groupSize := 8 if !isIpv4 { groupSize = 16 } if maskSize%groupSize == 0 { return append(subIpCidrList, ip), maskSize, nil } lastByteMaskSize := maskSize % 8 lastByteMaskIndex := maskSize / 8 subIpCidrNum := 0xFF >> lastByteMaskSize for i := 0; i <= subIpCidrNum; i++ { subIpCidr := make([]byte, len(ip)) copy(subIpCidr, ip) subIpCidr[lastByteMaskIndex] += byte(i) subIpCidrList = append(subIpCidrList, subIpCidr) } newMaskSize := (lastByteMaskIndex + 1) * 8 if !isIpv4 { newMaskSize = (lastByteMaskIndex/2 + 1) * 16 } return subIpCidrList, newMaskSize, nil } func addIpCidr(trie *IpCidrTrie, isIpv4 bool, ip net.IP, groupSize int) { if isIpv4 { addIpv4Cidr(trie, ip, groupSize) } else { addIpv6Cidr(trie, ip, groupSize) } } func addIpv4Cidr(trie *IpCidrTrie, ip net.IP, groupSize int) { preNode := trie.ipv4Trie node := preNode.getChild(uint32(ip[0])) if node == nil { err := preNode.addChild(uint32(ip[0])) if err != nil { return } node = preNode.getChild(uint32(ip[0])) } for i := 1; i < groupSize; i++ { if node.Mark { return } groupValue := uint32(ip[i]) if !node.hasChild(groupValue) { err := node.addChild(groupValue) if err != nil { log.Errorln(err.Error()) } } preNode = node node = node.getChild(groupValue) if node == nil { err := preNode.addChild(uint32(ip[i-1])) if err != nil { return } node = preNode.getChild(uint32(ip[i-1])) } } node.Mark = true cleanChild(node) } func addIpv6Cidr(trie *IpCidrTrie, ip net.IP, groupSize int) { preNode := trie.ipv6Trie node := preNode.getChild(getIpv6GroupValue(ip[0], ip[1])) if node == nil { err := preNode.addChild(getIpv6GroupValue(ip[0], ip[1])) if err != nil { return } node = preNode.getChild(getIpv6GroupValue(ip[0], ip[1])) } for i := 2; i < groupSize; i += 2 { if ip[i] == 0 && ip[i+1] == 0 { node.Mark = true } if node.Mark { return } groupValue := getIpv6GroupValue(ip[i], ip[i+1]) if !node.hasChild(groupValue) { err := node.addChild(groupValue) if err != nil { log.Errorln(err.Error()) } } preNode = node node = node.getChild(groupValue) if node == nil { err := preNode.addChild(getIpv6GroupValue(ip[i-2], ip[i-1])) if err != nil { return } node = preNode.getChild(getIpv6GroupValue(ip[i-2], ip[i-1])) } } node.Mark = true cleanChild(node) } func getIpv6GroupValue(high, low byte) uint32 { return (uint32(high) << 8) | uint32(low) } func cleanChild(node *IpCidrNode) { for i := uint32(0); i < uint32(len(node.child)); i++ { delete(node.child, i) } } func search(root *IpCidrNode, groupValues []uint32) *IpCidrNode { node := root.getChild(groupValues[0]) if node == nil || node.Mark { return node } for _, value := range groupValues[1:] { if !node.hasChild(value) { return nil } node = node.getChild(value) if node == nil || node.Mark { return node } } return nil } ================================================ FILE: core/Clash.Meta/component/trie/node.go ================================================ package trie import "strings" // Node is the trie's node type Node[T any] struct { childMap map[string]*Node[T] childNode *Node[T] // optimize for only one child childStr string inited bool data T } func (n *Node[T]) getChild(s string) *Node[T] { if n.childMap == nil { if n.childNode != nil && n.childStr == s { return n.childNode } return nil } return n.childMap[s] } func (n *Node[T]) hasChild(s string) bool { return n.getChild(s) != nil } func (n *Node[T]) addChild(s string, child *Node[T]) { if n.childMap == nil { if n.childNode == nil { n.childStr = s n.childNode = child return } n.childMap = map[string]*Node[T]{} if n.childNode != nil { n.childMap[n.childStr] = n.childNode } n.childStr = "" n.childNode = nil } n.childMap[s] = child } func (n *Node[T]) getOrNewChild(s string) *Node[T] { node := n.getChild(s) if node == nil { node = newNode[T]() n.addChild(s, node) } return node } func (n *Node[T]) optimize() { if len(n.childStr) > 0 { n.childStr = strClone(n.childStr) } if n.childNode != nil { n.childNode.optimize() } if n.childMap == nil { return } switch len(n.childMap) { case 0: n.childMap = nil return case 1: for key := range n.childMap { n.childStr = key n.childNode = n.childMap[key] } n.childMap = nil n.optimize() return } children := make(map[string]*Node[T], len(n.childMap)) // avoid map reallocate memory for key := range n.childMap { child := n.childMap[key] if child == nil { continue } key = strClone(key) children[key] = child child.optimize() } n.childMap = children } func strClone(key string) string { switch key { // try to save string's memory case wildcard: key = wildcard case dotWildcard: key = dotWildcard case complexWildcard: key = complexWildcard case domainStep: key = domainStep default: key = strings.Clone(key) } return key } func (n *Node[T]) isEmpty() bool { if n == nil || n.inited == false { return true } return false } func (n *Node[T]) setData(data T) { n.data = data n.inited = true } func (n *Node[T]) getChildren() map[string]*Node[T] { if n.childMap == nil { if n.childNode != nil { m := make(map[string]*Node[T]) m[n.childStr] = n.childNode return m } } else { return n.childMap } return nil } func (n *Node[T]) Data() T { return n.data } func newNode[T any]() *Node[T] { return &Node[T]{} } ================================================ FILE: core/Clash.Meta/component/trie/trie_test.go ================================================ package trie import ( "net" "testing" "github.com/stretchr/testify/assert" ) func TestIpv4AddSuccess(t *testing.T) { trie := NewIpCidrTrie() err := trie.AddIpCidrForString("10.0.0.2/16") assert.Equal(t, nil, err) } func TestIpv4AddFail(t *testing.T) { trie := NewIpCidrTrie() err := trie.AddIpCidrForString("333.00.23.2/23") assert.IsType(t, new(net.ParseError), err) err = trie.AddIpCidrForString("22.3.34.2/222") assert.IsType(t, new(net.ParseError), err) err = trie.AddIpCidrForString("2.2.2.2") assert.IsType(t, new(net.ParseError), err) } func TestIpv4Search(t *testing.T) { trie := NewIpCidrTrie() // Boundary testing assert.NoError(t, trie.AddIpCidrForString("149.154.160.0/20")) assert.Equal(t, true, trie.IsContainForString("149.154.160.0")) assert.Equal(t, true, trie.IsContainForString("149.154.175.255")) assert.Equal(t, false, trie.IsContainForString("149.154.176.0")) assert.Equal(t, false, trie.IsContainForString("149.154.159.255")) assert.NoError(t, trie.AddIpCidrForString("129.2.36.0/16")) assert.NoError(t, trie.AddIpCidrForString("10.2.36.0/18")) assert.NoError(t, trie.AddIpCidrForString("16.2.23.0/24")) assert.NoError(t, trie.AddIpCidrForString("11.2.13.2/26")) assert.NoError(t, trie.AddIpCidrForString("55.5.6.3/8")) assert.NoError(t, trie.AddIpCidrForString("66.23.25.4/6")) assert.Equal(t, true, trie.IsContainForString("129.2.3.65")) assert.Equal(t, false, trie.IsContainForString("15.2.3.1")) assert.Equal(t, true, trie.IsContainForString("11.2.13.1")) assert.Equal(t, true, trie.IsContainForString("55.0.0.0")) assert.Equal(t, true, trie.IsContainForString("64.0.0.0")) assert.Equal(t, false, trie.IsContainForString("128.0.0.0")) assert.Equal(t, false, trie.IsContain(net.ParseIP("22"))) assert.Equal(t, false, trie.IsContain(net.ParseIP(""))) } func TestIpv6AddSuccess(t *testing.T) { trie := NewIpCidrTrie() err := trie.AddIpCidrForString("2001:0db8:02de:0000:0000:0000:0000:0e13/32") assert.Equal(t, nil, err) err = trie.AddIpCidrForString("2001:1db8:f2de::0e13/18") assert.Equal(t, nil, err) } func TestIpv6AddFail(t *testing.T) { trie := NewIpCidrTrie() err := trie.AddIpCidrForString("2001::25de::cade/23") assert.IsType(t, new(net.ParseError), err) err = trie.AddIpCidrForString("2001:0fa3:25de::cade/222") assert.IsType(t, new(net.ParseError), err) err = trie.AddIpCidrForString("2001:0fa3:25de::cade") assert.IsType(t, new(net.ParseError), err) } func TestIpv6SearchSub(t *testing.T) { trie := NewIpCidrTrie() assert.NoError(t, trie.AddIpCidrForString("240e::/18")) assert.Equal(t, true, trie.IsContainForString("240e:964:ea02:100:1800::71")) } func TestIpv6Search(t *testing.T) { trie := NewIpCidrTrie() // Boundary testing assert.NoError(t, trie.AddIpCidrForString("2a0a:f280::/32")) assert.Equal(t, true, trie.IsContainForString("2a0a:f280:0000:0000:0000:0000:0000:0000")) assert.Equal(t, true, trie.IsContainForString("2a0a:f280:ffff:ffff:ffff:ffff:ffff:ffff")) assert.Equal(t, false, trie.IsContainForString("2a0a:f279:ffff:ffff:ffff:ffff:ffff:ffff")) assert.Equal(t, false, trie.IsContainForString("2a0a:f281:0000:0000:0000:0000:0000:0000")) assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23d:f001::e/128")) assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f002::e/12")) assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23d:f003::e/96")) assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f002::a/32")) assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f004::a/60")) assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23f:f005::a/64")) assert.Equal(t, true, trie.IsContainForString("2001:b28:f23d:f001::e")) assert.Equal(t, false, trie.IsContainForString("2222::fff2")) assert.Equal(t, true, trie.IsContainForString("2000::ffa0")) assert.Equal(t, true, trie.IsContainForString("2001:b28:f23f:f005:5662::")) assert.Equal(t, true, trie.IsContainForString("2001:67c:4e8:9666::1213")) assert.Equal(t, false, trie.IsContain(net.ParseIP("22233:22"))) } func TestIpv4InIpv6(t *testing.T) { trie := NewIpCidrTrie() // Boundary testing assert.NoError(t, trie.AddIpCidrForString("::ffff:198.18.5.138/128")) } ================================================ FILE: core/Clash.Meta/component/updater/patch.go ================================================ package updater import ( "fmt" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/mmdb" "github.com/oschwald/maxminddb-golang" ) func UpdateMMDBWithPath(path string) (err error) { defer mmdb.ReloadIP() data, err := downloadForBytes(geodata.MmdbUrl()) if err != nil { return fmt.Errorf("can't download MMDB database file: %w", err) } instance, err := maxminddb.FromBytes(data) if err != nil { return fmt.Errorf("invalid MMDB database file: %s", err) } _ = instance.Close() mmdb.IPInstance().Reader.Close() if err = saveFile(data, path); err != nil { return fmt.Errorf("can't save MMDB database file: %w", err) } return nil } func UpdateASNWithPath(path string) (err error) { defer mmdb.ReloadASN() data, err := downloadForBytes(geodata.ASNUrl()) if err != nil { return fmt.Errorf("can't download ASN database file: %w", err) } instance, err := maxminddb.FromBytes(data) if err != nil { return fmt.Errorf("invalid ASN database file: %s", err) } _ = instance.Close() mmdb.ASNInstance().Reader.Close() if err = saveFile(data, path); err != nil { return fmt.Errorf("can't save ASN database file: %w", err) } return nil } func UpdateGeoIpWithPath(path string) (err error) { geoLoader, err := geodata.GetGeoDataLoader("standard") data, err := downloadForBytes(geodata.GeoIpUrl()) if err != nil { return fmt.Errorf("can't download GeoIP database file: %w", err) } if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil { return fmt.Errorf("invalid GeoIP database file: %s", err) } if err = saveFile(data, path); err != nil { return fmt.Errorf("can't save GeoIP database file: %w", err) } return nil } func UpdateGeoSiteWithPath(path string) (err error) { geoLoader, err := geodata.GetGeoDataLoader("standard") data, err := downloadForBytes(geodata.GeoSiteUrl()) if err != nil { return fmt.Errorf("can't download GeoSite database file: %w", err) } if _, err = geoLoader.LoadSiteByBytes(data, "cn"); err != nil { return fmt.Errorf("invalid GeoSite database file: %s", err) } if err = saveFile(data, path); err != nil { return fmt.Errorf("can't save GeoSite database file: %w", err) } return nil } ================================================ FILE: core/Clash.Meta/component/updater/update_core.go ================================================ package updater import ( "archive/zip" "compress/gzip" "context" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "time" "github.com/metacubex/mihomo/component/ca" mihomoHttp "github.com/metacubex/mihomo/component/http" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" "github.com/metacubex/http" ) const ( baseReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/" versionReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt" baseAlphaURL = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/" versionAlphaURL = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt" // MaxPackageFileSize is a maximum package file length in bytes. The largest // package whose size is limited by this constant currently has the size of // approximately 32 MiB. MaxPackageFileSize = 32 * 1024 * 1024 ) const ( ReleaseChannel = "release" AlphaChannel = "alpha" ) // CoreUpdater is the mihomo updater. // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go type CoreUpdater struct { mu sync.Mutex } var DefaultCoreUpdater = CoreUpdater{} func (u *CoreUpdater) CoreBaseName() string { switch runtime.GOARCH { case "arm": // mihomo-linux-armv5 return fmt.Sprintf("mihomo-%s-%sv%s", runtime.GOOS, runtime.GOARCH, features.GOARM) case "arm64": if runtime.GOOS == "android" { // mihomo-android-arm64-v8 return fmt.Sprintf("mihomo-%s-%s-v8", runtime.GOOS, runtime.GOARCH) } else { // mihomo-linux-arm64 return fmt.Sprintf("mihomo-%s-%s", runtime.GOOS, runtime.GOARCH) } case "mips", "mipsle": // mihomo-linux-mips-hardfloat return fmt.Sprintf("mihomo-%s-%s-%s", runtime.GOOS, runtime.GOARCH, features.GOMIPS) case "amd64": // mihomo-linux-amd64-v1 return fmt.Sprintf("mihomo-%s-%s-%s", runtime.GOOS, runtime.GOARCH, features.GOAMD64) default: // mihomo-linux-386 // mihomo-linux-mips64 // mihomo-linux-riscv64 // mihomo-linux-s390x return fmt.Sprintf("mihomo-%s-%s", runtime.GOOS, runtime.GOARCH) } } func (u *CoreUpdater) Update(currentExePath string, channel string, force bool) (err error) { u.mu.Lock() defer u.mu.Unlock() info, err := os.Stat(currentExePath) if err != nil { return fmt.Errorf("check currentExePath %q: %w", currentExePath, err) } baseURL := baseAlphaURL versionURL := versionAlphaURL switch strings.ToLower(channel) { case ReleaseChannel: baseURL = baseReleaseURL versionURL = versionReleaseURL case AlphaChannel: break default: // auto if !strings.HasPrefix(C.Version, "alpha") { baseURL = baseReleaseURL versionURL = versionReleaseURL } } latestVersion, err := u.getLatestVersion(versionURL) if err != nil { return fmt.Errorf("get latest version: %w", err) } log.Infoln("current version %s, latest version %s", C.Version, latestVersion) if latestVersion == C.Version && !force { // don't change this output, some downstream dependencies on the upgrader's output fields return fmt.Errorf("update error: already using latest version %s", C.Version) } defer func() { if err != nil { log.Errorln("updater: failed: %v", err) } else { log.Infoln("updater: finished") } }() // ---- prepare ---- mihomoBaseName := u.CoreBaseName() packageName := mihomoBaseName + "-" + latestVersion if runtime.GOOS == "windows" { packageName = packageName + ".zip" } else { packageName = packageName + ".gz" } packageURL := baseURL + packageName log.Infoln("updater: updating using url: %s", packageURL) workDir := filepath.Dir(currentExePath) backupDir := filepath.Join(workDir, "meta-backup") updateDir := filepath.Join(workDir, "meta-update") packagePath := filepath.Join(updateDir, packageName) //log.Infoln(packagePath) updateExeName := mihomoBaseName if runtime.GOOS == "windows" { updateExeName = updateExeName + ".exe" } log.Infoln("updateExeName: %s", updateExeName) updateExePath := filepath.Join(updateDir, updateExeName) backupExePath := filepath.Join(backupDir, filepath.Base(currentExePath)) defer u.clean(updateDir) err = u.download(updateDir, packagePath, packageURL) if err != nil { return fmt.Errorf("downloading: %w", err) } err = u.unpack(updateDir, packagePath, info.Mode()) if err != nil { return fmt.Errorf("unpacking: %w", err) } err = u.backup(currentExePath, backupExePath, backupDir) if err != nil { return fmt.Errorf("backuping: %w", err) } err = u.copyFile(updateExePath, currentExePath) if err != nil { return fmt.Errorf("replacing: %w", err) } return nil } func (u *CoreUpdater) getLatestVersion(versionURL string) (version string, err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil, mihomoHttp.WithCAOption(ca.Option{ZeroTrust: true})) if err != nil { return "", err } defer func() { closeErr := resp.Body.Close() if closeErr != nil && err == nil { err = closeErr } }() body, err := io.ReadAll(resp.Body) if err != nil { return "", err } content := strings.TrimRight(string(body), "\n") return content, nil } // download package file and save it to disk func (u *CoreUpdater) download(updateDir, packagePath, packageURL string) (err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) defer cancel() resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil, mihomoHttp.WithCAOption(ca.Option{ZeroTrust: true})) if err != nil { return fmt.Errorf("http request failed: %w", err) } defer func() { closeErr := resp.Body.Close() if closeErr != nil && err == nil { err = closeErr } }() log.Debugln("updateDir %s", updateDir) err = os.Mkdir(updateDir, 0o755) if err != nil { return fmt.Errorf("mkdir error: %w", err) } log.Debugln("updater: saving package to file %s", packagePath) // Create the output file wc, err := os.OpenFile(packagePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) if err != nil { return fmt.Errorf("os.OpenFile(%s): %w", packagePath, err) } defer func() { closeErr := wc.Close() if closeErr != nil && err == nil { err = closeErr } }() log.Debugln("updater: reading http body") // This use of io.Copy is now safe, because we limited body's Reader. n, err := io.Copy(wc, io.LimitReader(resp.Body, MaxPackageFileSize)) if err != nil { return fmt.Errorf("io.Copy(): %w", err) } if n == MaxPackageFileSize { // Use whether n is equal to MaxPackageFileSize to determine whether the limit has been reached. // It is also possible that the size of the downloaded file is exactly the same as the maximum limit, // but we should not consider this too rare situation. return fmt.Errorf("attempted to read more than %d bytes", MaxPackageFileSize) } log.Debugln("updater: downloaded package to file %s", packagePath) return nil } // unpack extracts the files from the downloaded archive. func (u *CoreUpdater) unpack(updateDir, packagePath string, fileMode os.FileMode) error { log.Infoln("updater: unpacking package") if strings.HasSuffix(packagePath, ".zip") { _, err := u.zipFileUnpack(packagePath, updateDir, fileMode) if err != nil { return fmt.Errorf(".zip unpack failed: %w", err) } } else if strings.HasSuffix(packagePath, ".gz") { _, err := u.gzFileUnpack(packagePath, updateDir, fileMode) if err != nil { return fmt.Errorf(".gz unpack failed: %w", err) } } else { return fmt.Errorf("unknown package extension") } return nil } // backup creates a backup of the current executable file. func (u *CoreUpdater) backup(currentExePath, backupExePath, backupDir string) (err error) { log.Infoln("updater: backing up current ExecFile:%s to %s", currentExePath, backupExePath) _ = os.Mkdir(backupDir, 0o755) // On Windows, since the running executable cannot be overwritten or deleted, it uses os.Rename to move the file to the backup path. // On other platforms, it copies the file to the backup path, preserving the original file and its permissions. // The backup directory is created if it does not exist. if runtime.GOOS == "windows" { err = os.Rename(currentExePath, backupExePath) } else { err = u.copyFile(currentExePath, backupExePath) } if err != nil { return err } return nil } // clean removes the temporary directory itself and all it's contents. func (u *CoreUpdater) clean(updateDir string) { _ = os.RemoveAll(updateDir) } // Unpack a single .gz file to the specified directory // Existing files are overwritten // All files are created inside outDir, subdirectories are not created // Return the output file name func (u *CoreUpdater) gzFileUnpack(gzfile, outDir string, fileMode os.FileMode) (outputName string, err error) { f, err := os.Open(gzfile) if err != nil { return "", fmt.Errorf("os.Open(): %w", err) } defer func() { closeErr := f.Close() if closeErr != nil && err == nil { err = closeErr } }() gzReader, err := gzip.NewReader(f) if err != nil { return "", fmt.Errorf("gzip.NewReader(): %w", err) } defer func() { closeErr := gzReader.Close() if closeErr != nil && err == nil { err = closeErr } }() // Get the original file name from the .gz file header originalName := gzReader.Header.Name if originalName == "" { // Fallback: remove the .gz extension from the input file name if the header doesn't provide the original name originalName = filepath.Base(gzfile) originalName = strings.TrimSuffix(originalName, ".gz") } outputName = filepath.Join(outDir, originalName) // Create the output file wc, err := os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { return "", fmt.Errorf("os.OpenFile(%s): %w", outputName, err) } defer func() { closeErr := wc.Close() if closeErr != nil && err == nil { err = closeErr } }() // Copy the contents of the gzReader to the output file _, err = io.Copy(wc, gzReader) if err != nil { return "", fmt.Errorf("io.Copy(): %w", err) } return outputName, nil } // Unpack a single file from .zip file to the specified directory // Existing files are overwritten // All files are created inside 'outDir', subdirectories are not created // Return the output file name func (u *CoreUpdater) zipFileUnpack(zipfile, outDir string, fileMode os.FileMode) (outputName string, err error) { zrc, err := zip.OpenReader(zipfile) if err != nil { return "", fmt.Errorf("zip.OpenReader(): %w", err) } defer func() { closeErr := zrc.Close() if closeErr != nil && err == nil { err = closeErr } }() if len(zrc.File) == 0 { return "", fmt.Errorf("no files in the zip archive") } // Assuming the first file in the zip archive is the target file zf := zrc.File[0] var rc io.ReadCloser rc, err = zf.Open() if err != nil { return "", fmt.Errorf("zip file Open(): %w", err) } defer func() { closeErr := rc.Close() if closeErr != nil && err == nil { err = closeErr } }() fi := zf.FileInfo() name := fi.Name() outputName = filepath.Join(outDir, name) if fi.IsDir() { return "", fmt.Errorf("the target file is a directory") } var wc io.WriteCloser wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { return "", fmt.Errorf("os.OpenFile(): %w", err) } defer func() { closeErr := wc.Close() if closeErr != nil && err == nil { err = closeErr } }() _, err = io.Copy(wc, rc) if err != nil { return "", fmt.Errorf("io.Copy(): %w", err) } return outputName, nil } // Copy file on disk func (u *CoreUpdater) copyFile(src, dst string) (err error) { rc, err := os.Open(src) if err != nil { return fmt.Errorf("os.Open(%s): %w", src, err) } defer func() { closeErr := rc.Close() if closeErr != nil && err == nil { err = closeErr } }() info, err := rc.Stat() if err != nil { return fmt.Errorf("rc.Stat(): %w", err) } // Create the output file // If the file does not exist, creates it with permissions perm (before umask); // otherwise truncates it before writing, without changing permissions. wc, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) if err != nil { // On some file system (such as Android's /data) maybe return error: "text file busy" // Let's delete the target file and recreate it err = os.Remove(dst) if err != nil { return fmt.Errorf("os.Remove(%s): %w", dst, err) } wc, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) if err != nil { return fmt.Errorf("os.OpenFile(%s): %w", dst, err) } } defer func() { closeErr := wc.Close() if closeErr != nil && err == nil { err = closeErr } }() _, err = io.Copy(wc, rc) if err != nil { return fmt.Errorf("io.Copy(): %w", err) } if runtime.GOOS == "darwin" { err = exec.Command("/usr/bin/codesign", "--sign", "-", dst).Run() if err != nil { log.Warnln("codesign failed: %v", err) } } log.Infoln("updater: copy: %s to %s", src, dst) return nil } ================================================ FILE: core/Clash.Meta/component/updater/update_core_test.go ================================================ package updater import ( "fmt" "testing" ) func TestCoreBaseName(t *testing.T) { fmt.Println("Core base name =", DefaultCoreUpdater.CoreBaseName()) } ================================================ FILE: core/Clash.Meta/component/updater/update_geo.go ================================================ package updater import ( "context" "errors" "fmt" "os" "runtime" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/geodata" _ "github.com/metacubex/mihomo/component/geodata/standard" "github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/oschwald/maxminddb-golang" "golang.org/x/sync/errgroup" ) var ( autoUpdate bool updateInterval int updatingGeo atomic.Bool ) func GeoAutoUpdate() bool { return autoUpdate } func GeoUpdateInterval() int { return updateInterval } func SetGeoAutoUpdate(newAutoUpdate bool) { autoUpdate = newAutoUpdate } func SetGeoUpdateInterval(newGeoUpdateInterval int) { updateInterval = newGeoUpdateInterval } func UpdateMMDB() (err error) { vehicle := resource.NewHTTPVehicle(geodata.MmdbUrl(), C.Path.MMDB(), "", nil, defaultHttpTimeout, 0) var oldHash utils.HashType if buf, err := os.ReadFile(vehicle.Path()); err == nil { oldHash = utils.MakeHash(buf) } data, hash, err := vehicle.Read(context.Background(), oldHash) if err != nil { return fmt.Errorf("can't download MMDB database file: %w", err) } if oldHash.Equal(hash) { // same hash, ignored return nil } if len(data) == 0 { return fmt.Errorf("can't download MMDB database file: no data") } instance, err := maxminddb.FromBytes(data) if err != nil { return fmt.Errorf("invalid MMDB database file: %s", err) } _ = instance.Close() defer mmdb.ReloadIP() mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file if err = vehicle.Write(data); err != nil { return fmt.Errorf("can't save MMDB database file: %w", err) } return nil } func UpdateASN() (err error) { vehicle := resource.NewHTTPVehicle(geodata.ASNUrl(), C.Path.ASN(), "", nil, defaultHttpTimeout, 0) var oldHash utils.HashType if buf, err := os.ReadFile(vehicle.Path()); err == nil { oldHash = utils.MakeHash(buf) } data, hash, err := vehicle.Read(context.Background(), oldHash) if err != nil { return fmt.Errorf("can't download ASN database file: %w", err) } if oldHash.Equal(hash) { // same hash, ignored return nil } if len(data) == 0 { return fmt.Errorf("can't download ASN database file: no data") } instance, err := maxminddb.FromBytes(data) if err != nil { return fmt.Errorf("invalid ASN database file: %s", err) } _ = instance.Close() defer mmdb.ReloadASN() mmdb.ASNInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file if err = vehicle.Write(data); err != nil { return fmt.Errorf("can't save ASN database file: %w", err) } return nil } func UpdateGeoIp() (err error) { geoLoader, err := geodata.GetGeoDataLoader("standard") vehicle := resource.NewHTTPVehicle(geodata.GeoIpUrl(), C.Path.GeoIP(), "", nil, defaultHttpTimeout, 0) var oldHash utils.HashType if buf, err := os.ReadFile(vehicle.Path()); err == nil { oldHash = utils.MakeHash(buf) } data, hash, err := vehicle.Read(context.Background(), oldHash) if err != nil { return fmt.Errorf("can't download GeoIP database file: %w", err) } if oldHash.Equal(hash) { // same hash, ignored return nil } if len(data) == 0 { return fmt.Errorf("can't download GeoIP database file: no data") } if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil { return fmt.Errorf("invalid GeoIP database file: %s", err) } defer geodata.ClearGeoIPCache() if err = vehicle.Write(data); err != nil { return fmt.Errorf("can't save GeoIP database file: %w", err) } return nil } func UpdateGeoSite() (err error) { geoLoader, err := geodata.GetGeoDataLoader("standard") vehicle := resource.NewHTTPVehicle(geodata.GeoSiteUrl(), C.Path.GeoSite(), "", nil, defaultHttpTimeout, 0) var oldHash utils.HashType if buf, err := os.ReadFile(vehicle.Path()); err == nil { oldHash = utils.MakeHash(buf) } data, hash, err := vehicle.Read(context.Background(), oldHash) if err != nil { return fmt.Errorf("can't download GeoSite database file: %w", err) } if oldHash.Equal(hash) { // same hash, ignored return nil } if len(data) == 0 { return fmt.Errorf("can't download GeoSite database file: no data") } if _, err = geoLoader.LoadSiteByBytes(data, "cn"); err != nil { return fmt.Errorf("invalid GeoSite database file: %s", err) } defer geodata.ClearGeoSiteCache() if err = vehicle.Write(data); err != nil { return fmt.Errorf("can't save GeoSite database file: %w", err) } return nil } func updateGeoDatabases() error { defer runtime.GC() b := errgroup.Group{} if geodata.GeoIpEnable() { if geodata.GeodataMode() { b.Go(UpdateGeoIp) } else { b.Go(UpdateMMDB) } } if geodata.ASNEnable() { b.Go(UpdateASN) } if geodata.GeoSiteEnable() { b.Go(UpdateGeoSite) } return b.Wait() } var ErrGetDatabaseUpdateSkip = errors.New("GEO database is updating, skip") func UpdateGeoDatabases() error { log.Infoln("[GEO] Start updating GEO database") if updatingGeo.Load() { return ErrGetDatabaseUpdateSkip } updatingGeo.Store(true) defer updatingGeo.Store(false) log.Infoln("[GEO] Updating GEO database") if err := updateGeoDatabases(); err != nil { log.Errorln("[GEO] update GEO database error: %s", err.Error()) return err } return nil } func getUpdateTime() (time time.Time, err error) { filesToCheck := []string{ C.Path.GeoIP(), C.Path.MMDB(), C.Path.ASN(), C.Path.GeoSite(), } for _, file := range filesToCheck { var fileInfo os.FileInfo fileInfo, err = os.Stat(file) if err == nil { return fileInfo.ModTime(), nil } } return } func RegisterGeoUpdater() { if updateInterval <= 0 { log.Errorln("[GEO] Invalid update interval: %d", updateInterval) return } go func() { ticker := time.NewTicker(time.Duration(updateInterval) * time.Hour) defer ticker.Stop() lastUpdate, err := getUpdateTime() if err != nil { log.Errorln("[GEO] Get GEO database update time error: %s", err.Error()) return } log.Infoln("[GEO] last update time %s", lastUpdate) if lastUpdate.Add(time.Duration(updateInterval) * time.Hour).Before(time.Now()) { log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(updateInterval)*time.Hour) if err := UpdateGeoDatabases(); err != nil { log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) return } } for range ticker.C { log.Infoln("[GEO] updating database every %d hours", updateInterval) if err := UpdateGeoDatabases(); err != nil { log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) } } }() } ================================================ FILE: core/Clash.Meta/component/updater/update_ui.go ================================================ package updater import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "errors" "fmt" "io" "os" "path" "path/filepath" "strings" "sync" "syscall" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) type UIUpdater struct { externalUIURL string externalUIPath string autoDownloadUI bool mutex sync.Mutex } type compressionType int const ( typeUnknown compressionType = iota typeZip typeTarGzip ) func (t compressionType) String() string { switch t { case typeZip: return "zip" case typeTarGzip: return "tar.gz" default: return "unknown" } } var DefaultUiUpdater = &UIUpdater{} func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater { updater := &UIUpdater{} // checkout externalUI exist if externalUI != "" { updater.autoDownloadUI = true updater.externalUIPath = C.Path.Resolve(externalUI) } else { // default externalUI path updater.externalUIPath = path.Join(C.Path.HomeDir(), "ui") } // checkout UIpath/name exist if externalUIName != "" { updater.autoDownloadUI = true updater.externalUIPath = path.Join(updater.externalUIPath, externalUIName) } if externalUIURL != "" { updater.externalUIURL = externalUIURL } return updater } func (u *UIUpdater) AutoDownloadUI() { u.mutex.Lock() defer u.mutex.Unlock() if u.autoDownloadUI { dirEntries, _ := os.ReadDir(u.externalUIPath) if len(dirEntries) > 0 { log.Infoln("UI already exists, skip downloading") } else { log.Infoln("External UI downloading ...") err := u.downloadUI() if err != nil { log.Errorln("Error downloading UI: %s", err) } } } } func (u *UIUpdater) DownloadUI() error { u.mutex.Lock() defer u.mutex.Unlock() return u.downloadUI() } func detectFileType(data []byte) compressionType { if len(data) < 4 { return typeUnknown } // Zip: 0x50 0x4B 0x03 0x04 if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 { return typeZip } // GZip: 0x1F 0x8B if data[0] == 0x1F && data[1] == 0x8B { return typeTarGzip } return typeUnknown } func (u *UIUpdater) downloadUI() error { data, err := downloadForBytes(u.externalUIURL) if err != nil { return fmt.Errorf("can't download file: %w", err) } tmpDir := C.Path.Resolve("downloadUI.tmp") defer os.RemoveAll(tmpDir) os.RemoveAll(tmpDir) // cleanup tmp dir before extract log.Debugln("extractedFolder: %s", tmpDir) err = extract(data, tmpDir) if err != nil { return fmt.Errorf("can't extract compressed file: %w", err) } log.Debugln("cleanupFolder: %s", u.externalUIPath) err = cleanup(u.externalUIPath) // cleanup files in dir don't remove dir itself if err != nil { if !os.IsNotExist(err) { return fmt.Errorf("cleanup exist file error: %w", err) } } err = u.prepareUIPath() if err != nil { return fmt.Errorf("prepare UI path failed: %w", err) } log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath) err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target if err != nil { return fmt.Errorf("move UI folder failed: %w", err) } return nil } func (u *UIUpdater) prepareUIPath() error { if _, err := os.Stat(u.externalUIPath); os.IsNotExist(err) { log.Infoln("dir %s does not exist, creating", u.externalUIPath) if err := os.MkdirAll(u.externalUIPath, os.ModePerm); err != nil { log.Warnln("create dir %s error: %s", u.externalUIPath, err) } } return nil } func unzip(data []byte, dest string) error { r, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return err } // check whether or not only exists singleRoot dir for _, f := range r.File { fpath := filepath.Join(dest, f.Name) if !inDest(fpath, dest) { return fmt.Errorf("invalid file path: %s", fpath) } info := f.FileInfo() if info.IsDir() { os.MkdirAll(fpath, os.ModePerm) continue } if info.Mode()&os.ModeSymlink != 0 { continue // disallow symlink } if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { return err } outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode().Perm()) if err != nil { return err } rc, err := f.Open() if err != nil { return err } _, err = io.Copy(outFile, rc) outFile.Close() rc.Close() if err != nil { return err } } return nil } func untgz(data []byte, dest string) error { gzr, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return err } defer gzr.Close() tr := tar.NewReader(gzr) for { header, err := tr.Next() if err == io.EOF { break } if err != nil { return err } fpath := filepath.Join(dest, header.Name) if !inDest(fpath, dest) { return fmt.Errorf("invalid file path: %s", fpath) } switch header.Typeflag { case tar.TypeDir: if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil { return err } case tar.TypeReg: if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { return err } outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode).Perm()) if err != nil { return err } if _, err := io.Copy(outFile, tr); err != nil { outFile.Close() return err } outFile.Close() } } return nil } func extract(data []byte, dest string) error { fileType := detectFileType(data) log.Debugln("compression Type: %s", fileType) switch fileType { case typeZip: return unzip(data, dest) case typeTarGzip: return untgz(data, dest) default: return fmt.Errorf("unknown or unsupported file type") } } func cleanTarPath(path string) string { // remove prefix ./ or ../ path = strings.TrimPrefix(path, "./") path = strings.TrimPrefix(path, "../") // normalize path path = filepath.Clean(path) // transfer delimiters to system std path = filepath.FromSlash(path) // remove prefix path delimiters path = strings.TrimPrefix(path, string(os.PathSeparator)) return path } func cleanup(root string) error { dirEntryList, err := os.ReadDir(root) if err != nil { return err } for _, dirEntry := range dirEntryList { err = os.RemoveAll(filepath.Join(root, dirEntry.Name())) if err != nil { return err } } return nil } func moveDir(src string, dst string) error { dirEntryList, err := os.ReadDir(src) if err != nil { return err } if len(dirEntryList) == 1 && dirEntryList[0].IsDir() { src = filepath.Join(src, dirEntryList[0].Name()) log.Debugln("match the singleRoot: %s", src) dirEntryList, err = os.ReadDir(src) if err != nil { return err } } for _, dirEntry := range dirEntryList { srcPath := filepath.Join(src, dirEntry.Name()) dstPath := filepath.Join(dst, dirEntry.Name()) err = os.Rename(srcPath, dstPath) if err != nil { // Fallback for invalid cross-device link (errno:18). if errors.Is(err, syscall.Errno(18)) { err = copyAll(srcPath, dstPath) _ = os.RemoveAll(srcPath) } } if err != nil { return err } } return nil } // copyAll copy the src path and any children it contains to dst // modify from [os.CopyFS] func copyAll(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } fpath, err := filepath.Rel(src, path) if err != nil { return err } newPath := filepath.Join(dst, fpath) switch info.Mode().Type() { case os.ModeDir: return os.MkdirAll(newPath, info.Mode().Perm()) case os.ModeSymlink: target, err := os.Readlink(path) if err != nil { return err } return os.Symlink(target, newPath) case 0: r, err := os.Open(path) if err != nil { return err } defer r.Close() w, err := os.OpenFile(newPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode().Perm()) if err != nil { return err } if _, err := io.Copy(w, r); err != nil { w.Close() return &os.PathError{Op: "Copy", Path: newPath, Err: err} } return w.Close() default: return &os.PathError{Op: "CopyFS", Path: path, Err: os.ErrInvalid} } }) } func inDest(fpath, dest string) bool { if rel, err := filepath.Rel(dest, fpath); err == nil { if filepath.IsLocal(rel) { return true } } return false } ================================================ FILE: core/Clash.Meta/component/updater/utils.go ================================================ package updater import ( "context" "io" "os" "time" mihomoHttp "github.com/metacubex/mihomo/component/http" "github.com/metacubex/http" ) const defaultHttpTimeout = time.Second * 90 func downloadForBytes(url string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultHttpTimeout) defer cancel() resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) } func saveFile(bytes []byte, path string) error { return os.WriteFile(path, bytes, 0o644) } ================================================ FILE: core/Clash.Meta/component/wildcard/wildcard.go ================================================ // Package wildcard modified IGLOU-EU/go-wildcard to support: // // `*` matches zero or more characters // `?` matches exactly one character // // The original go-wildcard library used `.` to match exactly one character, and `?` to match zero or one character. // `.` is a valid delimiter in domain name matching and should not be used as a wildcard. // The `?` matching logic strictly matches only one character in most scenarios. // So, the `?` matching logic in the original go-wildcard library has been removed and its wildcard `.` has been replaced with `?`. package wildcard // copy and modified from https://github.com/IGLOU-EU/go-wildcard/tree/ce22b7af48e487517a492d3727d9386492043e21 // which is licensed under OpenBSD's ISC-style license. // Copyright (c) 2023 Iglou.eu contact@iglou.eu Copyright (c) 2023 Adrien Kara adrien@iglou.eu func Match(pattern, s string) bool { if pattern == "" { return s == pattern } if pattern == "*" || s == pattern { return true } return matchByString(pattern, s) } func matchByString(pattern, s string) bool { var patternIndex, sIndex, lastStar int patternLen := len(pattern) sLen := len(s) star := -1 Loop: if sIndex >= sLen { goto checkPattern } if patternIndex >= patternLen { if star != -1 { patternIndex = star + 1 lastStar++ sIndex = lastStar goto Loop } return false } switch pattern[patternIndex] { case '?': // It matches any single character. So, we don't need to check anything. case '*': // '*' matches zero or more characters. Store its position and increment the pattern index. star = patternIndex lastStar = sIndex patternIndex++ goto Loop default: // If the characters don't match, check if there was a previous '*' to backtrack. if pattern[patternIndex] != s[sIndex] { if star != -1 { patternIndex = star + 1 lastStar++ sIndex = lastStar goto Loop } return false } } patternIndex++ sIndex++ goto Loop // Check if the remaining pattern characters are '*', which can match the end of the string. checkPattern: if patternIndex < patternLen { if pattern[patternIndex] == '*' { patternIndex++ goto checkPattern } } return patternIndex == patternLen } ================================================ FILE: core/Clash.Meta/component/wildcard/wildcard_test.go ================================================ package wildcard /* * copy and modified from https://github.com/IGLOU-EU/go-wildcard/tree/ce22b7af48e487517a492d3727d9386492043e21 * * Copyright (c) 2023 Iglou.eu * Copyright (c) 2023 Adrien Kara * * Licensed under the BSD 3-Clause License, */ import ( "testing" ) // TestMatch validates the logic of wild card matching, // it need to support '*', '?' and only validate for byte comparison // over string, not rune or grapheme cluster func TestMatch(t *testing.T) { cases := []struct { s string pattern string result bool }{ {"", "", true}, {"", "*", true}, {"", "**", true}, {"", "?", false}, {"", "?*", false}, {"", "*?", false}, {"a", "", false}, {"a", "a", true}, {"a", "*", true}, {"a", "**", true}, {"a", "?", true}, {"a", "?*", true}, {"a", "*?", true}, {"match the exact string", "match the exact string", true}, {"do not match a different string", "this is a different string", false}, {"Match The Exact String WITH DIFFERENT CASE", "Match The Exact String WITH DIFFERENT CASE", true}, {"do not match a different string WITH DIFFERENT CASE", "this is a different string WITH DIFFERENT CASE", false}, {"Do Not Match The Exact String With Different Case", "do not match the exact string with different case", false}, {"match an emoji 😃", "match an emoji 😃", true}, {"do not match because of different emoji 😃", "do not match because of different emoji 😄", false}, {"🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", "🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", true}, {"🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", "🦌🐇🦡🐿️🌲🌳🏰🌳🌲🌞🌧️❄️🌬️⛈️🔥🎄🎅🎁🎉🎊🥳👨‍👩‍👧‍👦💏👪💖👩‍💼🛀", false}, {"match a string with a *", "match a string *", true}, {"match a string with a * at the beginning", "* at the beginning", true}, {"match a string with two *", "match * with *", true}, {"do not match a string with extra and a *", "do not match a string * with more", false}, {"match a string with a ?", "match ? string with a ?", true}, {"match a string with a ? at the beginning", "?atch a string with a ? at the beginning", true}, {"match a string with two ?", "match a ??ring with two ?", true}, {"do not match a string with extra ?", "do not match a string with extra ??", false}, {"abc.edf.hjg", "abc.edf.hjg", true}, {"abc.edf.hjg", "ab.cedf.hjg", false}, {"abc.edf.hjg", "abc.edfh.jg", false}, {"abc.edf.hjg", "abc.edf.hjq", false}, {"abc.edf.hjg", "abc.*.hjg", true}, {"abc.edf.hjg", "abc.*.hjq", false}, {"abc.edf.hjg", "abc*hjg", true}, {"abc.edf.hjg", "abc*hjq", false}, {"abc.edf.hjg", "a*g", true}, {"abc.edf.hjg", "a*q", false}, {"abc.edf.hjg", "ab?.edf.hjg", true}, {"abc.edf.hjg", "?b?.edf.hjg", true}, {"abc.edf.hjg", "??c.edf.hjg", true}, {"abc.edf.hjg", "a??.edf.hjg", true}, {"abc.edf.hjg", "ab??.edf.hjg", false}, {"abc.edf.hjg", "??.edf.hjg", false}, } for i, c := range cases { t.Run(c.s, func(t *testing.T) { result := Match(c.pattern, c.s) if c.result != result { t.Errorf("Test %d: Expected `%v`, found `%v`; With Pattern: `%s` and String: `%s`", i+1, c.result, result, c.pattern, c.s) } }) } } func match(pattern, name string) bool { // https://research.swtch.com/glob px := 0 nx := 0 nextPx := 0 nextNx := 0 for px < len(pattern) || nx < len(name) { if px < len(pattern) { c := pattern[px] switch c { default: // ordinary character if nx < len(name) && name[nx] == c { px++ nx++ continue } case '?': // single-character wildcard if nx < len(name) { px++ nx++ continue } case '*': // zero-or-more-character wildcard // Try to match at nx. // If that doesn't work out, // restart at nx+1 next. nextPx = px nextNx = nx + 1 px++ continue } } // Mismatch. Maybe restart. if 0 < nextNx && nextNx <= len(name) { px = nextPx nx = nextNx continue } return false } // Matched all of pattern to all of name. Success. return true } func FuzzMatch(f *testing.F) { f.Fuzz(func(t *testing.T, pattern, name string) { result1 := Match(pattern, name) result2 := match(pattern, name) if result1 != result2 { t.Fatalf("Match failed for pattern `%s` and name `%s`", pattern, name) } }) } func BenchmarkMatch(b *testing.B) { cases := []struct { s string pattern string result bool }{ {"abc.edf.hjg", "abc.edf.hjg", true}, {"abc.edf.hjg", "ab.cedf.hjg", false}, {"abc.edf.hjg", "abc.edfh.jg", false}, {"abc.edf.hjg", "abc.edf.hjq", false}, {"abc.edf.hjg", "abc.*.hjg", true}, {"abc.edf.hjg", "abc.*.hjq", false}, {"abc.edf.hjg", "abc*hjg", true}, {"abc.edf.hjg", "abc*hjq", false}, {"abc.edf.hjg", "a*g", true}, {"abc.edf.hjg", "a*q", false}, {"abc.edf.hjg", "ab?.edf.hjg", true}, {"abc.edf.hjg", "?b?.edf.hjg", true}, {"abc.edf.hjg", "??c.edf.hjg", true}, {"abc.edf.hjg", "a??.edf.hjg", true}, {"abc.edf.hjg", "ab??.edf.hjg", false}, {"abc.edf.hjg", "??.edf.hjg", false}, {"r4.cdn-aa-wow-this-is-long-a1.video-yajusenpai1145141919810-oh-hell-yeah-this-is-also-very-long-and-sukka-the-fox-has-a-very-big-fluffy-fox-tail-ao-wu-ao-wu-regex-and-wildcard-both-might-have-deadly-back-tracing-issue-be-careful-or-use-linear-matching.com", "*.cdn-*-*.video**.com", true}, } b.Run("Match", func(b *testing.B) { for i := 0; i < b.N; i++ { for _, c := range cases { result := Match(c.pattern, c.s) if c.result != result { b.Errorf("Test %d: Expected `%v`, found `%v`; With Pattern: `%s` and String: `%s`", i+1, c.result, result, c.pattern, c.s) } } } }) b.Run("match", func(b *testing.B) { for i := 0; i < b.N; i++ { for _, c := range cases { result := match(c.pattern, c.s) if c.result != result { b.Errorf("Test %d: Expected `%v`, found `%v`; With Pattern: `%s` and String: `%s`", i+1, c.result, result, c.pattern, c.s) } } } }) } ================================================ FILE: core/Clash.Meta/config/config.go ================================================ package config import ( "errors" "fmt" "net" "net/netip" "net/url" "path/filepath" "strings" "time" _ "unsafe" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/common/orderedmap" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/yaml" "github.com/metacubex/mihomo/component/auth" "github.com/metacubex/mihomo/component/cidr" "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/sniffer" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" snifferTypes "github.com/metacubex/mihomo/constant/sniffer" "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/listener" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/log" R "github.com/metacubex/mihomo/rules" RC "github.com/metacubex/mihomo/rules/common" RP "github.com/metacubex/mihomo/rules/provider" RW "github.com/metacubex/mihomo/rules/wrapper" T "github.com/metacubex/mihomo/tunnel" "golang.org/x/exp/slices" ) // General config type General struct { Inbound Mode T.TunnelMode `json:"mode"` UnifiedDelay bool `json:"unified-delay"` LogLevel log.LogLevel `json:"log-level"` IPv6 bool `json:"ipv6"` Interface string `json:"interface-name"` RoutingMark int `json:"routing-mark"` GeoXUrl GeoXUrl `json:"geox-url"` GeoAutoUpdate bool `json:"geo-auto-update"` GeoUpdateInterval int `json:"geo-update-interval"` GeodataMode bool `json:"geodata-mode"` GeodataLoader string `json:"geodata-loader"` GeositeMatcher string `json:"geosite-matcher"` TCPConcurrent bool `json:"tcp-concurrent"` FindProcessMode process.FindProcessMode `json:"find-process-mode"` Sniffing bool `json:"sniffing"` GlobalClientFingerprint string `json:"global-client-fingerprint"` GlobalUA string `json:"global-ua"` ETagSupport bool `json:"etag-support"` KeepAliveIdle int `json:"keep-alive-idle"` KeepAliveInterval int `json:"keep-alive-interval"` DisableKeepAlive bool `json:"disable-keep-alive"` } // Inbound config type Inbound 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"` Tun LC.Tun `json:"tun"` TuicServer LC.TuicServer `json:"tuic-server"` ShadowSocksConfig string `json:"ss-config"` VmessConfig string `json:"vmess-config"` Authentication []string `json:"authentication"` SkipAuthPrefixes []netip.Prefix `json:"skip-auth-prefixes"` LanAllowedIPs []netip.Prefix `json:"lan-allowed-ips"` LanDisAllowedIPs []netip.Prefix `json:"lan-disallowed-ips"` AllowLan bool `json:"allow-lan"` BindAddress string `json:"bind-address"` InboundTfo bool `json:"inbound-tfo"` InboundMPTCP bool `json:"inbound-mptcp"` } // GeoXUrl config type GeoXUrl struct { GeoIp string `json:"geo-ip"` Mmdb string `json:"mmdb"` ASN string `json:"asn"` GeoSite string `json:"geo-site"` } // Controller config type Controller struct { ExternalController string ExternalControllerTLS string ExternalControllerUnix string ExternalControllerPipe string ExternalUI string ExternalUIURL string ExternalUIName string ExternalDohServer string Secret string Cors Cors } type Cors struct { AllowOrigins []string AllowPrivateNetwork bool } // Experimental config type Experimental struct { QUICGoDisableGSO bool QUICGoDisableECN bool IP4PEnable bool } // IPTables config type IPTables struct { Enable bool InboundInterface string Bypass []string DnsRedirect bool } // NTP config type NTP struct { Enable bool Server string Port int Interval int DialerProxy string WriteToSystem bool } // DNS config type DNS struct { Enable bool PreferH3 bool IPv6 bool IPv6Timeout uint UseHosts bool UseSystemHosts bool NameServer []dns.NameServer Fallback []dns.NameServer FallbackIPFilter []C.IpMatcher FallbackDomainFilter []C.DomainMatcher Listen string EnhancedMode C.DNSMode DefaultNameserver []dns.NameServer CacheAlgorithm string CacheMaxSize int FakeIPRange netip.Prefix FakeIPPool *fakeip.Pool FakeIPRange6 netip.Prefix FakeIPPool6 *fakeip.Pool FakeIPSkipper *fakeip.Skipper FakeIPTTL int NameServerPolicy []dns.Policy ProxyServerNameserver []dns.NameServer ProxyServerPolicy []dns.Policy DirectNameServer []dns.NameServer DirectFollowPolicy bool } // Profile config type Profile struct { StoreSelected bool StoreFakeIP bool } // TLS config type TLS struct { Certificate string PrivateKey string ClientAuthType string ClientAuthCert string EchKey string CustomTrustCert []string } // Config is mihomo config manager type Config struct { General *General Controller *Controller Experimental *Experimental IPTables *IPTables NTP *NTP DNS *DNS Hosts *trie.DomainTrie[resolver.HostValue] Profile *Profile Rules []C.Rule SubRules map[string][]C.Rule Users []auth.AuthUser Proxies map[string]C.Proxy Listeners map[string]C.InboundListener Providers map[string]P.ProxyProvider RuleProviders map[string]P.RuleProvider Tunnels []LC.Tunnel Sniffer *sniffer.Config TLS *TLS } type RawCors struct { AllowOrigins []string `yaml:"allow-origins" json:"allow-origins"` AllowPrivateNetwork bool `yaml:"allow-private-network" json:"allow-private-network"` } type RawDNS struct { Enable bool `yaml:"enable" json:"enable"` PreferH3 bool `yaml:"prefer-h3" json:"prefer-h3"` IPv6 bool `yaml:"ipv6" json:"ipv6"` IPv6Timeout uint `yaml:"ipv6-timeout" json:"ipv6-timeout"` UseHosts bool `yaml:"use-hosts" json:"use-hosts"` UseSystemHosts bool `yaml:"use-system-hosts" json:"use-system-hosts"` RespectRules bool `yaml:"respect-rules" json:"respect-rules"` NameServer []string `yaml:"nameserver" json:"nameserver"` Fallback []string `yaml:"fallback" json:"fallback"` FallbackFilter RawFallbackFilter `yaml:"fallback-filter" json:"fallback-filter"` Listen string `yaml:"listen" json:"listen"` EnhancedMode C.DNSMode `yaml:"enhanced-mode" json:"enhanced-mode"` FakeIPRange string `yaml:"fake-ip-range" json:"fake-ip-range"` FakeIPRange6 string `yaml:"fake-ip-range6" json:"fake-ip-range6"` FakeIPFilter []string `yaml:"fake-ip-filter" json:"fake-ip-filter"` FakeIPFilterMode C.FilterMode `yaml:"fake-ip-filter-mode" json:"fake-ip-filter-mode"` FakeIPTTL int `yaml:"fake-ip-ttl" json:"fake-ip-ttl"` DefaultNameserver []string `yaml:"default-nameserver" json:"default-nameserver"` CacheAlgorithm string `yaml:"cache-algorithm" json:"cache-algorithm"` CacheMaxSize int `yaml:"cache-max-size" json:"cache-max-size"` NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy" json:"nameserver-policy"` ProxyServerNameserver []string `yaml:"proxy-server-nameserver" json:"proxy-server-nameserver"` ProxyServerNameserverPolicy *orderedmap.OrderedMap[string, any] `yaml:"proxy-server-nameserver-policy" json:"proxy-server-nameserver-policy"` DirectNameServer []string `yaml:"direct-nameserver" json:"direct-nameserver"` DirectNameServerFollowPolicy bool `yaml:"direct-nameserver-follow-policy" json:"direct-nameserver-follow-policy"` } type RawFallbackFilter struct { GeoIP bool `yaml:"geoip" json:"geoip"` GeoIPCode string `yaml:"geoip-code" json:"geoip-code"` IPCIDR []string `yaml:"ipcidr" json:"ipcidr"` Domain []string `yaml:"domain" json:"domain"` GeoSite []string `yaml:"geosite" json:"geosite"` } type RawClashForAndroid struct { AppendSystemDNS bool `yaml:"append-system-dns" json:"append-system-dns"` UiSubtitlePattern string `yaml:"ui-subtitle-pattern" json:"ui-subtitle-pattern"` } type RawNTP struct { Enable bool `yaml:"enable" json:"enable"` Server string `yaml:"server" json:"server"` Port int `yaml:"port" json:"port"` Interval int `yaml:"interval" json:"interval"` DialerProxy string `yaml:"dialer-proxy" json:"dialer-proxy"` WriteToSystem bool `yaml:"write-to-system" json:"write-to-system"` } type RawTun struct { Enable bool `yaml:"enable" json:"enable"` Device string `yaml:"device" json:"device"` Stack C.TUNStack `yaml:"stack" json:"stack"` DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` AutoRoute bool `yaml:"auto-route" json:"auto-route"` AutoDetectInterface bool `yaml:"auto-detect-interface"` MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` GSO bool `yaml:"gso" json:"gso,omitempty"` GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` //Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` IPRoute2TableIndex int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` IPRoute2RuleIndex int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` AutoRedirectIPRoute2FallbackRuleIndex int `yaml:"auto-redirect-iproute2-fallback-rule-index" json:"auto-redirect-iproute2-fallback-rule-index,omitempty"` LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"` StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` RouteExcludeAddress []netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` RouteExcludeAddressSet []string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` ExcludeSrcPort []uint16 `yaml:"exclude-src-port" json:"exclude-src-port,omitempty"` ExcludeSrcPortRange []string `yaml:"exclude-src-port-range" json:"exclude-src-port-range,omitempty"` ExcludeDstPort []uint16 `yaml:"exclude-dst-port" json:"exclude-dst-port,omitempty"` ExcludeDstPortRange []string `yaml:"exclude-dst-port-range" json:"exclude-dst-port-range,omitempty"` IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` // darwin special config RecvMsgX bool `yaml:"recvmsgx" json:"recvmsgx,omitempty"` SendMsgX bool `yaml:"sendmsgx" json:"sendmsgx,omitempty"` } type RawTuicServer struct { Enable bool `yaml:"enable" json:"enable"` Listen string `yaml:"listen" json:"listen"` Token []string `yaml:"token" json:"token"` Users map[string]string `yaml:"users" json:"users,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"` MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` CWND int `yaml:"cwnd" json:"cwnd,omitempty"` } type RawIPTables struct { Enable bool `yaml:"enable" json:"enable"` InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` Bypass []string `yaml:"bypass" json:"bypass"` DnsRedirect bool `yaml:"dns-redirect" json:"dns-redirect"` } type RawExperimental struct { Fingerprints []string `yaml:"fingerprints"` QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"` QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"` IP4PEnable bool `yaml:"dialer-ip4p-convert"` } type RawProfile struct { StoreSelected bool `yaml:"store-selected" json:"store-selected"` StoreFakeIP bool `yaml:"store-fake-ip" json:"store-fake-ip"` } type RawGeoXUrl struct { GeoIp string `yaml:"geoip" json:"geoip"` Mmdb string `yaml:"mmdb" json:"mmdb"` ASN string `yaml:"asn" json:"asn"` GeoSite string `yaml:"geosite" json:"geosite"` } type RawSniffer struct { Enable bool `yaml:"enable" json:"enable"` OverrideDest bool `yaml:"override-destination" json:"override-destination"` Sniffing []string `yaml:"sniffing" json:"sniffing"` ForceDomain []string `yaml:"force-domain" json:"force-domain"` SkipSrcAddress []string `yaml:"skip-src-address" json:"skip-src-address"` SkipDstAddress []string `yaml:"skip-dst-address" json:"skip-dst-address"` SkipDomain []string `yaml:"skip-domain" json:"skip-domain"` Ports []string `yaml:"port-whitelist" json:"port-whitelist"` ForceDnsMapping bool `yaml:"force-dns-mapping" json:"force-dns-mapping"` ParsePureIp bool `yaml:"parse-pure-ip" json:"parse-pure-ip"` Sniff map[string]RawSniffingConfig `yaml:"sniff" json:"sniff"` } type RawSniffingConfig struct { Ports []string `yaml:"ports" json:"ports"` OverrideDest *bool `yaml:"override-destination" json:"override-destination"` } type RawTLS struct { Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type"` ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert"` EchKey string `yaml:"ech-key" json:"ech-key"` CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` } type RawConfig struct { Port int `yaml:"port" json:"port"` SocksPort int `yaml:"socks-port" json:"socks-port"` RedirPort int `yaml:"redir-port" json:"redir-port"` TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"` MixedPort int `yaml:"mixed-port" json:"mixed-port"` ShadowSocksConfig string `yaml:"ss-config" json:"ss-config"` VmessConfig string `yaml:"vmess-config" json:"vmess-config"` InboundTfo bool `yaml:"inbound-tfo" json:"inbound-tfo"` InboundMPTCP bool `yaml:"inbound-mptcp" json:"inbound-mptcp"` Authentication []string `yaml:"authentication" json:"authentication"` SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes" json:"skip-auth-prefixes"` LanAllowedIPs []netip.Prefix `yaml:"lan-allowed-ips" json:"lan-allowed-ips"` LanDisAllowedIPs []netip.Prefix `yaml:"lan-disallowed-ips" json:"lan-disallowed-ips"` AllowLan bool `yaml:"allow-lan" json:"allow-lan"` BindAddress string `yaml:"bind-address" json:"bind-address"` Mode T.TunnelMode `yaml:"mode" json:"mode"` UnifiedDelay bool `yaml:"unified-delay" json:"unified-delay"` LogLevel log.LogLevel `yaml:"log-level" json:"log-level"` IPv6 bool `yaml:"ipv6" json:"ipv6"` ExternalController string `yaml:"external-controller" json:"external-controller"` ExternalControllerPipe string `yaml:"external-controller-pipe" json:"external-controller-pipe"` ExternalControllerUnix string `yaml:"external-controller-unix" json:"external-controller-unix"` ExternalControllerTLS string `yaml:"external-controller-tls" json:"external-controller-tls"` ExternalControllerCors RawCors `yaml:"external-controller-cors" json:"external-controller-cors"` ExternalUI string `yaml:"external-ui" json:"external-ui"` ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"` ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"` ExternalDohServer string `yaml:"external-doh-server" json:"external-doh-server"` Secret string `yaml:"secret" json:"secret"` Interface string `yaml:"interface-name" json:"interface-name"` RoutingMark int `yaml:"routing-mark" json:"routing-mark"` Tunnels []LC.Tunnel `yaml:"tunnels" json:"tunnels"` GeoAutoUpdate bool `yaml:"geo-auto-update" json:"geo-auto-update"` GeoUpdateInterval int `yaml:"geo-update-interval" json:"geo-update-interval"` GeodataMode bool `yaml:"geodata-mode" json:"geodata-mode"` GeodataLoader string `yaml:"geodata-loader" json:"geodata-loader"` GeositeMatcher string `yaml:"geosite-matcher" json:"geosite-matcher"` TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` FindProcessMode process.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` GlobalClientFingerprint string `yaml:"global-client-fingerprint" json:"global-client-fingerprint"` GlobalUA string `yaml:"global-ua" json:"global-ua"` ETagSupport bool `yaml:"etag-support" json:"etag-support"` KeepAliveIdle int `yaml:"keep-alive-idle" json:"keep-alive-idle"` KeepAliveInterval int `yaml:"keep-alive-interval" json:"keep-alive-interval"` DisableKeepAlive bool `yaml:"disable-keep-alive" json:"disable-keep-alive"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers" json:"proxy-providers"` RuleProvider map[string]map[string]any `yaml:"rule-providers" json:"rule-providers"` Proxy []map[string]any `yaml:"proxies" json:"proxies"` ProxyGroup []map[string]any `yaml:"proxy-groups" json:"proxy-groups"` Rule []string `yaml:"rules" json:"rule"` SubRules map[string][]string `yaml:"sub-rules" json:"sub-rules"` Listeners []map[string]any `yaml:"listeners" json:"listeners"` Hosts map[string]any `yaml:"hosts" json:"hosts"` DNS RawDNS `yaml:"dns" json:"dns"` NTP RawNTP `yaml:"ntp" json:"ntp"` Tun RawTun `yaml:"tun" json:"tun"` TuicServer RawTuicServer `yaml:"tuic-server" json:"tuic-server"` IPTables RawIPTables `yaml:"iptables" json:"iptables"` Experimental RawExperimental `yaml:"experimental" json:"experimental"` Profile RawProfile `yaml:"profile" json:"profile"` GeoXUrl RawGeoXUrl `yaml:"geox-url" json:"geox-url"` Sniffer RawSniffer `yaml:"sniffer" json:"sniffer"` TLS RawTLS `yaml:"tls" json:"tls"` ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"` } // Parse config func Parse(buf []byte) (*Config, error) { rawCfg, err := UnmarshalRawConfig(buf) if err != nil { return nil, err } return ParseRawConfig(rawCfg) } func DefaultRawConfig() *RawConfig { return &RawConfig{ AllowLan: false, BindAddress: "*", LanAllowedIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}, IPv6: true, Mode: T.Rule, GeoAutoUpdate: false, GeoUpdateInterval: 24, GeodataMode: geodata.GeodataMode(), GeodataLoader: "memconservative", UnifiedDelay: false, Authentication: []string{}, LogLevel: log.INFO, Hosts: map[string]any{}, Rule: []string{}, Proxy: []map[string]any{}, ProxyGroup: []map[string]any{}, TCPConcurrent: false, FindProcessMode: process.FindProcessStrict, GlobalUA: "Clash.Meta/ClashMetaForAndroid/5.0", ETagSupport: true, DNS: RawDNS{ Enable: false, IPv6: false, UseHosts: true, UseSystemHosts: true, IPv6Timeout: 100, EnhancedMode: C.DNSMapping, FakeIPRange: "198.18.0.1/16", FakeIPTTL: 1, FallbackFilter: RawFallbackFilter{ GeoIP: true, GeoIPCode: "CN", IPCIDR: []string{}, GeoSite: []string{}, }, DefaultNameserver: []string{ "114.114.114.114", "223.5.5.5", "8.8.8.8", "1.0.0.1", }, NameServer: []string{ "https://doh.pub/dns-query", "tls://223.5.5.5:853", }, FakeIPFilter: []string{ "dns.msftnsci.com", "www.msftnsci.com", "www.msftconnecttest.com", }, FakeIPFilterMode: C.FilterBlackList, }, NTP: RawNTP{ Enable: false, WriteToSystem: false, Server: "time.apple.com", Port: 123, Interval: 30, }, Tun: RawTun{ Enable: false, Device: "", Stack: C.TunGvisor, DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query AutoRoute: true, AutoDetectInterface: true, Inet6Address: []netip.Prefix{netip.MustParsePrefix("fdfe:dcba:9876::1/126")}, RecvMsgX: true, SendMsgX: false, // In the current implementation, if enabled, the kernel may freeze during multi-thread downloads, so it is disabled by default. }, TuicServer: RawTuicServer{ Enable: false, Token: nil, Users: nil, Certificate: "", PrivateKey: "", Listen: "", CongestionController: "", MaxIdleTime: 15000, AuthenticationTimeout: 1000, ALPN: []string{"h3"}, MaxUdpRelayPacketSize: 1500, }, IPTables: RawIPTables{ Enable: false, InboundInterface: "lo", Bypass: []string{}, DnsRedirect: true, }, Experimental: RawExperimental{ // https://github.com/quic-go/quic-go/issues/4178 // Quic-go currently cannot automatically fall back on platforms that do not support ecn, so this feature is turned off by default. QUICGoDisableECN: true, }, Profile: RawProfile{ StoreSelected: true, }, GeoXUrl: RawGeoXUrl{ Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", ASN: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb", GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", }, Sniffer: RawSniffer{ Enable: false, Sniff: map[string]RawSniffingConfig{}, ForceDomain: []string{}, SkipDomain: []string{}, Ports: []string{}, ForceDnsMapping: true, ParsePureIp: true, OverrideDest: true, }, ExternalUIURL: "https://github.com/Zephyruso/zashboard/releases/latest/download/dist-no-fonts.zip", ExternalControllerCors: RawCors{ AllowOrigins: []string{"*"}, AllowPrivateNetwork: true, }, } } func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { // config with default value rawCfg := DefaultRawConfig() if err := yaml.Unmarshal(buf, rawCfg); err != nil { return nil, err } return rawCfg, nil } func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config := &Config{} log.Infoln("Start initial configuration in progress") //Segment finished in xxm startTime := time.Now() general, err := parseGeneral(rawCfg) if err != nil { return nil, err } config.General = general // We need to temporarily apply some configuration in general and roll back after parsing the complete configuration. // The loading and downloading of geodata in the parseRules and parseRuleProviders rely on these. // This implementation is very disgusting, but there is currently no better solution rollback := temporaryUpdateGeneral(config.General) defer rollback() controller, err := parseController(rawCfg) if err != nil { return nil, err } config.Controller = controller experimental, err := parseExperimental(rawCfg) if err != nil { return nil, err } config.Experimental = experimental iptables, err := parseIPTables(rawCfg) if err != nil { return nil, err } config.IPTables = iptables ntpCfg, err := parseNTP(rawCfg) if err != nil { return nil, err } config.NTP = ntpCfg profile, err := parseProfile(rawCfg) if err != nil { return nil, err } config.Profile = profile tlsCfg, err := parseTLS(rawCfg) if err != nil { return nil, err } config.TLS = tlsCfg proxies, providers, err := parseProxies(rawCfg) if err != nil { return nil, err } config.Proxies = proxies config.Providers = providers listeners, err := parseListeners(rawCfg) if err != nil { return nil, err } config.Listeners = listeners log.Infoln("Geodata Loader mode: %s", geodata.LoaderName()) log.Infoln("Geosite Matcher implementation: %s", geodata.SiteMatcherName()) ruleProviders, err := parseRuleProviders(rawCfg) if err != nil { return nil, err } config.RuleProviders = ruleProviders subRules, err := parseSubRules(rawCfg, proxies, ruleProviders) if err != nil { return nil, err } config.SubRules = subRules rules, err := parseRules(rawCfg.Rule, proxies, ruleProviders, subRules, "rules") if err != nil { return nil, err } config.Rules = rules hosts, err := parseHosts(rawCfg) if err != nil { return nil, err } config.Hosts = hosts parseIPV6(rawCfg) // must before DNS and Tun dnsCfg, err := parseDNS(rawCfg, ruleProviders) if err != nil { return nil, err } config.DNS = dnsCfg err = parseTun(rawCfg.Tun, dnsCfg, config.General) if err != nil { return nil, err } err = parseTuicServer(rawCfg.TuicServer, config.General) if err != nil { return nil, err } config.Users = parseAuthentication(rawCfg.Authentication) config.Tunnels = rawCfg.Tunnels // verify tunnels for _, t := range config.Tunnels { if len(t.Proxy) > 0 { if _, ok := config.Proxies[t.Proxy]; !ok { return nil, fmt.Errorf("tunnel proxy %s not found", t.Proxy) } } } config.Sniffer, err = parseSniffer(rawCfg.Sniffer, ruleProviders) if err != nil { return nil, err } elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm return config, nil } //go:linkname temporaryUpdateGeneral func temporaryUpdateGeneral(general *General) func() func parseGeneral(cfg *RawConfig) (*General, error) { return &General{ Inbound: Inbound{ Port: cfg.Port, SocksPort: cfg.SocksPort, RedirPort: cfg.RedirPort, TProxyPort: cfg.TProxyPort, MixedPort: cfg.MixedPort, ShadowSocksConfig: cfg.ShadowSocksConfig, VmessConfig: cfg.VmessConfig, AllowLan: cfg.AllowLan, SkipAuthPrefixes: cfg.SkipAuthPrefixes, LanAllowedIPs: cfg.LanAllowedIPs, LanDisAllowedIPs: cfg.LanDisAllowedIPs, BindAddress: cfg.BindAddress, InboundTfo: cfg.InboundTfo, InboundMPTCP: cfg.InboundMPTCP, }, UnifiedDelay: cfg.UnifiedDelay, Mode: cfg.Mode, LogLevel: cfg.LogLevel, IPv6: cfg.IPv6, Interface: cfg.Interface, RoutingMark: cfg.RoutingMark, GeoXUrl: GeoXUrl{ GeoIp: cfg.GeoXUrl.GeoIp, Mmdb: cfg.GeoXUrl.Mmdb, ASN: cfg.GeoXUrl.ASN, GeoSite: cfg.GeoXUrl.GeoSite, }, GeoAutoUpdate: cfg.GeoAutoUpdate, GeoUpdateInterval: cfg.GeoUpdateInterval, GeodataMode: cfg.GeodataMode, GeodataLoader: cfg.GeodataLoader, GeositeMatcher: cfg.GeositeMatcher, TCPConcurrent: cfg.TCPConcurrent, FindProcessMode: cfg.FindProcessMode, GlobalClientFingerprint: cfg.GlobalClientFingerprint, GlobalUA: cfg.GlobalUA, ETagSupport: cfg.ETagSupport, KeepAliveIdle: cfg.KeepAliveIdle, KeepAliveInterval: cfg.KeepAliveInterval, DisableKeepAlive: cfg.DisableKeepAlive, }, nil } func parseController(cfg *RawConfig) (*Controller, error) { if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) { return nil, C.Path.ErrNotSafePath(path) } if uiName := cfg.ExternalUIName; uiName != "" && !filepath.IsLocal(uiName) { return nil, fmt.Errorf("external UI name is not local: %s", uiName) } return &Controller{ ExternalController: cfg.ExternalController, ExternalUI: cfg.ExternalUI, ExternalUIURL: cfg.ExternalUIURL, ExternalUIName: cfg.ExternalUIName, Secret: cfg.Secret, ExternalControllerPipe: cfg.ExternalControllerPipe, ExternalControllerUnix: cfg.ExternalControllerUnix, ExternalControllerTLS: cfg.ExternalControllerTLS, ExternalDohServer: cfg.ExternalDohServer, Cors: Cors{ AllowOrigins: cfg.ExternalControllerCors.AllowOrigins, AllowPrivateNetwork: cfg.ExternalControllerCors.AllowPrivateNetwork, }, }, nil } func parseExperimental(cfg *RawConfig) (*Experimental, error) { return &Experimental{ QUICGoDisableGSO: cfg.Experimental.QUICGoDisableGSO, QUICGoDisableECN: cfg.Experimental.QUICGoDisableECN, IP4PEnable: cfg.Experimental.IP4PEnable, }, nil } func parseIPTables(cfg *RawConfig) (*IPTables, error) { return &IPTables{ Enable: cfg.IPTables.Enable, InboundInterface: cfg.IPTables.InboundInterface, Bypass: cfg.IPTables.Bypass, DnsRedirect: cfg.IPTables.DnsRedirect, }, nil } func parseNTP(cfg *RawConfig) (*NTP, error) { return &NTP{ Enable: cfg.NTP.Enable, Server: cfg.NTP.Server, Port: cfg.NTP.Port, Interval: cfg.NTP.Interval, DialerProxy: cfg.NTP.DialerProxy, WriteToSystem: cfg.NTP.WriteToSystem, }, nil } func parseProfile(cfg *RawConfig) (*Profile, error) { return &Profile{ StoreSelected: cfg.Profile.StoreSelected, StoreFakeIP: cfg.Profile.StoreFakeIP, }, nil } func parseTLS(cfg *RawConfig) (*TLS, error) { return &TLS{ Certificate: cfg.TLS.Certificate, PrivateKey: cfg.TLS.PrivateKey, ClientAuthType: cfg.TLS.ClientAuthType, ClientAuthCert: cfg.TLS.ClientAuthCert, EchKey: cfg.TLS.EchKey, CustomTrustCert: cfg.TLS.CustomTrustCert, }, nil } func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]P.ProxyProvider, err error) { proxies = make(map[string]C.Proxy) providersMap = make(map[string]P.ProxyProvider) proxiesConfig := cfg.Proxy groupsConfig := cfg.ProxyGroup providersConfig := cfg.ProxyProvider var ( proxyList []string AllProxies []string hasGlobal bool ) proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect()) proxies["REJECT"] = adapter.NewProxy(outbound.NewReject()) proxies["REJECT-DROP"] = adapter.NewProxy(outbound.NewRejectDrop()) proxies["COMPATIBLE"] = adapter.NewProxy(outbound.NewCompatible()) proxies["PASS"] = adapter.NewProxy(outbound.NewPass()) proxyList = append(proxyList, "DIRECT", "REJECT") // parse proxy for idx, mapping := range proxiesConfig { proxy, err := adapter.ParseProxy(mapping) if err != nil { return nil, nil, fmt.Errorf("proxy %d: %w", idx, err) } if _, exist := proxies[proxy.Name()]; exist { return nil, nil, fmt.Errorf("proxy %s is the duplicate name", proxy.Name()) } proxies[proxy.Name()] = proxy proxyList = append(proxyList, proxy.Name()) AllProxies = append(AllProxies, proxy.Name()) } // keep the original order of ProxyGroups in config file for idx, mapping := range groupsConfig { groupName, existName := mapping["name"].(string) if !existName { return nil, nil, fmt.Errorf("proxy group %d: missing name", idx) } if groupName == "GLOBAL" { hasGlobal = true } proxyList = append(proxyList, groupName) } // check if any loop exists and sort the ProxyGroups if err := proxyGroupsDagSort(groupsConfig); err != nil { return nil, nil, err } var AllProviders []string // parse and initial providers for name, mapping := range providersConfig { if name == provider.ReservedName { return nil, nil, fmt.Errorf("can not defined a provider called `%s`", provider.ReservedName) } pd, err := provider.ParseProxyProvider(name, mapping) if err != nil { return nil, nil, fmt.Errorf("parse proxy provider %s error: %w", name, err) } providersMap[name] = pd AllProviders = append(AllProviders, name) } slices.Sort(AllProxies) slices.Sort(AllProviders) // parse proxy group for idx, mapping := range groupsConfig { group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap, AllProxies, AllProviders) if err != nil { return nil, nil, fmt.Errorf("proxy group[%d]: %w", idx, err) } groupName := group.Name() if _, exist := proxies[groupName]; exist { return nil, nil, fmt.Errorf("proxy group %s: the duplicate name", groupName) } proxies[groupName] = adapter.NewProxy(group) } var ps []C.Proxy for _, v := range proxyList { if proxies[v].Type() == C.Pass { continue } ps = append(ps, proxies[v]) } hc := provider.NewHealthCheck(ps, "", 5000, 0, true, nil) pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) providersMap[provider.ReservedName] = pd if !hasGlobal { global := outboundgroup.NewSelector( &outboundgroup.GroupCommonOption{ Name: "GLOBAL", }, []P.ProxyProvider{pd}, ) proxies["GLOBAL"] = adapter.NewProxy(global) } // validate dialer-proxy references if err := validateDialerProxies(proxies); err != nil { return nil, nil, err } return proxies, providersMap, nil } func parseListeners(cfg *RawConfig) (listeners map[string]C.InboundListener, err error) { listeners = make(map[string]C.InboundListener) for index, mapping := range cfg.Listeners { inboundListener, err := listener.ParseListener(mapping) if err != nil { return nil, fmt.Errorf("proxy %d: %w", index, err) } name := inboundListener.Name() if _, exist := mapping[name]; exist { return nil, fmt.Errorf("listener %s is the duplicate name", name) } listeners[name] = inboundListener } return } func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]P.RuleProvider, err error) { RP.SetTunnel(T.Tunnel) ruleProviders = map[string]P.RuleProvider{} // parse rule provider for name, mapping := range cfg.RuleProvider { rp, err := RP.ParseRuleProvider(name, mapping, R.ParseRule) if err != nil { return nil, err } ruleProviders[name] = rp } return } func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy, ruleProviders map[string]P.RuleProvider) (subRules map[string][]C.Rule, err error) { subRules = map[string][]C.Rule{} for name := range cfg.SubRules { subRules[name] = make([]C.Rule, 0) } for name, rawRules := range cfg.SubRules { if len(name) == 0 { return nil, fmt.Errorf("sub-rule name is empty") } var rules []C.Rule rules, err = parseRules(rawRules, proxies, ruleProviders, subRules, fmt.Sprintf("sub-rules[%s]", name)) if err != nil { return nil, err } subRules[name] = rules } if err = verifySubRule(subRules); err != nil { return nil, err } return } func verifySubRule(subRules map[string][]C.Rule) error { for name := range subRules { err := verifySubRuleCircularReferences(name, subRules, []string{}) if err != nil { return err } } return nil } func verifySubRuleCircularReferences(n string, subRules map[string][]C.Rule, arr []string) error { isInArray := func(v string, array []string) bool { for _, c := range array { if v == c { return true } } return false } arr = append(arr, n) for i, rule := range subRules[n] { if rule.RuleType() == C.SubRules { if _, ok := subRules[rule.Adapter()]; !ok { return fmt.Errorf("sub-rule[%d:%s] error: [%s] not found", i, n, rule.Adapter()) } if isInArray(rule.Adapter(), arr) { arr = append(arr, rule.Adapter()) return fmt.Errorf("sub-rule error: circular references [%s]", strings.Join(arr, "->")) } if err := verifySubRuleCircularReferences(rule.Adapter(), subRules, arr); err != nil { return err } } } return nil } func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders map[string]P.RuleProvider, subRules map[string][]C.Rule, format string) ([]C.Rule, error) { var rules []C.Rule // parse rules for idx, line := range rulesConfig { tp, payload, target, params := RC.ParseRulePayload(line, true) if target == "" { return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line) } if _, ok := proxies[target]; !ok { if tp != "SUB-RULE" { return nil, fmt.Errorf("%s[%d] [%s] error: proxy [%s] not found", format, idx, line, target) } else if _, ok = subRules[target]; !ok { return nil, fmt.Errorf("%s[%d] [%s] error: sub-rule [%s] not found", format, idx, line, target) } } parsed, parseErr := R.ParseRule(tp, payload, target, params, subRules) if parseErr != nil { return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error()) } for _, name := range parsed.ProviderNames() { if _, ok := ruleProviders[name]; !ok { return nil, fmt.Errorf("%s[%d] [%s] error: rule set [%s] not found", format, idx, line, name) } } if format == "rules" { // only wrap top level rules parsed = RW.NewRuleWrapper(parsed) } rules = append(rules, parsed) } return rules, nil } func parseHosts(cfg *RawConfig) (*trie.DomainTrie[resolver.HostValue], error) { tree := trie.New[resolver.HostValue]() // add default hosts hostValue, _ := resolver.NewHostValueByIPs( []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1})}) if err := tree.Insert("localhost", hostValue); err != nil { log.Errorln("insert localhost to host error: %s", err.Error()) } if len(cfg.Hosts) != 0 { for domain, anyValue := range cfg.Hosts { hosts, err := utils.ToStringSlice(anyValue) if err != nil { return nil, err } if len(hosts) == 1 && hosts[0] == "lan" { if addrs, err := net.InterfaceAddrs(); err != nil { log.Errorln("insert lan to host error: %s", err) } else { hosts = make([]string, 0, len(addrs)) for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && !ipnet.IP.IsLinkLocalUnicast() { hosts = append(hosts, ipnet.IP.String()) } } } } value, err := resolver.NewHostValue(hosts) if err != nil { return nil, fmt.Errorf("%s is not a valid value", anyValue) } if value.IsDomain { node := tree.Search(value.Domain) for node != nil && node.Data().IsDomain { if node.Data().Domain == domain { return nil, fmt.Errorf("%s, there is a cycle in domain name mapping", domain) } node = tree.Search(node.Data().Domain) } } _ = tree.Insert(domain, value) } } tree.Optimize() return tree, nil } func hostWithDefaultPort(host string, defPort string) (string, error) { hostname, port, err := net.SplitHostPort(host) if err != nil { if !strings.Contains(err.Error(), "missing port in address") { return "", err } host = host + ":" + defPort if hostname, port, err = net.SplitHostPort(host); err != nil { return "", err } } return net.JoinHostPort(hostname, port), nil } func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.NameServer, error) { var nameservers []dns.NameServer for idx, server := range servers { server = parsePureDNSServer(server) u, err := url.Parse(server) if err != nil { return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) } var proxyName string params := map[string]string{} for _, s := range strings.Split(u.Fragment, "&") { arr := strings.SplitN(s, "=", 2) switch len(arr) { case 1: proxyName = arr[0] case 2: params[arr[0]] = arr[1] } } var addr, dnsNetType string switch u.Scheme { case "udp": addr, err = hostWithDefaultPort(u.Host, "53") dnsNetType = "" // UDP case "tcp": addr, err = hostWithDefaultPort(u.Host, "53") dnsNetType = "tcp" // TCP case "tls": addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "tls" // DNS over TLS case "http", "https": addr, err = hostWithDefaultPort(u.Host, "443") dnsNetType = "https" // DNS over HTTPS if u.Scheme == "http" { addr, err = hostWithDefaultPort(u.Host, "80") } if err == nil { clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} addr = clearURL.String() } case "quic": addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "quic" // DNS over QUIC case "system": dnsNetType = "system" // System DNS case "dhcp": addr = server[len("dhcp://"):] // some special notation cannot be parsed by url dnsNetType = "dhcp" // UDP from DHCP if addr == "system" { // Compatible with old writing "dhcp://system" dnsNetType = "system" addr = "" } case "rcode": dnsNetType = "rcode" addr = u.Host switch addr { case "success", "format_error", "server_failure", "name_error", "not_implemented", "refused": default: err = fmt.Errorf("unsupported RCode type: %s", addr) } default: return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) } if err != nil { return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) } if respectRules && len(proxyName) == 0 { proxyName = dns.RespectRules } nameserver := dns.NameServer{ Net: dnsNetType, Addr: addr, ProxyName: proxyName, Params: params, PreferH3: preferH3, } if slices.ContainsFunc(nameservers, nameserver.Equal) { continue // skip duplicates nameserver } nameservers = append(nameservers, nameserver) } return nameservers, nil } func init() { dns.ParseNameServer = func(servers []string) ([]dns.NameServer, error) { // using by wireguard return parseNameServer(servers, false, false) } } func parsePureDNSServer(server string) string { addPre := func(server string) string { return "udp://" + server } if server == "system" { return "system://" } if ip, err := netip.ParseAddr(server); err != nil { if strings.Contains(server, "://") { return server } return addPre(server) } else { if ip.Is4() { return addPre(server) } else { return addPre("[" + server + "]") } } } func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], ruleProviders map[string]P.RuleProvider, respectRules bool, preferH3 bool) ([]dns.Policy, error) { var policy []dns.Policy for pair := nsPolicy.Oldest(); pair != nil; pair = pair.Next() { k, v := pair.Key, pair.Value servers, err := utils.ToStringSlice(v) if err != nil { return nil, err } nameservers, err := parseNameServer(servers, respectRules, preferH3) if err != nil { return nil, err } kLower := strings.ToLower(k) if strings.Contains(kLower, ",") { if strings.HasPrefix(kLower, "geosite:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, subkey := range subkeys { newKey := "geosite:" + subkey policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } } else if strings.HasPrefix(kLower, "rule-set:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, subkey := range subkeys { newKey := "rule-set:" + subkey policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } } else { subkeys := strings.Split(k, ",") for _, subkey := range subkeys { policy = append(policy, dns.Policy{Domain: subkey, NameServers: nameservers}) } } } else { if strings.HasPrefix(kLower, "geosite:") { policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers}) } else if strings.HasPrefix(kLower, "rule-set:") { policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers}) } else { policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers}) } } } for idx, p := range policy { domain, nameservers := p.Domain, p.NameServers if strings.HasPrefix(domain, "rule-set:") { domainSetName := domain[9:] matcher, err := parseDomainRuleSet(domainSetName, "dns.nameserver-policy", ruleProviders) if err != nil { return nil, err } policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} } else if strings.HasPrefix(domain, "geosite:") { country := domain[8:] matcher, err := RC.NewGEOSITE(country, "dns.nameserver-policy") if err != nil { return nil, err } policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} } else { if _, valid := trie.ValidAndSplitDomain(domain); !valid { return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) } } } return policy, nil } func parseDNS(rawCfg *RawConfig, ruleProviders map[string]P.RuleProvider) (*DNS, error) { cfg := rawCfg.DNS if cfg.Enable && len(cfg.NameServer) == 0 { return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") } if cfg.RespectRules && len(cfg.ProxyServerNameserver) == 0 { return nil, fmt.Errorf("if “respect-rules” is turned on, “proxy-server-nameserver” cannot be empty") } dnsCfg := &DNS{ Enable: cfg.Enable, Listen: cfg.Listen, PreferH3: cfg.PreferH3, IPv6Timeout: cfg.IPv6Timeout, IPv6: cfg.IPv6, UseHosts: cfg.UseHosts, UseSystemHosts: cfg.UseSystemHosts, EnhancedMode: cfg.EnhancedMode, CacheAlgorithm: cfg.CacheAlgorithm, CacheMaxSize: cfg.CacheMaxSize, } var err error if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer, cfg.RespectRules, cfg.PreferH3); err != nil { return nil, err } if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback, cfg.RespectRules, cfg.PreferH3); err != nil { return nil, err } if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, ruleProviders, cfg.RespectRules, cfg.PreferH3); err != nil { return nil, err } if dnsCfg.ProxyServerNameserver, err = parseNameServer(cfg.ProxyServerNameserver, false, cfg.PreferH3); err != nil { return nil, err } if dnsCfg.ProxyServerPolicy, err = parseNameServerPolicy(cfg.ProxyServerNameserverPolicy, ruleProviders, false, cfg.PreferH3); err != nil { return nil, err } if len(dnsCfg.ProxyServerPolicy) != 0 && len(dnsCfg.ProxyServerNameserver) == 0 { return nil, errors.New("disallow empty `proxy-server-nameserver` when `proxy-server-nameserver-policy` is set") } if dnsCfg.DirectNameServer, err = parseNameServer(cfg.DirectNameServer, false, cfg.PreferH3); err != nil { return nil, err } dnsCfg.DirectFollowPolicy = cfg.DirectNameServerFollowPolicy if len(cfg.DefaultNameserver) == 0 { return nil, errors.New("default nameserver should have at least one nameserver") } if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver, false, cfg.PreferH3); err != nil { return nil, err } // check default nameserver is pure ip addr for _, ns := range dnsCfg.DefaultNameserver { if ns.Net == "system" { continue } host, _, err := net.SplitHostPort(ns.Addr) if err != nil || net.ParseIP(host) == nil { u, err := url.Parse(ns.Addr) if err == nil && net.ParseIP(u.Host) == nil { if ip, _, err := net.SplitHostPort(u.Host); err != nil || net.ParseIP(ip) == nil { return nil, errors.New("default nameserver should be pure IP") } } } } if cfg.FakeIPRange != "" { dnsCfg.FakeIPRange, err = netip.ParsePrefix(cfg.FakeIPRange) if err != nil { return nil, err } if !dnsCfg.FakeIPRange.Addr().Is4() { return nil, errors.New("dns.fake-ip-range must be a IPv4 prefix") } } if cfg.FakeIPRange6 != "" { dnsCfg.FakeIPRange6, err = netip.ParsePrefix(cfg.FakeIPRange6) if err != nil { return nil, err } if !dnsCfg.FakeIPRange6.Addr().Is6() { return nil, errors.New("dns.fake-ip-range6 must be a IPv6 prefix") } } if cfg.EnhancedMode == C.DNSFakeIP { var fakeIPTrie *trie.DomainTrie[struct{}] if len(dnsCfg.Fallback) != 0 { fakeIPTrie = trie.New[struct{}]() for _, fb := range dnsCfg.Fallback { if net.ParseIP(fb.Addr) != nil { continue } _ = fakeIPTrie.Insert(fb.Addr, struct{}{}) } } skipper := &fakeip.Skipper{Mode: cfg.FakeIPFilterMode} if cfg.FakeIPFilterMode == C.FilterRule { rules, err := parseFakeIPRules(cfg.FakeIPFilter, ruleProviders) if err != nil { return nil, err } skipper.Rules = rules } else { host, err := parseDomain(cfg.FakeIPFilter, fakeIPTrie, "dns.fake-ip-filter", ruleProviders) if err != nil { return nil, err } skipper.Host = host } dnsCfg.FakeIPSkipper = skipper dnsCfg.FakeIPTTL = cfg.FakeIPTTL if dnsCfg.FakeIPRange.IsValid() { pool, err := fakeip.New(fakeip.Options{ IPNet: dnsCfg.FakeIPRange, Size: 1000, Persistence: rawCfg.Profile.StoreFakeIP, }) if err != nil { return nil, err } dnsCfg.FakeIPPool = pool } if dnsCfg.FakeIPRange6.IsValid() { pool6, err := fakeip.New(fakeip.Options{ IPNet: dnsCfg.FakeIPRange6, Size: 1000, Persistence: rawCfg.Profile.StoreFakeIP, }) if err != nil { return nil, err } dnsCfg.FakeIPPool6 = pool6 } if dnsCfg.FakeIPPool == nil && dnsCfg.FakeIPPool6 == nil { return nil, errors.New("disallow `fake-ip-range` and `fake-ip-range6` both empty with fake-ip mode") } } if len(cfg.Fallback) != 0 { if cfg.FallbackFilter.GeoIP { matcher, err := RC.NewGEOIP(cfg.FallbackFilter.GeoIPCode, "dns.fallback-filter.geoip", false, true) if err != nil { return nil, fmt.Errorf("load GeoIP dns fallback filter error, %w", err) } dnsCfg.FallbackIPFilter = append(dnsCfg.FallbackIPFilter, matcher.DnsFallbackFilter()) } if len(cfg.FallbackFilter.IPCIDR) > 0 { cidrSet := cidr.NewIpCidrSet() for idx, ipcidr := range cfg.FallbackFilter.IPCIDR { err = cidrSet.AddIpCidrForString(ipcidr) if err != nil { return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %w", idx, err) } } err = cidrSet.Merge() if err != nil { return nil, err } matcher := cidrSet // dns.fallback-filter.ipcidr dnsCfg.FallbackIPFilter = append(dnsCfg.FallbackIPFilter, matcher) } if len(cfg.FallbackFilter.Domain) > 0 { domainTrie := trie.New[struct{}]() for idx, domain := range cfg.FallbackFilter.Domain { err = domainTrie.Insert(domain, struct{}{}) if err != nil { return nil, fmt.Errorf("DNS FallbackDomain[%d] format error: %w", idx, err) } } matcher := domainTrie.NewDomainSet() // dns.fallback-filter.domain dnsCfg.FallbackDomainFilter = append(dnsCfg.FallbackDomainFilter, matcher) } if len(cfg.FallbackFilter.GeoSite) > 0 { log.Warnln("replace fallback-filter.geosite with nameserver-policy, it will be removed in the future") for idx, geoSite := range cfg.FallbackFilter.GeoSite { matcher, err := RC.NewGEOSITE(geoSite, "dns.fallback-filter.geosite") if err != nil { return nil, fmt.Errorf("DNS FallbackGeosite[%d] format error: %w", idx, err) } dnsCfg.FallbackDomainFilter = append(dnsCfg.FallbackDomainFilter, matcher) } } } return dnsCfg, nil } func parseFakeIPRules(rawRules []string, ruleProviders map[string]P.RuleProvider) ([]C.Rule, error) { var rules []C.Rule for idx, line := range rawRules { tp, payload, action, params := RC.ParseRulePayload(line, true) action = strings.ToLower(action) if action != fakeip.UseFakeIP && action != fakeip.UseRealIP { return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: invalid action '%s', must be 'fake-ip' or 'real-ip'", idx, line, action) } if tp == "RULE-SET" { if rp, ok := ruleProviders[payload]; !ok { return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule-set '%s' not found", idx, line, payload) } else { switch rp.Behavior() { case P.IPCIDR: return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule-set behavior is %s, must be domain or classical", idx, line, rp.Behavior()) case P.Classical: log.Warnln("%s provider is %s, only matching domain rules in fake-ip-filter", rp.Name(), rp.Behavior()) default: } } } parsed, err := R.ParseRule(tp, payload, action, params, nil) if err != nil { return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: %w", idx, line, err) } if !isDomainRule(parsed.RuleType()) && parsed.RuleType() != C.MATCH { return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule type '%s' not supported, only domain-based rules allowed", idx, line, tp) } rules = append(rules, parsed) } return rules, nil } func isDomainRule(rt C.RuleType) bool { switch rt { case C.Domain, C.DomainSuffix, C.DomainKeyword, C.DomainRegex, C.DomainWildcard, C.GEOSITE, C.RuleSet: return true default: return false } } func parseAuthentication(rawRecords []string) []auth.AuthUser { var users []auth.AuthUser for _, line := range rawRecords { if user, pass, found := strings.Cut(line, ":"); found { users = append(users, auth.AuthUser{User: user, Pass: pass}) } } return users } func parseIPV6(rawCfg *RawConfig) { if !rawCfg.IPv6 || !verifyIP6() { rawCfg.DNS.FakeIPRange6 = "" rawCfg.Tun.Inet6Address = nil } } func parseTun(rawTun RawTun, dns *DNS, general *General) error { tunAddressPrefix := dns.FakeIPRange if !tunAddressPrefix.IsValid() { tunAddressPrefix = netip.MustParsePrefix("198.18.0.1/16") } tunAddressPrefix = netip.PrefixFrom(tunAddressPrefix.Addr(), 30) general.Tun = LC.Tun{ Enable: rawTun.Enable, Device: rawTun.Device, Stack: rawTun.Stack, DNSHijack: rawTun.DNSHijack, AutoRoute: rawTun.AutoRoute, AutoDetectInterface: rawTun.AutoDetectInterface, MTU: rawTun.MTU, GSO: rawTun.GSO, GSOMaxSize: rawTun.GSOMaxSize, Inet4Address: []netip.Prefix{tunAddressPrefix}, Inet6Address: rawTun.Inet6Address, IPRoute2TableIndex: rawTun.IPRoute2TableIndex, IPRoute2RuleIndex: rawTun.IPRoute2RuleIndex, AutoRedirect: rawTun.AutoRedirect, AutoRedirectInputMark: rawTun.AutoRedirectInputMark, AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark, AutoRedirectIPRoute2FallbackRuleIndex: rawTun.AutoRedirectIPRoute2FallbackRuleIndex, LoopbackAddress: rawTun.LoopbackAddress, StrictRoute: rawTun.StrictRoute, RouteAddress: rawTun.RouteAddress, RouteAddressSet: rawTun.RouteAddressSet, RouteExcludeAddress: rawTun.RouteExcludeAddress, RouteExcludeAddressSet: rawTun.RouteExcludeAddressSet, IncludeInterface: rawTun.IncludeInterface, ExcludeInterface: rawTun.ExcludeInterface, IncludeUID: rawTun.IncludeUID, IncludeUIDRange: rawTun.IncludeUIDRange, ExcludeUID: rawTun.ExcludeUID, ExcludeUIDRange: rawTun.ExcludeUIDRange, ExcludeSrcPort: rawTun.ExcludeSrcPort, ExcludeSrcPortRange: rawTun.ExcludeSrcPortRange, ExcludeDstPort: rawTun.ExcludeDstPort, ExcludeDstPortRange: rawTun.ExcludeDstPortRange, IncludeAndroidUser: rawTun.IncludeAndroidUser, IncludePackage: rawTun.IncludePackage, ExcludePackage: rawTun.ExcludePackage, EndpointIndependentNat: rawTun.EndpointIndependentNat, UDPTimeout: rawTun.UDPTimeout, DisableICMPForwarding: rawTun.DisableICMPForwarding, FileDescriptor: rawTun.FileDescriptor, Inet4RouteAddress: rawTun.Inet4RouteAddress, Inet6RouteAddress: rawTun.Inet6RouteAddress, Inet4RouteExcludeAddress: rawTun.Inet4RouteExcludeAddress, Inet6RouteExcludeAddress: rawTun.Inet6RouteExcludeAddress, RecvMsgX: rawTun.RecvMsgX, SendMsgX: rawTun.SendMsgX, } return nil } func parseTuicServer(rawTuic RawTuicServer, general *General) error { general.TuicServer = LC.TuicServer{ Enable: rawTuic.Enable, Listen: rawTuic.Listen, Token: rawTuic.Token, Users: rawTuic.Users, Certificate: rawTuic.Certificate, PrivateKey: rawTuic.PrivateKey, CongestionController: rawTuic.CongestionController, MaxIdleTime: rawTuic.MaxIdleTime, AuthenticationTimeout: rawTuic.AuthenticationTimeout, ALPN: rawTuic.ALPN, MaxUdpRelayPacketSize: rawTuic.MaxUdpRelayPacketSize, CWND: rawTuic.CWND, } return nil } func parseSniffer(snifferRaw RawSniffer, ruleProviders map[string]P.RuleProvider) (*sniffer.Config, error) { snifferConfig := &sniffer.Config{ Enable: snifferRaw.Enable, ForceDnsMapping: snifferRaw.ForceDnsMapping, ParsePureIp: snifferRaw.ParsePureIp, } loadSniffer := make(map[snifferTypes.Type]sniffer.SnifferConfig) if len(snifferRaw.Sniff) != 0 { for sniffType, sniffConfig := range snifferRaw.Sniff { find := false ports, err := utils.NewUnsignedRangesFromList[uint16](sniffConfig.Ports) if err != nil { return nil, err } overrideDest := snifferRaw.OverrideDest if sniffConfig.OverrideDest != nil { overrideDest = *sniffConfig.OverrideDest } for _, snifferType := range snifferTypes.List { if snifferType.String() == strings.ToUpper(sniffType) { find = true loadSniffer[snifferType] = sniffer.SnifferConfig{ Ports: ports, OverrideDest: overrideDest, } } } if !find { return nil, fmt.Errorf("not find the sniffer[%s]", sniffType) } } } else { if snifferConfig.Enable && len(snifferRaw.Sniffing) != 0 { // Deprecated: Use Sniff instead log.Warnln("Deprecated: Use Sniff instead") } globalPorts, err := utils.NewUnsignedRangesFromList[uint16](snifferRaw.Ports) if err != nil { return nil, err } for _, snifferName := range snifferRaw.Sniffing { find := false for _, snifferType := range snifferTypes.List { if snifferType.String() == strings.ToUpper(snifferName) { find = true loadSniffer[snifferType] = sniffer.SnifferConfig{ Ports: globalPorts, OverrideDest: snifferRaw.OverrideDest, } } } if !find { return nil, fmt.Errorf("not find the sniffer[%s]", snifferName) } } } snifferConfig.Sniffers = loadSniffer forceDomain, err := parseDomain(snifferRaw.ForceDomain, nil, "sniffer.force-domain", ruleProviders) if err != nil { return nil, fmt.Errorf("error in force-domain, error:%w", err) } snifferConfig.ForceDomain = forceDomain skipSrcAddress, err := parseIPCIDR(snifferRaw.SkipSrcAddress, nil, "sniffer.skip-src-address", ruleProviders) if err != nil { return nil, fmt.Errorf("error in skip-src-address, error:%w", err) } snifferConfig.SkipSrcAddress = skipSrcAddress skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-dst-address", ruleProviders) if err != nil { return nil, fmt.Errorf("error in skip-dst-address, error:%w", err) } snifferConfig.SkipDstAddress = skipDstAddress skipDomain, err := parseDomain(snifferRaw.SkipDomain, nil, "sniffer.skip-domain", ruleProviders) if err != nil { return nil, fmt.Errorf("error in skip-domain, error:%w", err) } snifferConfig.SkipDomain = skipDomain return snifferConfig, nil } func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string, ruleProviders map[string]P.RuleProvider) (matchers []C.IpMatcher, err error) { var matcher C.IpMatcher for _, ipcidr := range addresses { ipcidrLower := strings.ToLower(ipcidr) if strings.HasPrefix(ipcidrLower, "geoip:") { subkeys := strings.Split(ipcidr, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, country := range subkeys { matcher, err = RC.NewGEOIP(country, adapterName, false, false) if err != nil { return nil, err } matchers = append(matchers, matcher) } } else if strings.HasPrefix(ipcidrLower, "rule-set:") { subkeys := strings.Split(ipcidr, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, domainSetName := range subkeys { matcher, err = parseIPRuleSet(domainSetName, adapterName, ruleProviders) if err != nil { return nil, err } matchers = append(matchers, matcher) } } else { if cidrSet == nil { cidrSet = cidr.NewIpCidrSet() } err = cidrSet.AddIpCidrForString(ipcidr) if err != nil { return nil, err } } } if !cidrSet.IsEmpty() { err = cidrSet.Merge() if err != nil { return nil, err } matcher = cidrSet matchers = append(matchers, matcher) } return } func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapterName string, ruleProviders map[string]P.RuleProvider) (matchers []C.DomainMatcher, err error) { var matcher C.DomainMatcher for _, domain := range domains { domainLower := strings.ToLower(domain) if strings.HasPrefix(domainLower, "geosite:") { subkeys := strings.Split(domain, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, country := range subkeys { matcher, err = RC.NewGEOSITE(country, adapterName) if err != nil { return nil, err } matchers = append(matchers, matcher) } } else if strings.HasPrefix(domainLower, "rule-set:") { subkeys := strings.Split(domain, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, domainSetName := range subkeys { matcher, err = parseDomainRuleSet(domainSetName, adapterName, ruleProviders) if err != nil { return nil, err } matchers = append(matchers, matcher) } } else { if domainTrie == nil { domainTrie = trie.New[struct{}]() } err = domainTrie.Insert(domain, struct{}{}) if err != nil { return nil, err } } } if !domainTrie.IsEmpty() { matcher = domainTrie.NewDomainSet() matchers = append(matchers, matcher) } return } func parseIPRuleSet(domainSetName string, adapterName string, ruleProviders map[string]P.RuleProvider) (C.IpMatcher, error) { if rp, ok := ruleProviders[domainSetName]; !ok { return nil, fmt.Errorf("not found rule-set: %s", domainSetName) } else { switch rp.Behavior() { case P.Domain: return nil, fmt.Errorf("rule provider type error, except ipcidr,actual %s", rp.Behavior()) case P.Classical: log.Warnln("%s provider is %s, only matching it contain ip rule", rp.Name(), rp.Behavior()) default: } } return RP.NewRuleSet(domainSetName, adapterName, false, true) } func parseDomainRuleSet(domainSetName string, adapterName string, ruleProviders map[string]P.RuleProvider) (C.DomainMatcher, error) { if rp, ok := ruleProviders[domainSetName]; !ok { return nil, fmt.Errorf("not found rule-set: %s", domainSetName) } else { switch rp.Behavior() { case P.IPCIDR: return nil, fmt.Errorf("rule provider type error, except domain,actual %s", rp.Behavior()) case P.Classical: log.Warnln("%s provider is %s, only matching it contain domain rule", rp.Name(), rp.Behavior()) default: } } return RP.NewRuleSet(domainSetName, adapterName, false, true) } ================================================ FILE: core/Clash.Meta/config/initial.go ================================================ package config import ( "fmt" "os" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) // Init prepare necessary files func Init(dir string) error { // initial homedir if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0o777); err != nil { return fmt.Errorf("can't create config directory %s: %s", dir, err.Error()) } } // initial config.yaml if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) { log.Infoln("Can't find config, create a initial config file") f, err := os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("can't create file %s: %s", C.Path.Config(), err.Error()) } f.Write([]byte(`mixed-port: 7890`)) f.Close() } return nil } ================================================ FILE: core/Clash.Meta/config/utils.go ================================================ package config import ( "fmt" "net" "net/netip" "os" "strconv" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/structure" C "github.com/metacubex/mihomo/constant" ) // Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. // Meanwhile, record the original index in the config file. // If loop is detected, return an error with location of loop. func proxyGroupsDagSort(groupsConfig []map[string]any) error { type graphNode struct { indegree int // topological order topo int // the original data in `groupsConfig` data map[string]any // `outdegree` and `from` are used in loop locating outdegree int option *outboundgroup.GroupCommonOption from []string } decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) graph := make(map[string]*graphNode) // Step 1.1 build dependency graph for _, mapping := range groupsConfig { option := &outboundgroup.GroupCommonOption{} if err := decoder.Decode(mapping, option); err != nil { return fmt.Errorf("ProxyGroup %s: %s", option.Name, err.Error()) } groupName := option.Name if node, ok := graph[groupName]; ok { if node.data != nil { return fmt.Errorf("ProxyGroup %s: duplicate group name", groupName) } node.data = mapping node.option = option } else { graph[groupName] = &graphNode{0, -1, mapping, 0, option, nil} } for _, proxy := range option.Proxies { if node, ex := graph[proxy]; ex { node.indegree++ } else { graph[proxy] = &graphNode{1, -1, nil, 0, nil, nil} } } } // Step 1.2 Topological Sort // topological index of **ProxyGroup** index := 0 queue := make([]string, 0) for name, node := range graph { // in the beginning, put nodes that have `node.indegree == 0` into queue. if node.indegree == 0 { queue = append(queue, name) } } // every element in queue have indegree == 0 for ; len(queue) > 0; queue = queue[1:] { name := queue[0] node := graph[name] if node.option != nil { index++ groupsConfig[len(groupsConfig)-index] = node.data if len(node.option.Proxies) == 0 { delete(graph, name) continue } for _, proxy := range node.option.Proxies { child := graph[proxy] child.indegree-- if child.indegree == 0 { queue = append(queue, proxy) } } } delete(graph, name) } // no loop is detected, return sorted ProxyGroup if len(graph) == 0 { return nil } // if loop is detected, locate the loop and throw an error // Step 2.1 rebuild the graph, fill `outdegree` and `from` filed for name, node := range graph { if node.option == nil { continue } if len(node.option.Proxies) == 0 { continue } for _, proxy := range node.option.Proxies { node.outdegree++ child := graph[proxy] if child.from == nil { child.from = make([]string, 0, child.indegree) } child.from = append(child.from, name) } } // Step 2.2 remove nodes outside the loop. so that we have only the loops remain in `graph` queue = make([]string, 0) // initialize queue with node have outdegree == 0 for name, node := range graph { if node.outdegree == 0 { queue = append(queue, name) } } // every element in queue have outdegree == 0 for ; len(queue) > 0; queue = queue[1:] { name := queue[0] node := graph[name] for _, f := range node.from { graph[f].outdegree-- if graph[f].outdegree == 0 { queue = append(queue, f) } } delete(graph, name) } // Step 2.3 report the elements in loop loopElements := make([]string, 0, len(graph)) for name := range graph { loopElements = append(loopElements, name) delete(graph, name) } return fmt.Errorf("loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements) } // validateDialerProxies checks if all dialer-proxy references are valid func validateDialerProxies(proxies map[string]C.Proxy) error { graph := make(map[string]string) // proxy name -> dialer-proxy name // collect all proxies with dialer-proxy configured for name, proxy := range proxies { dialerProxy := proxy.ProxyInfo().DialerProxy if dialerProxy != "" { // validate each dialer-proxy reference _, exist := proxies[dialerProxy] if !exist { return fmt.Errorf("proxy [%s] dialer-proxy [%s] not found", name, dialerProxy) } // build dependency graph graph[name] = dialerProxy } } // perform depth-first search to detect cycles for each proxy for name := range graph { visited := make(map[string]bool, len(graph)) path := make([]string, 0, len(graph)) if validateDialerProxiesHasCycle(name, graph, visited, path) { return fmt.Errorf("proxy [%s] has circular dialer-proxy dependency", name) } } return nil } // validateDialerProxiesHasCycle performs DFS to detect if there's a cycle starting from current proxy func validateDialerProxiesHasCycle(current string, graph map[string]string, visited map[string]bool, path []string) bool { // check if current is already in path (cycle detected) for _, p := range path { if p == current { return true } } // already visited and no cycle if visited[current] { return false } visited[current] = true path = append(path, current) // check dialer-proxy of current proxy if dialerProxy, exists := graph[current]; exists { if validateDialerProxiesHasCycle(dialerProxy, graph, visited, path) { return true } } return false } func verifyIP6() bool { if skip, _ := strconv.ParseBool(os.Getenv("SKIP_SYSTEM_IPV6_CHECK")); skip { return true } if iAddrs, err := net.InterfaceAddrs(); err == nil { for _, addr := range iAddrs { if prefix, err := netip.ParsePrefix(addr.String()); err == nil { if addr := prefix.Addr().Unmap(); addr.Is6() && addr.IsGlobalUnicast() { return true } } } } else { // eg: Calling net.InterfaceAddrs() fails on Android SDK 30 // https://github.com/golang/go/issues/40569 return true // just ignore } return false } ================================================ FILE: core/Clash.Meta/config/utils_test.go ================================================ package config import ( "testing" "github.com/stretchr/testify/assert" ) func TestValidateDialerProxies(t *testing.T) { testCases := []struct { testName string proxy []map[string]any errContains string }{ { testName: "ValidReference", proxy: []map[string]any{ // create proxy with valid dialer-proxy reference {"name": "base-proxy", "type": "socks5", "server": "127.0.0.1", "port": 1080}, {"name": "proxy-with-dialer", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "base-proxy"}, }, errContains: "", }, { testName: "NotFoundReference", proxy: []map[string]any{ // create proxy with non-existent dialer-proxy reference {"name": "proxy-with-dialer", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "non-existent-proxy"}, }, errContains: "not found", }, { testName: "CircularDependency", proxy: []map[string]any{ // create proxy A that references B {"name": "proxy-a", "type": "socks5", "server": "127.0.0.1", "port": 1080, "dialer-proxy": "proxy-c"}, // create proxy B that references C {"name": "proxy-b", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "proxy-a"}, // create proxy C that references A (creates cycle) {"name": "proxy-c", "type": "socks5", "server": "127.0.0.1", "port": 1082, "dialer-proxy": "proxy-a"}, }, errContains: "circular", }, { testName: "ComplexChain", proxy: []map[string]any{ // create a valid chain: proxy-d -> proxy-c -> proxy-b -> proxy-a {"name": "proxy-a", "type": "socks5", "server": "127.0.0.1", "port": 1080}, {"name": "proxy-b", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "proxy-a"}, {"name": "proxy-c", "type": "socks5", "server": "127.0.0.1", "port": 1082, "dialer-proxy": "proxy-b"}, {"name": "proxy-d", "type": "socks5", "server": "127.0.0.1", "port": 1083, "dialer-proxy": "proxy-c"}, }, errContains: "", }, { testName: "EmptyDialerProxy", proxy: []map[string]any{ // create proxy without dialer-proxy {"name": "simple-proxy", "type": "socks5", "server": "127.0.0.1", "port": 1080}, }, errContains: "", }, { testName: "SelfReference", proxy: []map[string]any{ // create proxy that references itself {"name": "self-proxy", "type": "socks5", "server": "127.0.0.1", "port": 1080, "dialer-proxy": "self-proxy"}, }, errContains: "circular", }, } for _, testCase := range testCases { t.Run(testCase.testName, func(t *testing.T) { config := RawConfig{Proxy: testCase.proxy} _, _, err := parseProxies(&config) if testCase.errContains == "" { assert.NoError(t, err, testCase.testName) } else { assert.ErrorContains(t, err, testCase.errContains, testCase.testName) } }) } } ================================================ FILE: core/Clash.Meta/constant/adapters.go ================================================ package constant import ( "context" "errors" "fmt" "net" "net/netip" "sync" "time" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/dialer" ) // Adapter Type const ( Direct AdapterType = iota Reject RejectDrop Compatible Pass Dns Relay Selector Fallback URLTest LoadBalance Shadowsocks ShadowsocksR Snell Socks5 Http Vmess Vless Trojan Hysteria Hysteria2 WireGuard Tuic Ssh Mieru AnyTLS Sudoku Masque TrustTunnel ) const ( DefaultTCPTimeout = dialer.DefaultTCPTimeout DefaultUDPTimeout = dialer.DefaultUDPTimeout DefaultDropTime = 12 * DefaultTCPTimeout DefaultTLSTimeout = DefaultTCPTimeout ) var DefaultTestURL = "https://www.gstatic.com/generate_204" var ErrNotSupport = errors.New("no support") type Connection interface { Chains() Chain ProviderChains() Chain AppendToChains(adapter ProxyAdapter) RemoteDestination() string } type Chain []string func (c Chain) String() string { switch len(c) { case 0: return "" case 1: return c[0] default: return fmt.Sprintf("%s[%s]", c[len(c)-1], c[0]) } } func (c Chain) Last() string { switch len(c) { case 0: return "" default: return c[len(c)-1] } } type Conn interface { N.ExtendedConn Connection } type PacketConn interface { N.EnhancePacketConn Connection ResolveUDP(ctx context.Context, metadata *Metadata) error } type Dialer interface { DialContext(ctx context.Context, network, address string) (net.Conn, error) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) } type ProxyInfo struct { XUDP bool TFO bool MPTCP bool SMUX bool Interface string RoutingMark int ProviderName string DialerProxy string } type ProxyAdapter interface { Name() string Type() AdapterType Addr() string SupportUDP() bool // ProxyInfo contains some extra information maybe useful for MarshalJSON ProxyInfo() ProxyInfo MarshalJSON() ([]byte, error) // DialContext return a C.Conn with protocol which // contains multiplexing-related reuse logic (if any) DialContext(ctx context.Context, metadata *Metadata) (Conn, error) ListenPacketContext(ctx context.Context, metadata *Metadata) (PacketConn, error) // SupportUOT return UDP over TCP support SupportUOT() bool // IsL3Protocol return ProxyAdapter working in L3 (tell dns module not pass the domain to avoid loopback) IsL3Protocol(metadata *Metadata) bool // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. Unwrap(metadata *Metadata, touch bool) Proxy // Close releasing associated resources Close() error } type DelayHistory struct { Time time.Time `json:"time"` Delay uint16 `json:"delay"` } type ProxyState struct { Alive bool `json:"alive"` History []DelayHistory `json:"history"` } type DelayHistoryStoreType int type Proxy interface { ProxyAdapter Adapter() ProxyAdapter AliveForTestUrl(url string) bool DelayHistory() []DelayHistory ExtraDelayHistories() map[string]ProxyState LastDelayForTestUrl(url string) uint16 URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (uint16, error) } // AdapterType is enum of adapter type type AdapterType int func (at AdapterType) String() string { switch at { case Direct: return "Direct" case Reject: return "Reject" case RejectDrop: return "RejectDrop" case Compatible: return "Compatible" case Pass: return "Pass" case Dns: return "Dns" case Shadowsocks: return "Shadowsocks" case ShadowsocksR: return "ShadowsocksR" case Snell: return "Snell" case Socks5: return "Socks5" case Http: return "Http" case Vmess: return "Vmess" case Vless: return "Vless" case Trojan: return "Trojan" case Hysteria: return "Hysteria" case Hysteria2: return "Hysteria2" case WireGuard: return "WireGuard" case Tuic: return "Tuic" case Ssh: return "Ssh" case Mieru: return "Mieru" case AnyTLS: return "AnyTLS" case Sudoku: return "Sudoku" case Masque: return "Masque" case TrustTunnel: return "TrustTunnel" case Relay: return "Relay" case Selector: return "Selector" case Fallback: return "Fallback" case URLTest: return "URLTest" case LoadBalance: return "LoadBalance" default: return "Unknown" } } // UDPPacket contains the data of UDP packet, and offers control/info of UDP packet's source type UDPPacket interface { // Data get the payload of UDP Packet Data() []byte // WriteBack writes the payload with source IP/Port equals addr // - variable source IP/Port is important to STUN // - if addr is not provided, WriteBack will write out UDP packet with SourceIP/Port equals to original Target, // this is important when using Fake-IP. WriteBack // Drop call after packet is used, could recycle buffer in this function. Drop() // LocalAddr returns the source IP/Port of packet LocalAddr() net.Addr } type UDPPacketInAddr interface { InAddr() net.Addr } // PacketAdapter is a UDP Packet adapter for socks/redir/tun type PacketAdapter interface { UDPPacket // Metadata returns destination metadata Metadata() *Metadata // Key is a SNAT key Key() string } type packetAdapter struct { UDPPacket metadata *Metadata key string } // Metadata returns destination metadata func (s *packetAdapter) Metadata() *Metadata { return s.metadata } // Key is a SNAT key func (s *packetAdapter) Key() string { return s.key } func NewPacketAdapter(packet UDPPacket, metadata *Metadata) PacketAdapter { return &packetAdapter{ packet, metadata, packet.LocalAddr().String(), } } type WriteBack interface { WriteBack(b []byte, addr net.Addr) (n int, err error) } type WriteBackProxy interface { WriteBack UpdateWriteBack(wb WriteBack) } type PacketSender interface { // Send will send PacketAdapter nonblocking // the implement must call UDPPacket.Drop() inside Send Send(PacketAdapter) // Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy Process(PacketConn, WriteBackProxy) // Close stop the Process loop Close() // DoSniff will blocking after sniffer work done DoSniff(*Metadata) error // AddMapping add a destination NAT record AddMapping(originMetadata *Metadata, metadata *Metadata) // RestoreReadFrom restore destination NAT for ReadFrom // the implement must ensure returned netip.Add is valid (or just return input addr) RestoreReadFrom(addr netip.Addr) netip.Addr } type NatTable interface { GetOrCreate(key string, maker func() PacketSender) (PacketSender, bool) Delete(key string) GetForLocalConn(lAddr, rAddr string) *net.UDPConn AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) GetOrCreateLockForLocalConn(lAddr string, key string) (*sync.Cond, bool) DeleteForLocalConn(lAddr, key string) DeleteLockForLocalConn(lAddr, key string) } ================================================ FILE: core/Clash.Meta/constant/context.go ================================================ package constant import ( "net" N "github.com/metacubex/mihomo/common/net" "github.com/gofrs/uuid/v5" ) type PlainContext interface { ID() uuid.UUID } type ConnContext interface { PlainContext Metadata() *Metadata Conn() *N.BufferedConn } type PacketConnContext interface { PlainContext Metadata() *Metadata PacketConn() net.PacketConn } ================================================ FILE: core/Clash.Meta/constant/dns.go ================================================ package constant import ( "errors" "strings" ) // DNSModeMapping is a mapping for EnhancedMode enum var DNSModeMapping = map[string]DNSMode{ DNSNormal.String(): DNSNormal, DNSFakeIP.String(): DNSFakeIP, DNSMapping.String(): DNSMapping, } const ( DNSNormal DNSMode = iota DNSFakeIP DNSMapping DNSHosts ) type DNSMode int // UnmarshalText unserialize EnhancedMode func (e *DNSMode) UnmarshalText(data []byte) error { mode, exist := DNSModeMapping[strings.ToLower(string(data))] if !exist { return errors.New("invalid mode") } *e = mode return nil } // MarshalText serialize EnhancedMode func (e DNSMode) MarshalText() ([]byte, error) { return []byte(e.String()), nil } func (e DNSMode) String() string { switch e { case DNSNormal: return "normal" case DNSFakeIP: return "fake-ip" case DNSMapping: return "redir-host" case DNSHosts: return "hosts" default: return "unknown" } } type DNSPrefer int const ( DualStack DNSPrefer = iota IPv4Only IPv6Only IPv4Prefer IPv6Prefer ) var dnsPreferMap = map[string]DNSPrefer{ DualStack.String(): DualStack, IPv4Only.String(): IPv4Only, IPv6Only.String(): IPv6Only, IPv4Prefer.String(): IPv4Prefer, IPv6Prefer.String(): IPv6Prefer, } func (d DNSPrefer) String() string { switch d { case DualStack: return "dual" case IPv4Only: return "ipv4" case IPv6Only: return "ipv6" case IPv4Prefer: return "ipv4-prefer" case IPv6Prefer: return "ipv6-prefer" default: return "dual" } } func (d DNSPrefer) MarshalText() ([]byte, error) { return []byte(d.String()), nil } func (d *DNSPrefer) UnmarshalText(data []byte) error { p, exist := dnsPreferMap[strings.ToLower(string(data))] if !exist { p = DualStack } *d = p return nil } // FilterModeMapping is a mapping for FilterMode enum var FilterModeMapping = map[string]FilterMode{ FilterBlackList.String(): FilterBlackList, FilterWhiteList.String(): FilterWhiteList, FilterRule.String(): FilterRule, } type FilterMode int const ( FilterBlackList FilterMode = iota FilterWhiteList FilterRule ) func (e FilterMode) String() string { switch e { case FilterBlackList: return "blacklist" case FilterWhiteList: return "whitelist" case FilterRule: return "rule" default: return "unknown" } } func (e FilterMode) MarshalText() ([]byte, error) { return []byte(e.String()), nil } func (e *FilterMode) UnmarshalText(data []byte) error { mode, exist := FilterModeMapping[strings.ToLower(string(data))] if !exist { return errors.New("invalid mode") } *e = mode return nil } type HTTPVersion string const ( // HTTPVersion11 is HTTP/1.1. HTTPVersion11 HTTPVersion = "http/1.1" // HTTPVersion2 is HTTP/2. HTTPVersion2 HTTPVersion = "h2" // HTTPVersion3 is HTTP/3. HTTPVersion3 HTTPVersion = "h3" ) ================================================ FILE: core/Clash.Meta/constant/features/android.go ================================================ //go:build android package features const Android = true ================================================ FILE: core/Clash.Meta/constant/features/android_stub.go ================================================ //go:build !android package features const Android = false ================================================ FILE: core/Clash.Meta/constant/features/goflags.go ================================================ package features import "runtime/debug" var ( GOARM string GOMIPS string GOAMD64 string ) func init() { if info, ok := debug.ReadBuildInfo(); ok { for _, bs := range info.Settings { switch bs.Key { case "GOARM": GOARM = bs.Value case "GOMIPS": GOMIPS = bs.Value case "GOAMD64": GOAMD64 = bs.Value } } } } ================================================ FILE: core/Clash.Meta/constant/features/low_memory.go ================================================ //go:build with_low_memory package features const WithLowMemory = true ================================================ FILE: core/Clash.Meta/constant/features/low_memory_stub.go ================================================ //go:build !with_low_memory package features const WithLowMemory = false ================================================ FILE: core/Clash.Meta/constant/features/no_fake_tcp.go ================================================ //go:build no_fake_tcp package features const NoFakeTCP = true ================================================ FILE: core/Clash.Meta/constant/features/no_fake_tcp_stub.go ================================================ //go:build !no_fake_tcp package features const NoFakeTCP = false ================================================ FILE: core/Clash.Meta/constant/features/tags.go ================================================ package features func Tags() (tags []string) { if WithLowMemory { tags = append(tags, "with_low_memory") } if NoFakeTCP { tags = append(tags, "no_fake_tcp") } if WithGVisor { tags = append(tags, "with_gvisor") } return } ================================================ FILE: core/Clash.Meta/constant/features/version.go ================================================ package features var WindowsMajorVersion uint32 var WindowsMinorVersion uint32 var WindowsBuildNumber uint32 ================================================ FILE: core/Clash.Meta/constant/features/version_windows.go ================================================ package features import "golang.org/x/sys/windows" func init() { version := windows.RtlGetVersion() WindowsMajorVersion = version.MajorVersion WindowsMinorVersion = version.MinorVersion WindowsBuildNumber = version.BuildNumber } ================================================ FILE: core/Clash.Meta/constant/features/with_gvisor.go ================================================ //go:build with_gvisor package features const WithGVisor = true ================================================ FILE: core/Clash.Meta/constant/features/with_gvisor_stub.go ================================================ //go:build !with_gvisor package features const WithGVisor = false ================================================ FILE: core/Clash.Meta/constant/listener.go ================================================ package constant import "net" type Listener interface { RawAddress() string Address() string Close() error } type MultiAddrListener interface { Close() error Config() string AddrList() (addrList []net.Addr) } type InboundListener interface { Name() string Listen(tunnel Tunnel) error Close() error Address() string RawAddress() string Config() InboundConfig } type InboundConfig interface { Name() string Equal(config InboundConfig) bool } ================================================ FILE: core/Clash.Meta/constant/matcher.go ================================================ package constant import "net/netip" type DomainMatcher interface { MatchDomain(domain string) bool } type IpMatcher interface { MatchIp(ip netip.Addr) bool } ================================================ FILE: core/Clash.Meta/constant/metadata.go ================================================ package constant import ( "encoding/json" "fmt" "net" "net/netip" "strconv" ) // SOCKS address types as defined in RFC 1928 section 5. const ( AtypIPv4 AddrType = 1 AtypDomainName AddrType = 3 AtypIPv6 AddrType = 4 ) const ( TCP NetWork = iota UDP ALLNet InvalidNet = 0xff ) const ( HTTP Type = iota HTTPS SOCKS4 SOCKS5 SHADOWSOCKS VMESS VLESS REDIR TPROXY TROJAN TUNNEL TUN TUIC HYSTERIA2 ANYTLS MIERU SUDOKU TRUSTTUNNEL INNER ) type AddrType byte func (a AddrType) String() string { switch a { case AtypIPv4: return "IPv4" case AtypDomainName: return "DomainName" case AtypIPv6: return "IPv6" default: return "Unknown" } } type NetWork int func (n NetWork) String() string { switch n { case TCP: return "tcp" case UDP: return "udp" case ALLNet: return "all" default: return "invalid" } } func (n NetWork) MarshalJSON() ([]byte, error) { return json.Marshal(n.String()) } type Type int func (t Type) String() string { switch t { case HTTP: return "HTTP" case HTTPS: return "HTTPS" case SOCKS4: return "Socks4" case SOCKS5: return "Socks5" case SHADOWSOCKS: return "ShadowSocks" case VMESS: return "Vmess" case VLESS: return "Vless" case REDIR: return "Redir" case TPROXY: return "TProxy" case TROJAN: return "Trojan" case TUNNEL: return "Tunnel" case TUN: return "Tun" case TUIC: return "Tuic" case HYSTERIA2: return "Hysteria2" case ANYTLS: return "AnyTLS" case MIERU: return "Mieru" case SUDOKU: return "Sudoku" case TRUSTTUNNEL: return "TrustTunnel" case INNER: return "Inner" default: return "Unknown" } } func ParseType(t string) (*Type, error) { var res Type switch t { case "HTTP": res = HTTP case "HTTPS": res = HTTPS case "SOCKS4": res = SOCKS4 case "SOCKS5": res = SOCKS5 case "SHADOWSOCKS": res = SHADOWSOCKS case "VMESS": res = VMESS case "VLESS": res = VLESS case "REDIR": res = REDIR case "TPROXY": res = TPROXY case "TROJAN": res = TROJAN case "TUNNEL": res = TUNNEL case "TUN": res = TUN case "TUIC": res = TUIC case "HYSTERIA2": res = HYSTERIA2 case "ANYTLS": res = ANYTLS case "MIERU": res = MIERU case "SUDOKU": res = SUDOKU case "TRUSTTUNNEL": res = TRUSTTUNNEL case "INNER": res = INNER default: return nil, fmt.Errorf("unknown type: %s", t) } return &res, nil } func (t Type) MarshalJSON() ([]byte, error) { return json.Marshal(t.String()) } // Metadata is used to store connection address type Metadata struct { NetWork NetWork `json:"network"` Type Type `json:"type"` SrcIP netip.Addr `json:"sourceIP"` DstIP netip.Addr `json:"destinationIP"` SrcGeoIP []string `json:"sourceGeoIP"` // can be nil if never queried, empty slice if got no result DstGeoIP []string `json:"destinationGeoIP"` // can be nil if never queried, empty slice if got no result SrcIPASN string `json:"sourceIPASN"` DstIPASN string `json:"destinationIPASN"` SrcPort uint16 `json:"sourcePort,string"` // `,string` is used to compatible with old version json output DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output InIP netip.Addr `json:"inboundIP"` InPort uint16 `json:"inboundPort,string"` // `,string` is used to compatible with old version json output InName string `json:"inboundName"` InUser string `json:"inboundUser"` Host string `json:"host"` DNSMode DNSMode `json:"dnsMode"` Uid uint32 `json:"uid"` Process string `json:"process"` ProcessPath string `json:"processPath"` SpecialProxy string `json:"specialProxy"` SpecialRules string `json:"specialRules"` RemoteDst string `json:"remoteDestination"` DSCP uint8 `json:"dscp"` RawSrcAddr net.Addr `json:"-"` RawDstAddr net.Addr `json:"-"` // Only domain rule SniffHost string `json:"sniffHost"` } func (m *Metadata) RemoteAddress() string { return net.JoinHostPort(m.String(), strconv.FormatUint(uint64(m.DstPort), 10)) } func (m *Metadata) SourceAddress() string { return net.JoinHostPort(m.SrcIP.String(), strconv.FormatUint(uint64(m.SrcPort), 10)) } func (m *Metadata) SourceAddrPort() netip.AddrPort { return netip.AddrPortFrom(m.SrcIP.Unmap(), m.SrcPort) } func (m *Metadata) SourceDetail() string { if m.Type == INNER { return fmt.Sprintf("%s", MihomoName) } switch { case m.Process != "" && m.Uid != 0: return fmt.Sprintf("%s(%s, uid=%d)", m.SourceAddress(), m.Process, m.Uid) case m.Uid != 0: return fmt.Sprintf("%s(uid=%d)", m.SourceAddress(), m.Uid) case m.Process != "": return fmt.Sprintf("%s(%s)", m.SourceAddress(), m.Process) default: return fmt.Sprintf("%s", m.SourceAddress()) } } func (m *Metadata) SourceValid() bool { return m.SrcPort != 0 && m.SrcIP.IsValid() } func (m *Metadata) AddrType() AddrType { switch true { case m.Host != "" || !m.DstIP.IsValid(): return AtypDomainName case m.DstIP.Is4(): return AtypIPv4 default: return AtypIPv6 } } func (m *Metadata) Resolved() bool { return m.DstIP.IsValid() } func (m *Metadata) RuleHost() string { if len(m.SniffHost) == 0 { return m.Host } else { return m.SniffHost } } // Pure is used to solve unexpected behavior // when dialing proxy connection in DNSMapping mode. func (m *Metadata) Pure() *Metadata { if (m.DNSMode == DNSMapping || m.DNSMode == DNSHosts) && m.DstIP.IsValid() { copyM := *m copyM.Host = "" return ©M } return m } func (m *Metadata) Clone() *Metadata { copyM := *m return ©M } func (m *Metadata) AddrPort() netip.AddrPort { return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort) } func (m *Metadata) UDPAddr() *net.UDPAddr { if m.NetWork != UDP || !m.DstIP.IsValid() { return nil } return net.UDPAddrFromAddrPort(m.AddrPort()) } func (m *Metadata) String() string { if m.Host != "" { return m.Host } else if m.DstIP.IsValid() { return m.DstIP.String() } else { return "" } } func (m *Metadata) Valid() bool { return m.Host != "" || m.DstIP.IsValid() } func (m *Metadata) SetRemoteAddr(addr net.Addr) error { if addr == nil { return nil } if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok { if rawAddr := rawAddr.RawAddr(); rawAddr != nil { if err := m.SetRemoteAddr(rawAddr); err == nil { return nil } } } if addr, ok := addr.(interface{ AddrPort() netip.AddrPort }); ok { // *net.TCPAddr, *net.UDPAddr, M.Socksaddr if addrPort := addr.AddrPort(); addrPort.Port() != 0 { m.DstPort = addrPort.Port() if addrPort.IsValid() { // sing's M.Socksaddr maybe return an invalid AddrPort if it's a DomainName m.DstIP = addrPort.Addr().Unmap() return nil } else { if addr, ok := addr.(interface{ AddrString() string }); ok { // must be sing's M.Socksaddr m.Host = addr.AddrString() // actually is M.Socksaddr.Fqdn return nil } } } } return m.SetRemoteAddress(addr.String()) } func (m *Metadata) SetRemoteAddress(rawAddress string) error { host, port, err := net.SplitHostPort(rawAddress) if err != nil { return err } var uint16Port uint16 if port, err := strconv.ParseUint(port, 10, 16); err == nil { uint16Port = uint16(port) } if ip, err := netip.ParseAddr(host); err != nil { m.Host = host m.DstIP = netip.Addr{} } else { m.Host = "" m.DstIP = ip.Unmap() } m.DstPort = uint16Port return nil } func (m *Metadata) SwapSrcDst() { m.SrcIP, m.DstIP = m.DstIP, m.SrcIP m.SrcPort, m.DstPort = m.DstPort, m.SrcPort m.SrcIPASN, m.DstIPASN = m.DstIPASN, m.SrcIPASN m.SrcGeoIP, m.DstGeoIP = m.DstGeoIP, m.SrcGeoIP } ================================================ FILE: core/Clash.Meta/constant/path.go ================================================ package constant import ( "fmt" "os" P "path" "path/filepath" "strconv" "strings" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/constant/features" ) const Name = "mihomo" var ( GeositeName = "GeoSite.dat" GeoipName = "GeoIP.dat" ASNName = "ASN.mmdb" ) // Path is used to get the configuration path // // on Unix systems, `$HOME/.config/mihomo`. // on Windows, `%USERPROFILE%/.config/mihomo`. var Path = func() *path { homeDir, err := os.UserHomeDir() if err != nil { homeDir, _ = os.Getwd() } allowUnsafePath, _ := strconv.ParseBool(os.Getenv("SKIP_SAFE_PATH_CHECK")) homeDir = P.Join(homeDir, ".config", Name) if _, err = os.Stat(homeDir); err != nil { if configHome, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok { homeDir = P.Join(configHome, Name) } } var safePaths []string for _, safePath := range filepath.SplitList(os.Getenv("SAFE_PATHS")) { safePath = strings.TrimSpace(safePath) if len(safePath) == 0 { continue } safePaths = append(safePaths, safePath) } return &path{homeDir: homeDir, configFile: "config.yaml", allowUnsafePath: allowUnsafePath, safePaths: safePaths} }() type path struct { homeDir string configFile string allowUnsafePath bool safePaths []string } // SetHomeDir is used to set the configuration path func SetHomeDir(root string) { Path.homeDir = root } // SetConfig is used to set the configuration file func SetConfig(file string) { Path.configFile = file } func (p *path) HomeDir() string { return p.homeDir } func (p *path) Config() string { return p.configFile } // Resolve return a absolute path or a relative path with homedir func (p *path) Resolve(path string) string { if !filepath.IsAbs(path) { return filepath.Join(p.HomeDir(), path) } return path } // IsSafePath return true if path is a subpath of homedir (or in the SAFE_PATHS environment variable) func (p *path) IsSafePath(path string) bool { if p.allowUnsafePath || features.Android { return true } path = p.Resolve(path) for _, safePath := range p.SafePaths() { if rel, err := filepath.Rel(safePath, path); err == nil { if filepath.IsLocal(rel) { return true } } } return false } func (p *path) SafePaths() []string { return append([]string{p.homeDir}, p.safePaths...) // add homedir to safePaths } func (p *path) ErrNotSafePath(path string) error { return ErrNotSafePath{Path: path, SafePaths: p.SafePaths()} } type ErrNotSafePath struct { Path string SafePaths []string } func (e ErrNotSafePath) Error() string { return fmt.Sprintf("path is not subpath of home directory or SAFE_PATHS: %s \n allowed paths: %s", e.Path, e.SafePaths) } func (p *path) GetPathByHash(prefix, name string) string { hash := utils.MakeHash([]byte(name)) filename := hash.String() return filepath.Join(p.HomeDir(), prefix, filename) } func (p *path) MMDB() string { files, err := os.ReadDir(p.homeDir) if err != nil { return "" } for _, fi := range files { if fi.IsDir() { // 目录则直接跳过 continue } else { if strings.EqualFold(fi.Name(), "Country.mmdb") || strings.EqualFold(fi.Name(), "geoip.db") || strings.EqualFold(fi.Name(), "geoip.metadb") { GeoipName = fi.Name() return P.Join(p.homeDir, fi.Name()) } } } return P.Join(p.homeDir, "geoip.metadb") } func (p *path) ASN() string { files, err := os.ReadDir(p.homeDir) if err != nil { return "" } for _, fi := range files { if fi.IsDir() { // 目录则直接跳过 continue } else { if strings.EqualFold(fi.Name(), "ASN.mmdb") { ASNName = fi.Name() return P.Join(p.homeDir, fi.Name()) } } } return P.Join(p.homeDir, ASNName) } func (p *path) OldCache() string { return P.Join(p.homeDir, ".cache") } func (p *path) Cache() string { return P.Join(p.homeDir, "cache.db") } func (p *path) GeoIP() string { files, err := os.ReadDir(p.homeDir) if err != nil { return "" } for _, fi := range files { if fi.IsDir() { // 目录则直接跳过 continue } else { if strings.EqualFold(fi.Name(), "GeoIP.dat") { GeoipName = fi.Name() return P.Join(p.homeDir, fi.Name()) } } } return P.Join(p.homeDir, "GeoIP.dat") } func (p *path) GeoSite() string { files, err := os.ReadDir(p.homeDir) if err != nil { return "" } for _, fi := range files { if fi.IsDir() { // 目录则直接跳过 continue } else { if strings.EqualFold(fi.Name(), "GeoSite.dat") { GeositeName = fi.Name() return P.Join(p.homeDir, fi.Name()) } } } return P.Join(p.homeDir, "GeoSite.dat") } func (p *path) GetAssetLocation(file string) string { return P.Join(p.homeDir, file) } func (p *path) GetExecutableFullPath() string { exePath, err := os.Executable() if err != nil { return "mihomo" } res, _ := filepath.EvalSymlinks(exePath) return res } ================================================ FILE: core/Clash.Meta/constant/path_test.go ================================================ package constant import ( "github.com/stretchr/testify/assert" "testing" ) func TestPath(t *testing.T) { assert.False(t, (&path{}).IsSafePath("/usr/share/metacubexd/")) assert.True(t, (&path{ safePaths: []string{"/usr/share/metacubexd"}, }).IsSafePath("/usr/share/metacubexd/")) assert.False(t, (&path{}).IsSafePath("../metacubexd/")) assert.True(t, (&path{ homeDir: "/usr/share/mihomo", safePaths: []string{"/usr/share/metacubexd"}, }).IsSafePath("../metacubexd/")) assert.False(t, (&path{ homeDir: "/usr/share/mihomo", safePaths: []string{"/usr/share/ycad"}, }).IsSafePath("../metacubexd/")) assert.False(t, (&path{}).IsSafePath("/opt/mykeys/key1.key")) assert.True(t, (&path{ safePaths: []string{"/opt/mykeys"}, }).IsSafePath("/opt/mykeys/key1.key")) assert.True(t, (&path{ safePaths: []string{"/opt/mykeys/"}, }).IsSafePath("/opt/mykeys/key1.key")) assert.True(t, (&path{ safePaths: []string{"/opt/mykeys/key1.key"}, }).IsSafePath("/opt/mykeys/key1.key")) assert.True(t, (&path{}).IsSafePath("key1.key")) assert.True(t, (&path{}).IsSafePath("./key1.key")) assert.True(t, (&path{}).IsSafePath("./mykey/key1.key")) assert.True(t, (&path{}).IsSafePath("./mykey/../key1.key")) assert.False(t, (&path{}).IsSafePath("./mykey/../../key1.key")) } ================================================ FILE: core/Clash.Meta/constant/provider/interface.go ================================================ package provider import ( "context" "fmt" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/constant" ) // Vehicle Type const ( File VehicleType = iota HTTP Compatible Inline ) // VehicleType defined type VehicleType int func (v VehicleType) String() string { switch v { case File: return "File" case HTTP: return "HTTP" case Compatible: return "Compatible" case Inline: return "Inline" default: return "Unknown" } } type Vehicle interface { Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) Write(buf []byte) error Path() string Url() string Proxy() string Type() VehicleType } // Provider Type const ( Proxy ProviderType = iota Rule ) // ProviderType defined type ProviderType int func (pt ProviderType) String() string { switch pt { case Proxy: return "Proxy" case Rule: return "Rule" default: return "Unknown" } } // Provider interface type Provider interface { Name() string VehicleType() VehicleType Type() ProviderType Initial() error Update() error } // ProxyProvider interface type ProxyProvider interface { Provider Proxies() []constant.Proxy Count() int // Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies. // Commonly used in DialContext and DialPacketConn Touch() HealthCheck() Version() uint32 RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) HealthCheckURL() string } // RuleProvider interface type RuleProvider interface { Provider Behavior() RuleBehavior Count() int Match(metadata *constant.Metadata, helper constant.RuleMatchHelper) bool Strategy() any } // Rule Behavior const ( Domain RuleBehavior = iota IPCIDR Classical ) // RuleBehavior defined type RuleBehavior int func (rt RuleBehavior) String() string { switch rt { case Domain: return "Domain" case IPCIDR: return "IPCIDR" case Classical: return "Classical" default: return "Unknown" } } func (rt RuleBehavior) Byte() byte { switch rt { case Domain: return 0 case IPCIDR: return 1 case Classical: return 2 default: return 255 } } func ParseBehavior(s string) (behavior RuleBehavior, err error) { switch s { case "domain": behavior = Domain case "ipcidr": behavior = IPCIDR case "classical": behavior = Classical default: err = fmt.Errorf("unsupported behavior type: %s", s) } return } const ( YamlRule RuleFormat = iota TextRule MrsRule ) type RuleFormat int func (rf RuleFormat) String() string { switch rf { case YamlRule: return "YamlRule" case TextRule: return "TextRule" case MrsRule: return "MrsRule" default: return "Unknown" } } func ParseRuleFormat(s string) (format RuleFormat, err error) { switch s { case "", "yaml": format = YamlRule case "text": format = TextRule case "mrs": format = MrsRule default: err = fmt.Errorf("unsupported format type: %s", s) } return } type Tunnel interface { Providers() map[string]ProxyProvider RuleProviders() map[string]RuleProvider RuleUpdateCallback() *utils.Callback[RuleProvider] } ================================================ FILE: core/Clash.Meta/constant/rule.go ================================================ package constant import "time" // Rule Type const ( Domain RuleType = iota DomainSuffix DomainKeyword DomainRegex DomainWildcard GEOSITE GEOIP SrcGEOIP IPASN SrcIPASN IPCIDR SrcIPCIDR IPSuffix SrcIPSuffix SrcPort DstPort InPort DSCP InUser InName InType ProcessName ProcessPath ProcessNameRegex ProcessPathRegex ProcessNameWildcard ProcessPathWildcard RuleSet Network Uid SubRules MATCH AND OR NOT ) type RuleType int func (rt RuleType) String() string { switch rt { case Domain: return "Domain" case DomainSuffix: return "DomainSuffix" case DomainKeyword: return "DomainKeyword" case DomainRegex: return "DomainRegex" case DomainWildcard: return "DomainWildcard" case GEOSITE: return "GeoSite" case GEOIP: return "GeoIP" case SrcGEOIP: return "SrcGeoIP" case IPASN: return "IPASN" case SrcIPASN: return "SrcIPASN" case IPCIDR: return "IPCIDR" case SrcIPCIDR: return "SrcIPCIDR" case IPSuffix: return "IPSuffix" case SrcIPSuffix: return "SrcIPSuffix" case SrcPort: return "SrcPort" case DstPort: return "DstPort" case InPort: return "InPort" case InUser: return "InUser" case InName: return "InName" case InType: return "InType" case ProcessName: return "ProcessName" case ProcessPath: return "ProcessPath" case ProcessNameRegex: return "ProcessNameRegex" case ProcessPathRegex: return "ProcessPathRegex" case ProcessNameWildcard: return "ProcessNameWildcard" case ProcessPathWildcard: return "ProcessPathWildcard" case MATCH: return "Match" case RuleSet: return "RuleSet" case Network: return "Network" case DSCP: return "DSCP" case Uid: return "Uid" case SubRules: return "SubRules" case AND: return "AND" case OR: return "OR" case NOT: return "NOT" default: return "Unknown" } } type Rule interface { RuleType() RuleType Match(metadata *Metadata, helper RuleMatchHelper) (bool, string) Adapter() string Payload() string ProviderNames() []string } type RuleWrapper interface { Rule // SetDisabled to set enable/disable rule SetDisabled(v bool) // IsDisabled return rule is disabled or not IsDisabled() bool // HitCount for statistics HitCount() uint64 // HitAt for statistics HitAt() time.Time // MissCount for statistics MissCount() uint64 // MissAt for statistics MissAt() time.Time // Unwrap return Rule Unwrap() Rule } type RuleMatchHelper struct { ResolveIP func() FindProcess func() } type RuleGroup interface { Rule GetRecodeSize() int } ================================================ FILE: core/Clash.Meta/constant/sniffer/sniffer.go ================================================ package sniffer import "github.com/metacubex/mihomo/constant" type Sniffer interface { SupportNetwork() constant.NetWork // SniffData must not change input bytes SniffData(bytes []byte) (string, error) Protocol() string SupportPort(port uint16) bool } type ReplaceDomain func(metadata *constant.Metadata, host string) type MultiPacketSniffer interface { WrapperSender(packetSender constant.PacketSender, replaceDomain ReplaceDomain) constant.PacketSender } const ( TLS Type = iota HTTP QUIC ) var ( List = []Type{TLS, HTTP, QUIC} ) type Type int func (rt Type) String() string { switch rt { case TLS: return "TLS" case HTTP: return "HTTP" case QUIC: return "QUIC" default: return "Unknown" } } ================================================ FILE: core/Clash.Meta/constant/tun.go ================================================ package constant import ( "errors" "strings" ) var StackTypeMapping = map[string]TUNStack{ strings.ToLower(TunGvisor.String()): TunGvisor, strings.ToLower(TunSystem.String()): TunSystem, strings.ToLower(TunMixed.String()): TunMixed, } const ( TunGvisor TUNStack = iota TunSystem TunMixed ) type TUNStack int // UnmarshalText unserialize TUNStack func (e *TUNStack) UnmarshalText(data []byte) error { mode, exist := StackTypeMapping[strings.ToLower(string(data))] if !exist { return errors.New("invalid tun stack") } *e = mode return nil } // MarshalText serialize TUNStack with json func (e TUNStack) MarshalText() ([]byte, error) { return []byte(e.String()), nil } func (e TUNStack) String() string { switch e { case TunGvisor: return "gVisor" case TunSystem: return "System" case TunMixed: return "Mixed" default: return "unknown" } } ================================================ FILE: core/Clash.Meta/constant/tunnel.go ================================================ package constant import "net" type Tunnel interface { // HandleTCPConn will handle a tcp connection blocking HandleTCPConn(conn net.Conn, metadata *Metadata) // HandleUDPPacket will handle a udp packet nonblocking HandleUDPPacket(packet UDPPacket, metadata *Metadata) // NatTable return nat table NatTable() NatTable } ================================================ FILE: core/Clash.Meta/constant/version.go ================================================ package constant var ( Meta = true Version = "1.19.24" BuildTime = "unknown time" MihomoName = "mihomo" ) ================================================ FILE: core/Clash.Meta/context/conn.go ================================================ package context import ( "github.com/metacubex/mihomo/common/utils" "net" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/gofrs/uuid/v5" ) type ConnContext struct { id uuid.UUID metadata *C.Metadata conn *N.BufferedConn } func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext { return &ConnContext{ id: utils.NewUUIDV4(), metadata: metadata, conn: N.NewBufferedConn(conn), } } // ID implement C.ConnContext ID func (c *ConnContext) ID() uuid.UUID { return c.id } // Metadata implement C.ConnContext Metadata func (c *ConnContext) Metadata() *C.Metadata { return c.metadata } // Conn implement C.ConnContext Conn func (c *ConnContext) Conn() *N.BufferedConn { return c.conn } ================================================ FILE: core/Clash.Meta/context/dns.go ================================================ package context import ( "context" "github.com/metacubex/mihomo/common/utils" "github.com/gofrs/uuid/v5" ) const ( DNSTypeHost = "host" DNSTypeFakeIP = "fakeip" DNSTypeRaw = "raw" ) type DNSContext struct { context.Context id uuid.UUID tp string } func NewDNSContext(ctx context.Context) *DNSContext { return &DNSContext{ Context: ctx, id: utils.NewUUIDV4(), } } // ID implement C.PlainContext ID func (c *DNSContext) ID() uuid.UUID { return c.id } // SetType set type of response func (c *DNSContext) SetType(tp string) { c.tp = tp } // Type return type of response func (c *DNSContext) Type() string { return c.tp } ================================================ FILE: core/Clash.Meta/context/packetconn.go ================================================ package context import ( "net" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/gofrs/uuid/v5" ) type PacketConnContext struct { id uuid.UUID metadata *C.Metadata packetConn net.PacketConn } func NewPacketConnContext(metadata *C.Metadata) *PacketConnContext { return &PacketConnContext{ id: utils.NewUUIDV4(), metadata: metadata, } } // ID implement C.PacketConnContext ID func (pc *PacketConnContext) ID() uuid.UUID { return pc.id } // Metadata implement C.PacketConnContext Metadata func (pc *PacketConnContext) Metadata() *C.Metadata { return pc.metadata } // PacketConn implement C.PacketConnContext PacketConn func (pc *PacketConnContext) PacketConn() net.PacketConn { return pc.packetConn } // InjectPacketConn injectPacketConn manually func (pc *PacketConnContext) InjectPacketConn(pconn C.PacketConn) { pc.packetConn = pconn } ================================================ FILE: core/Clash.Meta/dns/client.go ================================================ package dns import ( "context" "fmt" "net" "strings" "time" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" ) type client struct { port string host string dialer *dnsDialer schema string } var _ dnsClient = (*client)(nil) // Address implements dnsClient func (c *client) Address() string { return fmt.Sprintf("%s://%s", c.schema, net.JoinHostPort(c.host, c.port)) } func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { network := "udp" if c.schema != "udp" { network = "tcp" } addr := net.JoinHostPort(c.host, c.port) conn, err := c.dialer.DialContext(ctx, network, addr) if err != nil { return nil, err } defer conn.Close() // miekg/dns ExchangeContext doesn't respond to context cancel. // this is a workaround type result struct { msg *D.Msg err error } ch := make(chan result, 1) go func() { dClient := &D.Client{ UDPSize: 4096, Timeout: 5 * time.Second, } dConn := &D.Conn{ Conn: conn, UDPSize: dClient.UDPSize, } msg, _, err := dClient.ExchangeWithConn(m, dConn) // Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)! if msg != nil && msg.Truncated && network == "udp" { network = "tcp" log.Debugln("[DNS] Truncated reply from %s:%s for %s over UDP, retrying over TCP", c.host, c.port, m.Question[0].String()) var tcpConn net.Conn tcpConn, err = c.dialer.DialContext(ctx, network, addr) if err != nil { ch <- result{msg, err} return } defer tcpConn.Close() dConn.Conn = tcpConn msg, _, err = dClient.ExchangeWithConn(m, dConn) } ch <- result{msg, err} }() select { case <-ctx.Done(): return nil, ctx.Err() case ret := <-ch: return ret.msg, ret.err } } func (c *client) ResetConnection() {} func newClient(addr string, resolver *Resolver, netType string, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *client { host, port, _ := net.SplitHostPort(addr) c := &client{ port: port, host: host, dialer: newDNSDialer(resolver, proxyAdapter, proxyName), schema: "udp", } if strings.HasPrefix(netType, "tcp") { c.schema = "tcp" } return c } ================================================ FILE: core/Clash.Meta/dns/dhcp.go ================================================ package dns import ( "context" "net" "net/netip" "strings" "sync" "time" "github.com/metacubex/mihomo/component/dhcp" "github.com/metacubex/mihomo/component/iface" D "github.com/miekg/dns" ) const ( IfaceTTL = time.Second * 20 DHCPTTL = time.Hour DHCPTimeout = time.Minute ) type dhcpClient struct { ifaceName string lock sync.Mutex ifaceInvalidate time.Time dnsInvalidate time.Time ifaceAddr netip.Prefix done chan struct{} clients []dnsClient err error } var _ dnsClient = (*dhcpClient)(nil) // Address implements dnsClient func (d *dhcpClient) Address() string { addrs := make([]string, 0) for _, c := range d.clients { addrs = append(addrs, c.Address()) } return strings.Join(addrs, ",") } func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { clients, err := d.resolve(ctx) if err != nil { return nil, err } msg, _, err = batchExchange(ctx, clients, m) return } func (d *dhcpClient) ResetConnection() { for _, client := range d.clients { client.ResetConnection() } } func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) { d.lock.Lock() invalidated, err := d.invalidate() if err != nil { d.err = err } else if invalidated { done := make(chan struct{}) d.done = done go func() { ctx, cancel := context.WithTimeout(context.Background(), DHCPTimeout) defer cancel() var res []dnsClient dns, err := dhcp.ResolveDNSFromDHCP(ctx, d.ifaceName) // dns never empty if err is nil if err == nil { nameserver := make([]NameServer, 0, len(dns)) for _, item := range dns { nameserver = append(nameserver, NameServer{ Addr: net.JoinHostPort(item.String(), "53"), ProxyName: d.ifaceName, }) } res = transform(nameserver, nil) } d.lock.Lock() defer d.lock.Unlock() close(done) d.done = nil d.clients = res d.err = err }() } d.lock.Unlock() for { d.lock.Lock() res, err, done := d.clients, d.err, d.done d.lock.Unlock() // initializing if res == nil && err == nil { select { case <-done: continue case <-ctx.Done(): return nil, ctx.Err() } } // dirty return return res, err } } func (d *dhcpClient) invalidate() (bool, error) { if time.Now().Before(d.ifaceInvalidate) { return false, nil } d.ifaceInvalidate = time.Now().Add(IfaceTTL) ifaceObj, err := iface.ResolveInterface(d.ifaceName) if err != nil { return false, err } addr, err := ifaceObj.PickIPv4Addr(netip.Addr{}) if err != nil { return false, err } if time.Now().Before(d.dnsInvalidate) && d.ifaceAddr == addr { return false, nil } d.dnsInvalidate = time.Now().Add(DHCPTTL) d.ifaceAddr = addr return d.done == nil, nil } func newDHCPClient(ifaceName string) *dhcpClient { return &dhcpClient{ifaceName: ifaceName} } ================================================ FILE: core/Clash.Meta/dns/dialer.go ================================================ package dns // export functions from tunnel module import "github.com/metacubex/mihomo/tunnel" const RespectRules = tunnel.DnsRespectRules type dnsDialer = tunnel.DNSDialer var newDNSDialer = tunnel.NewDNSDialer ================================================ FILE: core/Clash.Meta/dns/doh.go ================================================ package dns import ( "context" "encoding/base64" "errors" "fmt" "io" "net" "net/url" "runtime" "strconv" "sync" "time" "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/http" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" "github.com/metacubex/tls" D "github.com/miekg/dns" "golang.org/x/exp/slices" ) // Values to configure HTTP and HTTP/2 transport. const ( // transportDefaultSendPingTimeout is the default timeout for pinging // idle connections in HTTP/2 transport. transportDefaultSendPingTimeout = 30 * time.Second // transportDefaultIdleConnTimeout is the default timeout for idle // connections in HTTP transport. transportDefaultIdleConnTimeout = 5 * time.Minute // dohMaxConnsPerHost controls the maximum number of connections for // each host. Note, that setting it to 1 may cause issues with Go's http // implementation, see https://github.com/AdguardTeam/dnsproxy/issues/278. dohMaxConnsPerHost = 2 dialTimeout = 10 * time.Second // dohMaxIdleConns controls the maximum number of connections being idle // at the same time. dohMaxIdleConns = 2 maxElapsedTime = time.Second * 30 ) var DefaultHTTPVersions = []C.HTTPVersion{C.HTTPVersion11, C.HTTPVersion2} // dnsOverHTTPS is a struct that implements the Upstream interface for the // DNS-over-HTTPS protocol. type dnsOverHTTPS struct { // The Client's Transport typically has internal state (cached TCP // connections), so Clients should be reused instead of created as // needed. Clients are safe for concurrent use by multiple goroutines. client *http.Client clientMu sync.Mutex // quicConfig is the QUIC configuration that is used if HTTP/3 is enabled // for this upstream. quicConfig *quic.Config quicConfigGuard sync.Mutex url *url.URL httpVersions []C.HTTPVersion dialer *dnsDialer addr string skipCertVerify bool } // type check var _ dnsClient = (*dnsOverHTTPS)(nil) // newDoH returns the DNS-over-HTTPS Upstream. func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) dnsClient { u, _ := url.Parse(urlString) httpVersions := DefaultHTTPVersions if preferH3 { httpVersions = append(httpVersions, C.HTTPVersion3) } if params["h3"] == "true" { httpVersions = []C.HTTPVersion{C.HTTPVersion3} } doh := &dnsOverHTTPS{ url: u, addr: u.String(), dialer: newDNSDialer(r, proxyAdapter, proxyName), quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, TokenStore: newQUICTokenStore(), }, httpVersions: httpVersions, } if params["skip-cert-verify"] == "true" { doh.skipCertVerify = true } runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) return doh } // Address implements the Upstream interface for *dnsOverHTTPS. func (doh *dnsOverHTTPS) Address() string { return doh.addr } func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { // Quote from https://www.rfc-editor.org/rfc/rfc8484.html: // In order to maximize HTTP cache friendliness, DoH clients using media // formats that include the ID field from the DNS message header, such // as "application/dns-message", SHOULD use a DNS ID of 0 in every DNS // request. m = m.Copy() id := m.Id m.Id = 0 defer func() { // Restore the original ID to not break compatibility with proxies. m.Id = id if msg != nil { msg.Id = id } }() // Check if there was already an active client before sending the request. // We'll only attempt to re-connect if there was one. client, isCached, err := doh.getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to init http client: %w", err) } // Make the first attempt to send the DNS query. msg, err = doh.exchangeHTTPS(ctx, client, m) // Make up to 2 attempts to re-create the HTTP client and send the request // again. There are several cases (mostly, with QUIC) where this workaround // is necessary to make HTTP client usable. We need to make 2 attempts in // the case when the connection was closed (due to inactivity for example) // AND the server refuses to open a 0-RTT connection. for i := 0; isCached && doh.shouldRetry(err) && i < 2; i++ { client, err = doh.resetClient(ctx, err) if err != nil { return nil, fmt.Errorf("failed to reset http client: %w", err) } msg, err = doh.exchangeHTTPS(ctx, client, m) } if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { // If the request failed anyway, make sure we don't use this client. _, resErr := doh.resetClient(ctx, err) return nil, fmt.Errorf("%w (resErr:%v)", err, resErr) } return msg, err } // Close implements the Upstream interface for *dnsOverHTTPS. func (doh *dnsOverHTTPS) Close() (err error) { doh.clientMu.Lock() defer doh.clientMu.Unlock() runtime.SetFinalizer(doh, nil) if doh.client == nil { return nil } return doh.closeClient(doh.client) } func (doh *dnsOverHTTPS) ResetConnection() { doh.clientMu.Lock() defer doh.clientMu.Unlock() if doh.client == nil { return } _ = doh.closeClient(doh.client) doh.client = nil } // closeClient cleans up resources used by client if necessary. func (doh *dnsOverHTTPS) closeClient(client *http.Client) (err error) { client.CloseIdleConnections() if isHTTP3(client) { // HTTP/3 may leak due to keep-alive connections. return client.Transport.(io.Closer).Close() } return nil } // exchangeHTTPS sends the DNS query to a DoH resolver using the specified // http.Client instance. func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) { buf, err := req.Pack() if err != nil { return nil, fmt.Errorf("packing message: %w", err) } // It appears, that GET requests are more memory-efficient with Golang // implementation of HTTP/2. method := http.MethodGet if isHTTP3(client) { // If we're using HTTP/3, use http3.MethodGet0RTT to force using 0-RTT. method = http3.MethodGet0RTT } requestUrl := *doh.url // don't modify origin url requestUrl.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) httpReq, err := http.NewRequestWithContext(ctx, method, requestUrl.String(), nil) if err != nil { return nil, fmt.Errorf("creating http request to %s: %w", doh.url, err) } httpReq.Header.Set("Accept", "application/dns-message") httpReq.Header.Set("User-Agent", "") httpResp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("requesting %s: %w", doh.url, err) } defer httpResp.Body.Close() body, err := io.ReadAll(httpResp.Body) if err != nil { return nil, fmt.Errorf("reading %s: %w", doh.url, err) } if httpResp.StatusCode != http.StatusOK { return nil, fmt.Errorf( "expected status %d, got %d from %s", http.StatusOK, httpResp.StatusCode, doh.url, ) } resp = &D.Msg{} err = resp.Unpack(body) if err != nil { return nil, fmt.Errorf( "unpacking response from %s: body is %s: %w", doh.url, body, err, ) } if resp.Id != req.Id { err = D.ErrId } return resp, err } // shouldRetry checks what error we have received and returns true if we should // re-create the HTTP client and retry the request. func (doh *dnsOverHTTPS) shouldRetry(err error) (ok bool) { if err == nil { return false } var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { // If this is a timeout error, trying to forcibly re-create the HTTP // client instance. This is an attempt to fix an issue with DoH client // stalling after a network change. // // See https://github.com/AdguardTeam/AdGuardHome/issues/3217. return true } if isQUICRetryError(err) { return true } return false } // resetClient triggers re-creation of the *http.Client that is used by this // upstream. This method accepts the error that caused resetting client as // depending on the error we may also reset the QUIC config. func (doh *dnsOverHTTPS) resetClient(ctx context.Context, resetErr error) (client *http.Client, err error) { doh.clientMu.Lock() defer doh.clientMu.Unlock() if errors.Is(resetErr, quic.Err0RTTRejected) { // Reset the TokenStore only if 0-RTT was rejected. doh.resetQUICConfig() } oldClient := doh.client if oldClient != nil { closeErr := doh.closeClient(oldClient) if closeErr != nil { log.Warnln("warning: failed to close the old http client: %v", closeErr) } } log.Debugln("re-creating the http client due to %v", resetErr) doh.client, err = doh.createClient(ctx) return doh.client, err } // getQUICConfig returns the QUIC config in a thread-safe manner. Note, that // this method returns a pointer, it is forbidden to change its properties. func (doh *dnsOverHTTPS) getQUICConfig() (c *quic.Config) { doh.quicConfigGuard.Lock() defer doh.quicConfigGuard.Unlock() return doh.quicConfig } // resetQUICConfig Re-create the token store to make sure we're not trying to // use invalid for 0-RTT. func (doh *dnsOverHTTPS) resetQUICConfig() { doh.quicConfigGuard.Lock() defer doh.quicConfigGuard.Unlock() doh.quicConfig = doh.quicConfig.Clone() doh.quicConfig.TokenStore = newQUICTokenStore() } // getClient gets or lazily initializes an HTTP client (and transport) that will // be used for this DoH resolver. func (doh *dnsOverHTTPS) getClient(ctx context.Context) (c *http.Client, isCached bool, err error) { startTime := time.Now() doh.clientMu.Lock() defer doh.clientMu.Unlock() if doh.client != nil { return doh.client, true, nil } // Timeout can be exceeded while waiting for the lock. This happens quite // often on mobile devices. elapsed := time.Since(startTime) if elapsed > maxElapsedTime { return nil, false, fmt.Errorf("timeout exceeded: %s", elapsed) } log.Debugln("creating a new http client") doh.client, err = doh.createClient(ctx) return doh.client, false, err } // createClient creates a new *http.Client instance. The HTTP protocol version // will depend on whether HTTP3 is allowed and provided by this upstream. Note, // that we'll attempt to establish a QUIC connection when creating the client in // order to check whether HTTP3 is supported. func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error) { transport, err := doh.createTransport(ctx) if err != nil { return nil, fmt.Errorf("[%s] initializing http transport: %w", doh.url.String(), err) } client := &http.Client{ Transport: transport, Timeout: DefaultTimeout, Jar: nil, } doh.client = client return doh.client, nil } // createTransport initializes an HTTP transport that will be used specifically // for this DoH resolver. This HTTP transport ensures that the HTTP requests // will be sent exactly to the IP address got from the bootstrap resolver. Note, // that this function will first attempt to establish a QUIC connection (if // HTTP3 is enabled in the upstream options). If this attempt is successful, // it returns an HTTP3 transport, otherwise it returns the H1/H2 transport. func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) { transport := &http.Transport{ DisableCompression: true, DialContext: doh.dialer.DialContext, IdleConnTimeout: transportDefaultIdleConnTimeout, MaxConnsPerHost: dohMaxConnsPerHost, MaxIdleConns: dohMaxIdleConns, } if doh.url.Scheme == "http" { return transport, nil } tlsConfig, err := ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ InsecureSkipVerify: doh.skipCertVerify, MinVersion: tls.VersionTLS12, SessionTicketsDisabled: false, }, }) if err != nil { return nil, err } var nextProtos []string for _, v := range doh.httpVersions { nextProtos = append(nextProtos, string(v)) } tlsConfig.NextProtos = nextProtos transport.TLSClientConfig = tlsConfig if slices.Contains(doh.httpVersions, C.HTTPVersion3) { // First, we attempt to create an HTTP3 transport. If the probe QUIC // connection is established successfully, we'll be using HTTP3 for this // upstream. transportH3, err := doh.createTransportH3(ctx, tlsConfig) if err == nil { log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String()) return transportH3, nil } } log.Debugln("[%s] using HTTP/2 for this upstream: %v", doh.url.String(), err) if !doh.supportsHTTP() { return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream") } // Since we have a custom DialContext, we need to use this field to // make golang http.Client attempt to use HTTP/2. Otherwise, it would // only be used when negotiated on the TLS level. transport.ForceAttemptHTTP2 = true // Enable HTTP/2 pings on idle connections. transport.HTTP2 = &http.HTTP2Config{ SendPingTimeout: transportDefaultSendPingTimeout, } return transport, nil } // http3Transport is a wrapper over *http3.Transport that tries to optimize // its behavior. The main thing that it does is trying to force use a single // connection to a host instead of creating a new one all the time. It also // helps mitigate race issues with quic-go. type http3Transport struct { baseTransport *http3.Transport closed bool mu sync.RWMutex } // type check var _ http.RoundTripper = (*http3Transport)(nil) // RoundTrip implements the http.RoundTripper interface for *http3Transport. func (h *http3Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { h.mu.RLock() defer h.mu.RUnlock() if h.closed { return nil, net.ErrClosed } // Try to use cached connection to the target host if it's available. resp, err = h.baseTransport.RoundTripOpt(req, http3.RoundTripOpt{OnlyCachedConn: true}) if errors.Is(err, http3.ErrNoCachedConn) { // If there are no cached connection, trigger creating a new one. resp, err = h.baseTransport.RoundTrip(req) } return resp, err } // type check var _ io.Closer = (*http3Transport)(nil) // Close implements the io.Closer interface for *http3Transport. func (h *http3Transport) Close() (err error) { h.mu.Lock() defer h.mu.Unlock() h.closed = true return h.baseTransport.Close() } func (h *http3Transport) CloseIdleConnections() { h.mu.RLock() defer h.mu.RUnlock() h.baseTransport.CloseIdleConnections() } // createTransportH3 tries to create an HTTP/3 transport for this upstream. // We should be able to fall back to H1/H2 in case if HTTP/3 is unavailable or // if it is too slow. In order to do that, this method will run two probes // in parallel (one for TLS, the other one for QUIC) and if QUIC is faster it // will create the *http3.Transport instance. func (doh *dnsOverHTTPS) createTransportH3( ctx context.Context, tlsConfig *tls.Config, ) (roundTripper http.RoundTripper, err error) { if !doh.supportsH3() { return nil, errors.New("HTTP3 support is not enabled") } addr, err := doh.probeH3(ctx, tlsConfig) if err != nil { return nil, err } rt := &http3.Transport{ Dial: func( ctx context.Context, // Ignore the address and always connect to the one that we got // from the bootstrapper. _ string, tlsCfg *tls.Config, cfg *quic.Config, ) (c *quic.Conn, err error) { return doh.dialQuic(ctx, addr, tlsCfg, cfg) }, DisableCompression: true, TLSClientConfig: tlsConfig, QUICConfig: doh.getQUICConfig(), } return &http3Transport{baseTransport: rt}, nil } func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { ip, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } portInt, err := strconv.Atoi(port) if err != nil { return nil, err } udpAddr := net.UDPAddr{ IP: net.ParseIP(ip), Port: portInt, } packetConn, err := doh.dialer.ListenPacket(ctx, "udp", addr) if err != nil { return nil, err } transport := quic.Transport{Conn: packetConn} transport.SetCreatedConn(true) // auto close conn transport.SetSingleUse(true) // auto close transport tlsCfg = tlsCfg.Clone() if host, _, err := net.SplitHostPort(doh.url.Host); err == nil { tlsCfg.ServerName = host } else { // It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port. tlsCfg.ServerName = doh.url.Host } quicConn, err := transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg) if err != nil { _ = packetConn.Close() return nil, err } return quicConn, nil } // probeH3 runs a test to check whether QUIC is faster than TLS for this // upstream. If the test is successful it will return the address that we // should use to establish the QUIC connections. func (doh *dnsOverHTTPS) probeH3( ctx context.Context, tlsConfig *tls.Config, ) (addr string, err error) { // We're using bootstrapped address instead of what's passed to the function // it does not create an actual connection, but it helps us determine // what IP is actually reachable (when there are v4/v6 addresses). rawConn, err := doh.dialer.DialContext(ctx, "udp", doh.url.Host) if err != nil { return "", fmt.Errorf("failed to dial: %w", err) } addr = rawConn.RemoteAddr().String() // It's never actually used. _ = rawConn.Close() // Avoid spending time on probing if this upstream only supports HTTP/3. if doh.supportsH3() && !doh.supportsHTTP() { return addr, nil } // Use a new *tls.Config with empty session cache for probe connections. // Surprisingly, this is really important since otherwise it invalidates // the existing cache. // TODO(ameshkov): figure out why the sessions cache invalidates here. probeTLSCfg := tlsConfig.Clone() probeTLSCfg.ClientSessionCache = nil // Do not expose probe connections to the callbacks that are passed to // the bootstrap options to avoid side-effects. // TODO(ameshkov): consider exposing, somehow mark that this is a probe. probeTLSCfg.VerifyPeerCertificate = nil probeTLSCfg.VerifyConnection = nil // Run probeQUIC and probeTLS in parallel and see which one is faster. chQuic := make(chan error, 1) chTLS := make(chan error, 1) go doh.probeQUIC(ctx, addr, probeTLSCfg, chQuic) go doh.probeTLS(ctx, probeTLSCfg, chTLS) select { case quicErr := <-chQuic: if quicErr != nil { // QUIC failed, return error since HTTP3 was not preferred. return "", quicErr } // Return immediately, QUIC was faster. return addr, quicErr case tlsErr := <-chTLS: if tlsErr != nil { // Return immediately, TLS failed. log.Debugln("probing TLS: %v", tlsErr) return addr, nil } return "", errors.New("TLS was faster than QUIC, prefer it") } } // probeQUIC attempts to establish a QUIC connection to the specified address. // We run probeQUIC and probeTLS in parallel and see which one is faster. func (doh *dnsOverHTTPS) probeQUIC(ctx context.Context, addr string, tlsConfig *tls.Config, ch chan error) { startTime := time.Now() conn, err := doh.dialQuic(ctx, addr, tlsConfig, doh.getQUICConfig()) if err != nil { ch <- fmt.Errorf("opening QUIC connection to %s: %w", doh.Address(), err) return } // Ignore the error since there's no way we can use it for anything useful. _ = conn.CloseWithError(QUICCodeNoError, "") ch <- nil elapsed := time.Now().Sub(startTime) log.Debugln("elapsed on establishing a QUIC connection: %s", elapsed) } // probeTLS attempts to establish a TLS connection to the specified address. We // run probeQUIC and probeTLS in parallel and see which one is faster. func (doh *dnsOverHTTPS) probeTLS(ctx context.Context, tlsConfig *tls.Config, ch chan error) { startTime := time.Now() conn, err := doh.tlsDial(ctx, "tcp", tlsConfig) if err != nil { ch <- fmt.Errorf("opening TLS connection: %w", err) return } // Ignore the error since there's no way we can use it for anything useful. _ = conn.Close() ch <- nil elapsed := time.Now().Sub(startTime) log.Debugln("elapsed on establishing a TLS connection: %s", elapsed) } // supportsH3 returns true if HTTP/3 is supported by this upstream. func (doh *dnsOverHTTPS) supportsH3() (ok bool) { for _, v := range doh.supportedHTTPVersions() { if v == C.HTTPVersion3 { return true } } return false } // supportsHTTP returns true if HTTP/1.1 or HTTP2 is supported by this upstream. func (doh *dnsOverHTTPS) supportsHTTP() (ok bool) { for _, v := range doh.supportedHTTPVersions() { if v == C.HTTPVersion11 || v == C.HTTPVersion2 { return true } } return false } // supportedHTTPVersions returns the list of supported HTTP versions. func (doh *dnsOverHTTPS) supportedHTTPVersions() (v []C.HTTPVersion) { v = doh.httpVersions if v == nil { v = DefaultHTTPVersions } return v } // isHTTP3 checks if the *http.Client is an HTTP/3 client. func isHTTP3(client *http.Client) (ok bool) { _, ok = client.Transport.(*http3Transport) return ok } // tlsDial is basically the same as tls.DialWithDialer, but we will call our own // dialContext function to get connection. func (doh *dnsOverHTTPS) tlsDial(ctx context.Context, network string, config *tls.Config) (*tls.Conn, error) { // We're using bootstrapped address instead of what's passed // to the function. rawConn, err := doh.dialer.DialContext(ctx, network, doh.url.Host) if err != nil { return nil, err } // We want the timeout to cover the whole process: TCP connection and // TLS handshake dialTimeout will be used as connection deadLine. conn := tls.Client(rawConn, config) ctx, cancel := context.WithTimeout(ctx, dialTimeout) defer cancel() err = conn.HandshakeContext(ctx) if err != nil { _ = rawConn.Close() return nil, err } return conn, nil } ================================================ FILE: core/Clash.Meta/dns/doq.go ================================================ package dns import ( "context" "encoding/binary" "errors" "fmt" "net" "runtime" "strconv" "sync" "time" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/quic-go" "github.com/metacubex/tls" D "github.com/miekg/dns" ) const NextProtoDQ = "doq" const ( // QUICCodeNoError is used when the connection or stream needs to be closed, // but there is no error to signal. QUICCodeNoError = quic.ApplicationErrorCode(0) // QUICCodeInternalError signals that the DoQ implementation encountered // an internal error and is incapable of pursuing the transaction or the // connection. QUICCodeInternalError = quic.ApplicationErrorCode(1) // QUICKeepAlivePeriod is the value that we pass to *quic.Config and that // controls the period with with keep-alive frames are being sent to the // connection. We set it to 20s as it would be in the quic-go@v0.27.1 with // KeepAlive field set to true This value is specified in // https://pkg.go.dev/github.com/metacubex/quic-go/internal/protocol#MaxKeepAliveInterval. // // TODO(ameshkov): Consider making it configurable. QUICKeepAlivePeriod = time.Second * 20 DefaultTimeout = time.Second * 5 ) // dnsOverQUIC is a struct that implements the Upstream interface for the // DNS-over-QUIC protocol (spec: https://www.rfc-editor.org/rfc/rfc9250.html). type dnsOverQUIC struct { // quicConfig is the QUIC configuration that is used for establishing // connections to the upstream. This configuration includes the TokenStore // that needs to be stored for the lifetime of dnsOverQUIC since we can // re-create the connection. quicConfig *quic.Config quicConfigGuard sync.Mutex // conn is the current active QUIC connection. It can be closed and // re-opened when needed. conn *quic.Conn connMu sync.RWMutex addr string dialer *dnsDialer skipCertVerify bool } // type check var _ dnsClient = (*dnsOverQUIC)(nil) // newDoQ returns the DNS-over-QUIC Upstream. func newDoQ(addr string, resolver *Resolver, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *dnsOverQUIC { doq := &dnsOverQUIC{ addr: addr, dialer: newDNSDialer(resolver, proxyAdapter, proxyName), quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, TokenStore: newQUICTokenStore(), }, } if params["skip-cert-verify"] == "true" { doq.skipCertVerify = true } runtime.SetFinalizer(doq, (*dnsOverQUIC).Close) return doq } // Address implements the Upstream interface for *dnsOverQUIC. func (doq *dnsOverQUIC) Address() string { return doq.addr } func (doq *dnsOverQUIC) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { // When sending queries over a QUIC connection, the DNS Message ID MUST be // set to zero. m = m.Copy() id := m.Id m.Id = 0 defer func() { // Restore the original ID to not break compatibility with proxies. m.Id = id if msg != nil { msg.Id = id } }() // Check if there was already an active conn before sending the request. // We'll only attempt to re-connect if there was one. hasConnection := doq.hasConnection() // Make the first attempt to send the DNS query. msg, err = doq.exchangeQUIC(ctx, m) // Make up to 2 attempts to re-open the QUIC connection and send the request // again. There are several cases where this workaround is necessary to // make DoQ usable. We need to make 2 attempts in the case when the // connection was closed (due to inactivity for example) AND the server // refuses to open a 0-RTT connection. for i := 0; hasConnection && doq.shouldRetry(err) && i < 2; i++ { log.Debugln("re-creating the QUIC connection and retrying due to %v", err) // Close the active connection to make sure we'll try to re-connect. doq.closeConnWithError(err) // Retry sending the request. msg, err = doq.exchangeQUIC(ctx, m) } if err != nil { // If we're unable to exchange messages, make sure the connection is // closed and signal about an internal error. doq.closeConnWithError(err) } return msg, err } // Close implements the Upstream interface for *dnsOverQUIC. func (doq *dnsOverQUIC) Close() (err error) { doq.connMu.Lock() defer doq.connMu.Unlock() runtime.SetFinalizer(doq, nil) if doq.conn != nil { err = doq.conn.CloseWithError(QUICCodeNoError, "") } return err } func (doq *dnsOverQUIC) ResetConnection() { doq.closeConnWithError(nil) } // exchangeQUIC attempts to open a QUIC connection, send the DNS message // through it and return the response it got from the server. func (doq *dnsOverQUIC) exchangeQUIC(ctx context.Context, msg *D.Msg) (resp *D.Msg, err error) { var conn *quic.Conn conn, err = doq.getConnection(ctx, true) if err != nil { return nil, err } var buf []byte buf, err = msg.Pack() if err != nil { return nil, fmt.Errorf("failed to pack DNS message for DoQ: %w", err) } var stream *quic.Stream stream, err = doq.openStream(ctx, conn) if err != nil { return nil, err } _, err = stream.Write(AddPrefix(buf)) if err != nil { return nil, fmt.Errorf("failed to write to a QUIC stream: %w", err) } // The client MUST send the DNS query over the selected stream, and MUST // indicate through the STREAM FIN mechanism that no further data will // be sent on that stream. Note, that stream.Close() closes the // write-direction of the stream, but does not prevent reading from it. _ = stream.Close() return doq.readMsg(stream) } // AddPrefix adds a 2-byte prefix with the DNS message length. func AddPrefix(b []byte) (m []byte) { m = make([]byte, 2+len(b)) binary.BigEndian.PutUint16(m, uint16(len(b))) copy(m[2:], b) return m } // shouldRetry checks what error we received and decides whether it is required // to re-open the connection and retry sending the request. func (doq *dnsOverQUIC) shouldRetry(err error) (ok bool) { return isQUICRetryError(err) } // getConnection opens or returns an existing *quic.Conn. useCached // argument controls whether we should try to use the existing cached // connection. If it is false, we will forcibly create a new connection and // close the existing one if needed. func (doq *dnsOverQUIC) getConnection(ctx context.Context, useCached bool) (*quic.Conn, error) { var conn *quic.Conn doq.connMu.RLock() conn = doq.conn if conn != nil && useCached { doq.connMu.RUnlock() return conn, nil } if conn != nil { // we're recreating the connection, let's create a new one. _ = conn.CloseWithError(QUICCodeNoError, "") } doq.connMu.RUnlock() doq.connMu.Lock() defer doq.connMu.Unlock() var err error conn, err = doq.openConnection(ctx) if err != nil { return nil, err } doq.conn = conn return conn, nil } // hasConnection returns true if there's an active QUIC connection. func (doq *dnsOverQUIC) hasConnection() (ok bool) { doq.connMu.Lock() defer doq.connMu.Unlock() return doq.conn != nil } // getQUICConfig returns the QUIC config in a thread-safe manner. Note, that // this method returns a pointer, it is forbidden to change its properties. func (doq *dnsOverQUIC) getQUICConfig() (c *quic.Config) { doq.quicConfigGuard.Lock() defer doq.quicConfigGuard.Unlock() return doq.quicConfig } // resetQUICConfig re-creates the tokens store as we may need to use a new one // if we failed to connect. func (doq *dnsOverQUIC) resetQUICConfig() { doq.quicConfigGuard.Lock() defer doq.quicConfigGuard.Unlock() doq.quicConfig = doq.quicConfig.Clone() doq.quicConfig.TokenStore = newQUICTokenStore() } // openStream opens a new QUIC stream for the specified connection. func (doq *dnsOverQUIC) openStream(ctx context.Context, conn *quic.Conn) (*quic.Stream, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() stream, err := conn.OpenStreamSync(ctx) if err == nil { return stream, nil } // We can get here if the old QUIC connection is not valid anymore. We // should try to re-create the connection again in this case. newConn, err := doq.getConnection(ctx, false) if err != nil { return nil, err } // Open a new stream. return newConn.OpenStreamSync(ctx) } // openConnection opens a new QUIC connection. func (doq *dnsOverQUIC) openConnection(ctx context.Context) (quicConn *quic.Conn, err error) { // we're using bootstrapped address instead of what's passed to the function // it does not create an actual connection, but it helps us determine // what IP is actually reachable (when there're v4/v6 addresses). rawConn, err := doq.dialer.DialContext(ctx, "udp", doq.addr) if err != nil { return nil, fmt.Errorf("failed to open a QUIC connection: %w", err) } addr := rawConn.RemoteAddr().String() // It's never actually used _ = rawConn.Close() ip, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } p, err := strconv.Atoi(port) udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: p} packetConn, err := doq.dialer.ListenPacket(ctx, "udp", addr) if err != nil { return nil, err } host, _, err := net.SplitHostPort(doq.addr) if err != nil { return nil, err } tlsConfig, err := ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: host, InsecureSkipVerify: doq.skipCertVerify, NextProtos: []string{ NextProtoDQ, }, SessionTicketsDisabled: false, }, }) if err != nil { return nil, err } transport := quic.Transport{Conn: packetConn} transport.SetCreatedConn(true) // auto close conn transport.SetSingleUse(true) // auto close transport quicConn, err = transport.Dial(ctx, &udpAddr, tlsConfig, doq.getQUICConfig()) if err != nil { _ = packetConn.Close() return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err) } return quicConn, nil } // closeConnWithError closes the active connection with error to make sure that // new queries were processed in another connection. We can do that in the case // of a fatal error. func (doq *dnsOverQUIC) closeConnWithError(err error) { doq.connMu.Lock() defer doq.connMu.Unlock() if doq.conn == nil { // Do nothing, there's no active conn anyways. return } code := QUICCodeNoError if err != nil { code = QUICCodeInternalError } if errors.Is(err, quic.Err0RTTRejected) { // Reset the TokenStore only if 0-RTT was rejected. doq.resetQUICConfig() } err = doq.conn.CloseWithError(code, "") if err != nil { log.Errorln("failed to close the conn: %v", err) } doq.conn = nil } // readMsg reads the incoming DNS message from the QUIC stream. func (doq *dnsOverQUIC) readMsg(stream *quic.Stream) (m *D.Msg, err error) { respBuf := pool.Get(MaxMsgSize) defer pool.Put(respBuf) n, err := stream.Read(respBuf) if err != nil && n == 0 { return nil, fmt.Errorf("reading response from %s: %w", doq.Address(), err) } // All DNS messages (queries and responses) sent over DoQ connections MUST // be encoded as a 2-octet length field followed by the message content as // specified in [RFC1035]. // IMPORTANT: Note, that we ignore this prefix here as this implementation // does not support receiving multiple messages over a single connection. m = new(D.Msg) err = m.Unpack(respBuf[2:]) if err != nil { return nil, fmt.Errorf("unpacking response from %s: %w", doq.Address(), err) } return m, nil } // newQUICTokenStore creates a new quic.TokenStore that is necessary to have // in order to benefit from 0-RTT. func newQUICTokenStore() (s quic.TokenStore) { // You can read more on address validation here: // https://datatracker.ietf.org/doc/html/rfc9000#section-8.1 // Setting maxOrigins to 1 and tokensPerOrigin to 10 assuming that this is // more than enough for the way we use it (one connection per upstream). return quic.NewLRUTokenStore(1, 10) } // isQUICRetryError checks the error and determines whether it may signal that // we should re-create the QUIC connection. This requirement is caused by // quic-go issues, see the comments inside this function. // TODO(ameshkov): re-test when updating quic-go. func isQUICRetryError(err error) (ok bool) { var qAppErr *quic.ApplicationError if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { // This error is often returned when the server has been restarted, // and we try to use the same connection on the client-side. It seems, // that the old connections aren't closed immediately on the server-side // and that's why one can run into this. // In addition to that, quic-go HTTP3 client implementation does not // clean up dead connections (this one is specific to DoH3 upstream): // https://github.com/metacubex/quic-go/issues/765 return true } var qIdleErr *quic.IdleTimeoutError if errors.As(err, &qIdleErr) { // This error means that the connection was closed due to being idle. // In this case we should forcibly re-create the QUIC connection. // Reproducing is rather simple, stop the server and wait for 30 seconds // then try to send another request via the same upstream. return true } var resetErr *quic.StatelessResetError if errors.As(err, &resetErr) { // A stateless reset is sent when a server receives a QUIC packet that // it doesn't know how to decrypt. For instance, it may happen when // the server was recently rebooted. We should reconnect and try again // in this case. return true } var qTransportError *quic.TransportError if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError { // A transport error with the NO_ERROR error code could be sent by the // server when it considers that it's time to close the connection. // For example, Google DNS eventually closes an active connection with // the NO_ERROR code and "Connection max age expired" message: // https://github.com/AdguardTeam/dnsproxy/issues/283 return true } if errors.Is(err, quic.Err0RTTRejected) { // This error happens when we try to establish a 0-RTT connection with // a token the server is no more aware of. This can be reproduced by // restarting the QUIC server (it will clear its tokens cache). The // next connection attempt will return this error until the client's // tokens cache is purged. return true } return false } ================================================ FILE: core/Clash.Meta/dns/dot.go ================================================ package dns import ( "context" "fmt" "net" "runtime" "sync" "time" "github.com/metacubex/mihomo/common/deque" "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/tls" D "github.com/miekg/dns" ) const maxOldDotConns = 8 type dnsOverTLS struct { port string host string dialer *dnsDialer skipCertVerify bool disableReuse bool access sync.Mutex connections deque.Deque[net.Conn] // LIFO } var _ dnsClient = (*dnsOverTLS)(nil) // Address implements dnsClient func (t *dnsOverTLS) Address() string { return fmt.Sprintf("tls://%s", net.JoinHostPort(t.host, t.port)) } func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { // miekg/dns ExchangeContext doesn't respond to context cancel. // this is a workaround type result struct { msg *D.Msg err error } ch := make(chan result, 1) go func() { var msg *D.Msg var err error defer func() { ch <- result{msg, err} }() for { // retry loop; only retry when reusing old conn err = ctx.Err() // check context first if err != nil { return } var conn net.Conn isOldConn := true if !t.disableReuse { t.access.Lock() if t.connections.Len() > 0 { conn = t.connections.PopBack() } t.access.Unlock() } if conn == nil { conn, err = t.dialContext(ctx) if err != nil { return } isOldConn = false } dClient := &D.Client{ UDPSize: 4096, Timeout: 5 * time.Second, } dConn := &D.Conn{ Conn: conn, UDPSize: dClient.UDPSize, } msg, _, err = dClient.ExchangeWithConn(m, dConn) if err != nil { _ = conn.Close() conn = nil if isOldConn { // retry continue } return } if !t.disableReuse { t.access.Lock() if t.connections.Len() >= maxOldDotConns { oldConn := t.connections.PopFront() go oldConn.Close() // close in a new goroutine, not blocking the current task } t.connections.PushBack(conn) t.access.Unlock() } else { _ = conn.Close() } return } }() select { case <-ctx.Done(): return nil, ctx.Err() case ret := <-ch: return ret.msg, ret.err } } func (t *dnsOverTLS) dialContext(ctx context.Context) (net.Conn, error) { conn, err := t.dialer.DialContext(ctx, "tcp", net.JoinHostPort(t.host, t.port)) if err != nil { return nil, err } tlsConfig, err := ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: t.host, InsecureSkipVerify: t.skipCertVerify, }, }) if err != nil { _ = conn.Close() return nil, err } tlsConn := tls.Client(conn, tlsConfig) if err = tlsConn.HandshakeContext(ctx); err != nil { _ = conn.Close() return nil, err } conn = tlsConn return conn, nil } func (t *dnsOverTLS) ResetConnection() { if !t.disableReuse { t.access.Lock() for t.connections.Len() > 0 { oldConn := t.connections.PopFront() go oldConn.Close() // close in a new goroutine, not blocking the current task } t.access.Unlock() } } func (t *dnsOverTLS) Close() error { runtime.SetFinalizer(t, nil) t.ResetConnection() return nil } func newDoTClient(addr string, resolver *Resolver, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *dnsOverTLS { host, port, _ := net.SplitHostPort(addr) c := &dnsOverTLS{ port: port, host: host, dialer: newDNSDialer(resolver, proxyAdapter, proxyName), } c.connections.SetBaseCap(maxOldDotConns) if params["skip-cert-verify"] == "true" { c.skipCertVerify = true } if params["disable-reuse"] == "true" { c.disableReuse = true } runtime.SetFinalizer(c, (*dnsOverTLS).Close) return c } ================================================ FILE: core/Clash.Meta/dns/edns0_subnet.go ================================================ package dns import ( "net/netip" "github.com/miekg/dns" ) func setEdns0Subnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) bool { 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 if subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET); isEDNS0Subnet { if !override { return false } break findExists } } } } if optRecord == nil { optRecord = &dns.OPT{ Hdr: dns.RR_Header{ Name: ".", Rrtype: dns.TypeOPT, }, } message.Extra = append(message.Extra, optRecord) } if subnetOption == nil { subnetOption = new(dns.EDNS0_SUBNET) optRecord.Option = append(optRecord.Option, subnetOption) } subnetOption.Code = dns.EDNS0SUBNET if clientSubnet.Addr().Is4() { subnetOption.Family = 1 } else { subnetOption.Family = 2 } subnetOption.SourceNetmask = uint8(clientSubnet.Bits()) subnetOption.Address = clientSubnet.Addr().AsSlice() return true } ================================================ FILE: core/Clash.Meta/dns/enhancer.go ================================================ package dns import ( "errors" "net/netip" "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/component/fakeip" C "github.com/metacubex/mihomo/constant" ) type ResolverEnhancer struct { mode C.DNSMode fakeIPPool *fakeip.Pool fakeIPPool6 *fakeip.Pool fakeIPSkipper *fakeip.Skipper fakeIPTTL int mapping *lru.LruCache[netip.Addr, string] useHosts bool } func (h *ResolverEnhancer) FakeIPEnabled() bool { return h.mode == C.DNSFakeIP } func (h *ResolverEnhancer) MappingEnabled() bool { return h.mode == C.DNSFakeIP || h.mode == C.DNSMapping } func (h *ResolverEnhancer) IsExistFakeIP(ip netip.Addr) bool { if !h.FakeIPEnabled() { return false } if pool := h.fakeIPPool; pool != nil { if pool.Exist(ip) { return true } } if pool6 := h.fakeIPPool6; pool6 != nil { if pool6.Exist(ip) { return true } } return false } func (h *ResolverEnhancer) IsFakeIP(ip netip.Addr) bool { if !h.FakeIPEnabled() { return false } if pool := h.fakeIPPool; pool != nil { if pool.IPNet().Contains(ip) && ip != pool.Gateway() && ip != pool.Broadcast() { return true } } if pool6 := h.fakeIPPool6; pool6 != nil { if pool6.IPNet().Contains(ip) && ip != pool6.Gateway() && ip != pool6.Broadcast() { return true } } return false } func (h *ResolverEnhancer) IsFakeBroadcastIP(ip netip.Addr) bool { if !h.FakeIPEnabled() { return false } if pool := h.fakeIPPool; pool != nil { if pool.Broadcast() == ip { return true } } if pool6 := h.fakeIPPool6; pool6 != nil { if pool6.Broadcast() == ip { return true } } return false } func (h *ResolverEnhancer) FindHostByIP(ip netip.Addr) (string, bool) { if pool := h.fakeIPPool; pool != nil { if host, existed := pool.LookBack(ip); existed { return host, true } } if pool6 := h.fakeIPPool6; pool6 != nil { if host, existed := pool6.LookBack(ip); existed { return host, true } } if mapping := h.mapping; mapping != nil { if host, existed := h.mapping.Get(ip); existed { return host, true } } return "", false } func (h *ResolverEnhancer) InsertHostByIP(ip netip.Addr, host string) { if mapping := h.mapping; mapping != nil { h.mapping.Set(ip, host) } } func (h *ResolverEnhancer) FlushFakeIP() error { var errs []error if pool := h.fakeIPPool; pool != nil { if err := pool.FlushFakeIP(); err != nil { errs = append(errs, err) } } if pool6 := h.fakeIPPool6; pool6 != nil { if err := pool6.FlushFakeIP(); err != nil { errs = append(errs, err) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) { if h.mapping != nil && o.mapping != nil { o.mapping.CloneTo(h.mapping) } if h.fakeIPPool != nil && o.fakeIPPool != nil { h.fakeIPPool.CloneFrom(o.fakeIPPool) } if h.fakeIPPool6 != nil && o.fakeIPPool6 != nil { h.fakeIPPool6.CloneFrom(o.fakeIPPool6) } } func (h *ResolverEnhancer) StoreFakePoolState() { if h.fakeIPPool != nil { h.fakeIPPool.StoreState() } if h.fakeIPPool6 != nil { h.fakeIPPool6.StoreState() } } type EnhancerConfig struct { IPv6 bool EnhancedMode C.DNSMode FakeIPPool *fakeip.Pool FakeIPPool6 *fakeip.Pool FakeIPSkipper *fakeip.Skipper FakeIPTTL int UseHosts bool } func NewEnhancer(cfg EnhancerConfig) *ResolverEnhancer { e := &ResolverEnhancer{ mode: cfg.EnhancedMode, useHosts: cfg.UseHosts, } if cfg.EnhancedMode != C.DNSNormal { e.fakeIPPool = cfg.FakeIPPool if cfg.IPv6 { e.fakeIPPool6 = cfg.FakeIPPool6 } e.fakeIPSkipper = cfg.FakeIPSkipper e.fakeIPTTL = cfg.FakeIPTTL if e.fakeIPTTL < 1 { e.fakeIPTTL = 1 } e.mapping = lru.New(lru.WithSize[netip.Addr, string](4096)) } return e } ================================================ FILE: core/Clash.Meta/dns/middleware.go ================================================ package dns import ( "net/netip" "strings" "time" "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" icontext "github.com/metacubex/mihomo/context" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" ) type ( handler func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) middleware func(next handler) handler ) func withHosts(mapping *lru.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] if !isIPRequest(q) { return next(ctx, r) } host := strings.TrimRight(q.Name, ".") handleCName := func(resp *D.Msg, domain string) { rr := &D.CNAME{} rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeCNAME, Class: D.ClassINET, Ttl: 10} rr.Target = domain + "." resp.Answer = append([]D.RR{rr}, resp.Answer...) } record, ok := resolver.DefaultHosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA) if !ok { if record != nil && record.IsDomain { // replace request domain newR := r.Copy() newR.Question[0].Name = record.Domain + "." resp, err := next(ctx, newR) if err == nil { resp.Id = r.Id resp.Question = r.Question handleCName(resp, record.Domain) } return resp, err } return next(ctx, r) } msg := r.Copy() handleIPs := func() { for _, ipAddr := range record.IPs { if ipAddr.Is4() && q.Qtype == D.TypeA { rr := &D.A{} rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 10} rr.A = ipAddr.AsSlice() msg.Answer = append(msg.Answer, rr) if mapping != nil { mapping.SetWithExpire(ipAddr, host, time.Now().Add(time.Second*10)) } } else if ipAddr.Is6() && q.Qtype == D.TypeAAAA { rr := &D.AAAA{} rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 10} rr.AAAA = ipAddr.AsSlice() msg.Answer = append(msg.Answer, rr) if mapping != nil { mapping.SetWithExpire(ipAddr, host, time.Now().Add(time.Second*10)) } } } } switch q.Qtype { case D.TypeA: handleIPs() case D.TypeAAAA: handleIPs() case D.TypeCNAME: handleCName(r, record.Domain) default: return next(ctx, r) } ctx.SetType(icontext.DNSTypeHost) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true msg.RecursionAvailable = true return msg, nil } } } func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] if !isIPRequest(q) { return next(ctx, r) } msg, err := next(ctx, r) if err != nil { return nil, err } host := strings.TrimRight(q.Name, ".") for _, ans := range msg.Answer { var ip netip.Addr var ttl uint32 switch a := ans.(type) { case *D.A: ip, _ = netip.AddrFromSlice(a.A) ttl = a.Hdr.Ttl case *D.AAAA: ip, _ = netip.AddrFromSlice(a.AAAA) ttl = a.Hdr.Ttl default: continue } if !ip.IsValid() { continue } if !ip.IsGlobalUnicast() { continue } ip = ip.Unmap() if ttl < 1 { ttl = 1 } mapping.SetWithExpire(ip, host, time.Now().Add(time.Second*time.Duration(ttl))) } return msg, nil } } } func withFakeIP(skipper *fakeip.Skipper, fakePool *fakeip.Pool, fakePool6 *fakeip.Pool, fakeIPTTL int) middleware { return func(next handler) handler { return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] host := strings.TrimRight(q.Name, ".") if skipper.ShouldSkipped(host) { return next(ctx, r) } var rr D.RR switch q.Qtype { case D.TypeA: if fakePool == nil { return handleMsgWithEmptyAnswer(r), nil } ip := fakePool.Lookup(host) rr = &D.A{ Hdr: D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL}, A: ip.AsSlice(), } case D.TypeAAAA: if fakePool6 == nil { return handleMsgWithEmptyAnswer(r), nil } ip := fakePool6.Lookup(host) rr = &D.AAAA{ Hdr: D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: dnsDefaultTTL}, AAAA: ip.AsSlice(), } case D.TypeSVCB, D.TypeHTTPS: return handleMsgWithEmptyAnswer(r), nil default: return next(ctx, r) } msg := r.Copy() msg.Answer = []D.RR{rr} ctx.SetType(icontext.DNSTypeFakeIP) setMsgTTL(msg, uint32(fakeIPTTL)) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true msg.RecursionAvailable = true return msg, nil } } } func withResolver(resolver *Resolver) handler { return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { ctx.SetType(icontext.DNSTypeRaw) q := r.Question[0] // return a empty AAAA msg when ipv6 disabled if !resolver.ipv6 && q.Qtype == D.TypeAAAA { return handleMsgWithEmptyAnswer(r), nil } msg, err := resolver.ExchangeContext(ctx, r) if err != nil { log.Debugln("[DNS Server] Exchange %s failed: %v", q.String(), err) return msg, err } msg.SetRcode(r, msg.Rcode) msg.Authoritative = true return msg, nil } } func compose(middlewares []middleware, endpoint handler) handler { length := len(middlewares) h := endpoint for i := length - 1; i >= 0; i-- { middleware := middlewares[i] h = middleware(h) } return h } func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { var middlewares []middleware if mapper.useHosts { middlewares = append(middlewares, withHosts(mapper.mapping)) } if mapper.mode == C.DNSFakeIP { middlewares = append(middlewares, withFakeIP(mapper.fakeIPSkipper, mapper.fakeIPPool, mapper.fakeIPPool6, mapper.fakeIPTTL)) } if mapper.mode != C.DNSNormal { middlewares = append(middlewares, withMapping(mapper.mapping)) } return compose(middlewares, withResolver(resolver)) } ================================================ FILE: core/Clash.Meta/dns/patch_android.go ================================================ //go:build android package dns import ( "github.com/metacubex/mihomo/component/resolver" ) var systemResolver []dnsClient func FlushCacheWithDefaultResolver() { resolver.ClearCache() resolver.ResetConnection() } func UpdateSystemDNS(addr []string) { if len(addr) == 0 { systemResolver = nil } ns := make([]NameServer, 0, len(addr)) for _, d := range addr { ns = append(ns, NameServer{Addr: d}) } systemResolver = transform(ns, nil) } func (c *systemClient) getDnsClients() ([]dnsClient, error) { return systemResolver, nil } func (c *systemClient) ResetConnection() { for _, r := range systemResolver { r.ResetConnection() } } ================================================ FILE: core/Clash.Meta/dns/policy.go ================================================ package dns import ( "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" ) type dnsPolicy interface { Match(domain string) []dnsClient } type domainTriePolicy struct { *trie.DomainTrie[[]dnsClient] } func (p domainTriePolicy) Match(domain string) []dnsClient { record := p.DomainTrie.Search(domain) if record != nil { return record.Data() } return nil } type domainMatcherPolicy struct { matcher C.DomainMatcher dnsClients []dnsClient } func (p domainMatcherPolicy) Match(domain string) []dnsClient { if p.matcher.MatchDomain(domain) { return p.dnsClients } return nil } ================================================ FILE: core/Clash.Meta/dns/rcode.go ================================================ package dns import ( "context" "fmt" D "github.com/miekg/dns" ) func newRCodeClient(addr string) rcodeClient { var rcode int switch addr { case "success": rcode = D.RcodeSuccess case "format_error": rcode = D.RcodeFormatError case "server_failure": rcode = D.RcodeServerFailure case "name_error": rcode = D.RcodeNameError case "not_implemented": rcode = D.RcodeNotImplemented case "refused": rcode = D.RcodeRefused default: panic(fmt.Errorf("unsupported RCode type: %s", addr)) } return rcodeClient{ rcode: rcode, addr: "rcode://" + addr, } } type rcodeClient struct { rcode int addr string } var _ dnsClient = rcodeClient{} func (r rcodeClient) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { m.Response = true m.Rcode = r.rcode return m, nil } func (r rcodeClient) Address() string { return r.addr } func (r rcodeClient) ResetConnection() {} ================================================ FILE: core/Clash.Meta/dns/resolver.go ================================================ package dns import ( "context" "errors" "net/netip" "time" "github.com/metacubex/mihomo/common/arc" "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/common/singleflight" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" "github.com/samber/lo" "golang.org/x/exp/maps" ) type dnsClient interface { ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) Address() string ResetConnection() } type dnsCache interface { GetWithExpire(key string) (*D.Msg, time.Time, bool) SetWithExpire(key string, value *D.Msg, expire time.Time) Clear() } type result struct { Msg *D.Msg Error error } type Resolver struct { ipv6 bool ipv6Timeout time.Duration main []dnsClient fallback []dnsClient fallbackDomainFilters []C.DomainMatcher fallbackIPFilters []C.IpMatcher group singleflight.Group[*D.Msg] cache dnsCache policy []dnsPolicy defaultResolver *Resolver } func (r *Resolver) LookupIPPrimaryIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) { ch := make(chan []netip.Addr, 1) go func() { defer close(ch) ip, err := r.lookupIP(ctx, host, D.TypeAAAA) if err != nil { return } ch <- ip }() ips, err = r.lookupIP(ctx, host, D.TypeA) if err == nil { return } ip, open := <-ch if !open { return nil, resolver.ErrIPNotFound } return ip, nil } func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) { ch := make(chan []netip.Addr, 1) go func() { defer close(ch) ip, err := r.lookupIP(ctx, host, D.TypeAAAA) if err != nil { return } ch <- ip }() ips, err = r.lookupIP(ctx, host, D.TypeA) var waitIPv6 *time.Timer if r != nil && r.ipv6Timeout > 0 { waitIPv6 = time.NewTimer(r.ipv6Timeout) } else { waitIPv6 = time.NewTimer(100 * time.Millisecond) } defer waitIPv6.Stop() select { case ipv6s, open := <-ch: if !open && err != nil { return nil, resolver.ErrIPNotFound } ips = append(ips, ipv6s...) case <-waitIPv6.C: // wait ipv6 result } return ips, nil } // LookupIPv4 request with TypeA func (r *Resolver) LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) { return r.lookupIP(ctx, host, D.TypeA) } // LookupIPv6 request with TypeAAAA func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) { return r.lookupIP(ctx, host, D.TypeAAAA) } func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { for _, filter := range r.fallbackIPFilters { if filter.MatchIp(ip) { return true } } return false } func (r *Resolver) ResolveECH(ctx context.Context, host string) ([]byte, error) { query := &D.Msg{} query.SetQuestion(D.Fqdn(host), D.TypeHTTPS) msg, err := r.ExchangeContext(ctx, query) if err != nil { return nil, err } for _, rr := range msg.Answer { switch resource := rr.(type) { case *D.HTTPS: for _, value := range resource.Value { if echConfig, ok := value.(*D.SVCBECHConfig); ok { return echConfig.ECH, nil } } } } return nil, errors.New("no ECH config found in DNS records") } // ExchangeContext a batch of dns request with context.Context, and it use cache func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { if len(m.Question) == 0 { return nil, errors.New("should have one question at least") } continueFetch := false defer func() { if continueFetch || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { go func() { ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) defer cancel() _, _ = r.exchangeWithoutCache(ctx, m) // ignore result, just for putMsgToCache }() } }() q := m.Question[0] domain := msgToDomain(m) msg, expireTime, hit := getMsgFromCache(r.cache, q) if hit { log.Debugln("[DNS] cache hit %s --> %s, expire at %s", domain, msgToLogString(msg), expireTime.Format("2006-01-02 15:04:05")) now := time.Now() if expireTime.Before(now) { setMsgTTL(msg, uint32(1)) // Continue fetch continueFetch = true } else { // updating TTL by subtracting common delta time from each DNS record updateMsgTTL(msg, uint32(time.Until(expireTime).Seconds())) } return } return r.exchangeWithoutCache(ctx, m) } // ExchangeWithoutCache a batch of dns request, and it do NOT GET from cache func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { q := m.Question[0] retryNum := 0 retryMax := 3 fn := func() (result *D.Msg, err error) { ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) // reset timeout in singleflight defer cancel() cache := false defer func() { if err != nil { result = &D.Msg{} result.Opcode = retryNum retryNum++ return } if cache { putMsgToCache(r.cache, q, result) } }() isIPReq := isIPRequest(q) if isIPReq { cache = true return r.ipExchange(ctx, m) } if matched := r.matchPolicy(m); len(matched) != 0 { result, cache, err = batchExchange(ctx, matched, m) return } result, cache, err = batchExchange(ctx, r.main, m) return } ch := r.group.DoChan(q.String(), fn) var result singleflight.Result[*D.Msg] select { case result = <-ch: break case <-ctx.Done(): select { case result = <-ch: // maybe ctxDone and chFinish in same time, get DoChan's result as much as possible break default: go func() { // start a retrying monitor in background result := <-ch ret, err, shared := result.Val, result.Err, result.Shared if err != nil && !shared && ret.Opcode < retryMax { // retry r.group.DoChan(q.String(), fn) } }() return nil, ctx.Err() } } ret, err, shared := result.Val, result.Err, result.Shared if err != nil && !shared && ret.Opcode < retryMax { // retry r.group.DoChan(q.String(), fn) } if err == nil { msg = ret if shared { msg = msg.Copy() } } return } func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { if r.policy == nil { return nil } domain := msgToDomain(m) if domain == "" { return nil } for _, policy := range r.policy { if dnsClients := policy.Match(domain); len(dnsClients) > 0 { return dnsClients } } return nil } func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool { if r.fallback == nil || len(r.fallbackDomainFilters) == 0 { return false } domain := msgToDomain(m) if domain == "" { return false } for _, df := range r.fallbackDomainFilters { if df.MatchDomain(domain) { return true } } return false } func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { if matched := r.matchPolicy(m); len(matched) != 0 { res := <-r.asyncExchange(ctx, matched, m) return res.Msg, res.Error } onlyFallback := r.shouldOnlyQueryFallback(m) if onlyFallback { res := <-r.asyncExchange(ctx, r.fallback, m) return res.Msg, res.Error } msgCh := r.asyncExchange(ctx, r.main, m) if r.fallback == nil || len(r.fallback) == 0 { // directly return if no fallback servers are available res := <-msgCh msg, err = res.Msg, res.Error return } res := <-msgCh if res.Error == nil { if ips := msgToIP(res.Msg); len(ips) != 0 { shouldNotFallback := lo.EveryBy(ips, func(ip netip.Addr) bool { return !r.shouldIPFallback(ip) }) if shouldNotFallback { msg, err = res.Msg, res.Error // no need to wait for fallback result return } } } res = <-r.asyncExchange(ctx, r.fallback, m) msg, err = res.Msg, res.Error return } func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) { ip, err := netip.ParseAddr(host) if err == nil { ip = ip.Unmap() isIPv4 := ip.Is4() if dnsType == D.TypeAAAA && !isIPv4 { return []netip.Addr{ip}, nil } else if dnsType == D.TypeA && isIPv4 { return []netip.Addr{ip}, nil } else { return []netip.Addr{}, resolver.ErrIPVersion } } query := &D.Msg{} query.SetQuestion(D.Fqdn(host), dnsType) msg, err := r.ExchangeContext(ctx, query) if err != nil { return []netip.Addr{}, err } ips = msgToIP(msg) ipLength := len(ips) if ipLength == 0 { return []netip.Addr{}, resolver.ErrIPNotFound } return } func (r *Resolver) asyncExchange(ctx context.Context, client []dnsClient, msg *D.Msg) <-chan *result { ch := make(chan *result, 1) go func() { res, _, err := batchExchange(ctx, client, msg) ch <- &result{Msg: res, Error: err} }() return ch } // Invalid return this resolver can or can't be used func (r *Resolver) Invalid() bool { if r == nil { return false } return len(r.main) > 0 } func (r *Resolver) ClearCache() { if r != nil && r.cache != nil { r.cache.Clear() } } func (r *Resolver) ResetConnection() { if r != nil { for _, c := range r.main { c.ResetConnection() } for _, c := range r.fallback { c.ResetConnection() } if dr := r.defaultResolver; dr != nil { dr.ResetConnection() } } } type NameServer struct { Net string Addr string ProxyAdapter C.ProxyAdapter ProxyName string Params map[string]string PreferH3 bool } func (ns NameServer) Equal(ns2 NameServer) bool { defer func() { // C.ProxyAdapter compare maybe panic, just ignore recover() }() if ns.Net == ns2.Net && ns.Addr == ns2.Addr && ns.ProxyAdapter == ns2.ProxyAdapter && ns.ProxyName == ns2.ProxyName && maps.Equal(ns.Params, ns2.Params) && ns.PreferH3 == ns2.PreferH3 { return true } return false } type Policy struct { Domain string Matcher C.DomainMatcher NameServers []NameServer } type Config struct { Main, Fallback []NameServer Default []NameServer ProxyServer []NameServer DirectServer []NameServer DirectFollowPolicy bool IPv6 bool IPv6Timeout uint FallbackIPFilter []C.IpMatcher FallbackDomainFilter []C.DomainMatcher Policy []Policy ProxyServerPolicy []Policy CacheAlgorithm string CacheMaxSize int } func (config Config) newCache() dnsCache { if config.CacheMaxSize == 0 { config.CacheMaxSize = 4096 } switch config.CacheAlgorithm { case "arc": return arc.New(arc.WithSize[string, *D.Msg](config.CacheMaxSize)) default: return lru.New(lru.WithSize[string, *D.Msg](config.CacheMaxSize), lru.WithStale[string, *D.Msg](true)) } } type Resolvers struct { *Resolver ProxyResolver *Resolver DirectResolver *Resolver } func (rs Resolvers) ClearCache() { rs.Resolver.ClearCache() rs.ProxyResolver.ClearCache() rs.DirectResolver.ClearCache() } func (rs Resolvers) ResetConnection() { rs.Resolver.ResetConnection() rs.ProxyResolver.ResetConnection() rs.DirectResolver.ResetConnection() } func NewResolver(config Config) (rs Resolvers) { defaultResolver := &Resolver{ main: transform(config.Default, nil), cache: config.newCache(), ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } var nameServerCache []struct { NameServer dnsClient } cacheTransform := func(nameserver []NameServer) (result []dnsClient) { LOOP: for _, ns := range nameserver { for _, nsc := range nameServerCache { if nsc.NameServer.Equal(ns) { result = append(result, nsc.dnsClient) continue LOOP } } // not in cache dc := transform([]NameServer{ns}, defaultResolver) if len(dc) > 0 { dc := dc[0] nameServerCache = append(nameServerCache, struct { NameServer dnsClient }{NameServer: ns, dnsClient: dc}) result = append(result, dc) } } return } makePolicy := func(policies []Policy) (dnsPolicies []dnsPolicy) { var triePolicy *trie.DomainTrie[[]dnsClient] insertPolicy := func(policy dnsPolicy) { if triePolicy != nil { triePolicy.Optimize() dnsPolicies = append(dnsPolicies, domainTriePolicy{triePolicy}) triePolicy = nil } if policy != nil { dnsPolicies = append(dnsPolicies, policy) } } for _, policy := range policies { if policy.Matcher != nil { insertPolicy(domainMatcherPolicy{matcher: policy.Matcher, dnsClients: cacheTransform(policy.NameServers)}) } else { if triePolicy == nil { triePolicy = trie.New[[]dnsClient]() } _ = triePolicy.Insert(policy.Domain, cacheTransform(policy.NameServers)) } } insertPolicy(nil) return } r := &Resolver{ ipv6: config.IPv6, main: cacheTransform(config.Main), cache: config.newCache(), ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, policy: makePolicy(config.Policy), } r.defaultResolver = defaultResolver rs.Resolver = r if len(config.ProxyServer) != 0 { rs.ProxyResolver = &Resolver{ ipv6: config.IPv6, main: cacheTransform(config.ProxyServer), cache: config.newCache(), ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, policy: makePolicy(config.ProxyServerPolicy), } } if len(config.DirectServer) != 0 { rs.DirectResolver = &Resolver{ ipv6: config.IPv6, main: cacheTransform(config.DirectServer), cache: config.newCache(), ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } if config.DirectFollowPolicy { rs.DirectResolver.policy = r.policy } } if len(config.Fallback) != 0 { r.fallback = cacheTransform(config.Fallback) r.fallbackIPFilters = config.FallbackIPFilter r.fallbackDomainFilters = config.FallbackDomainFilter } return } var ParseNameServer func(servers []string) ([]NameServer, error) // define in config/config.go ================================================ FILE: core/Clash.Meta/dns/server.go ================================================ package dns import ( "context" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" ) var ( address string server = &Server{} dnsDefaultTTL uint32 = 600 ) type Server struct { service resolver.Service tcpServer *D.Server udpServer *D.Server } // ServeDNS implement D.Handler ServeDNS func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { msg, err := s.service.ServeMsg(context.Background(), r) if err != nil { m := new(D.Msg) m.SetRcode(r, D.RcodeServerFailure) // does not matter if this write fails w.WriteMsg(m) return } msg.Compress = true w.WriteMsg(msg) } func (s *Server) SetService(service resolver.Service) { s.service = service } func ReCreateServer(addr string, service resolver.Service) { if addr == address && service != nil { server.SetService(service) return } if server.tcpServer != nil { _ = server.tcpServer.Shutdown() server.tcpServer = nil } if server.udpServer != nil { _ = server.udpServer.Shutdown() server.udpServer = nil } server.service = nil address = "" if addr == "" || service == nil { return } var err error defer func() { if err != nil { log.Errorln("Start DNS server error: %s", err.Error()) } }() _, port, err := net.SplitHostPort(addr) if port == "0" || port == "" || err != nil { return } address = addr server = &Server{service: service} go func() { p, err := inbound.ListenPacket("udp", addr) if err != nil { log.Errorln("Start DNS server(UDP) error: %s", err.Error()) return } if err := sockopt.UDPReuseaddr(p); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } log.Infoln("DNS server(UDP) listening at: %s", p.LocalAddr().String()) server.udpServer = &D.Server{Addr: addr, PacketConn: p, Handler: server} _ = server.udpServer.ActivateAndServe() }() go func() { l, err := inbound.Listen("tcp", addr) if err != nil { log.Errorln("Start DNS server(TCP) error: %s", err.Error()) return } log.Infoln("DNS server(TCP) listening at: %s", l.Addr().String()) server.tcpServer = &D.Server{Addr: addr, Listener: l, Handler: server} _ = server.tcpServer.ActivateAndServe() }() } ================================================ FILE: core/Clash.Meta/dns/service.go ================================================ package dns import ( "context" "errors" "github.com/metacubex/mihomo/component/resolver" icontext "github.com/metacubex/mihomo/context" D "github.com/miekg/dns" ) type Service struct { handler handler } // ServeMsg implement [resolver.Service] ResolveMsg func (s *Service) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { if len(msg.Question) == 0 { return nil, errors.New("at least one question is required") } return s.handler(icontext.NewDNSContext(ctx), msg) } var _ resolver.Service = (*Service)(nil) func NewService(resolver *Resolver, mapper *ResolverEnhancer) *Service { return &Service{handler: newHandler(resolver, mapper)} } ================================================ FILE: core/Clash.Meta/dns/system.go ================================================ package dns import ( "context" "fmt" "strings" "sync" "time" "github.com/metacubex/mihomo/component/resolver" D "github.com/miekg/dns" ) const ( SystemDnsFlushTime = 5 * time.Minute SystemDnsDeleteTimes = 12 // 12*5 = 60min ) type systemDnsClient struct { disableTimes uint32 dnsClient } type systemClient struct { mu sync.Mutex dnsClients map[string]*systemDnsClient lastFlush time.Time defaultNS []dnsClient } func (c *systemClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { dnsClients, err := c.getDnsClients() if len(dnsClients) == 0 && len(c.defaultNS) > 0 { dnsClients = c.defaultNS err = nil } if err != nil { return } msg, _, err = batchExchange(ctx, dnsClients, m) return } // Address implements dnsClient func (c *systemClient) Address() string { dnsClients, _ := c.getDnsClients() isDefault := "" if len(dnsClients) == 0 && len(c.defaultNS) > 0 { dnsClients = c.defaultNS isDefault = "[defaultNS]" } addrs := make([]string, 0, len(dnsClients)) for _, c := range dnsClients { addrs = append(addrs, c.Address()) } return fmt.Sprintf("system%s(%s)", isDefault, strings.Join(addrs, ",")) } var _ dnsClient = (*systemClient)(nil) func newSystemClient() *systemClient { return &systemClient{ dnsClients: map[string]*systemDnsClient{}, } } func init() { r := NewResolver(Config{}) c := newSystemClient() c.defaultNS = transform([]NameServer{{Addr: "114.114.114.114:53"}, {Addr: "8.8.8.8:53"}}, nil) r.main = []dnsClient{c} resolver.SystemResolver = r } ================================================ FILE: core/Clash.Meta/dns/system_common.go ================================================ //go:build !android package dns import ( "net" "time" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/log" "golang.org/x/exp/slices" ) func (c *systemClient) getDnsClients() ([]dnsClient, error) { c.mu.Lock() defer c.mu.Unlock() var err error if time.Since(c.lastFlush) > SystemDnsFlushTime { var nameservers []string if nameservers, err = dnsReadConfig(); err == nil { log.Debugln("[DNS] system dns update to %s", nameservers) for _, addr := range nameservers { if resolver.IsSystemDnsBlacklisted(addr) { continue } if _, ok := c.dnsClients[addr]; !ok { clients := transform( []NameServer{{ Addr: net.JoinHostPort(addr, "53"), Net: "udp", }}, nil, ) if len(clients) > 0 { c.dnsClients[addr] = &systemDnsClient{ disableTimes: 0, dnsClient: clients[0], } } } } available := 0 for nameserver, sdc := range c.dnsClients { if slices.Contains(nameservers, nameserver) { sdc.disableTimes = 0 // enable available++ } else { if sdc.disableTimes > SystemDnsDeleteTimes { delete(c.dnsClients, nameserver) // drop too old dnsClient } else { sdc.disableTimes++ } } } if available > 0 { c.lastFlush = time.Now() } } } dnsClients := make([]dnsClient, 0, len(c.dnsClients)) for _, sdc := range c.dnsClients { if sdc.disableTimes == 0 { dnsClients = append(dnsClients, sdc.dnsClient) } } if len(dnsClients) > 0 { return dnsClients, nil } return nil, err } func (c *systemClient) ResetConnection() {} ================================================ FILE: core/Clash.Meta/dns/system_posix.go ================================================ //go:build !windows package dns import ( "bufio" "fmt" "net/netip" "os" "strings" ) const resolvConf = "/etc/resolv.conf" func dnsReadConfig() (servers []string, err error) { file, err := os.Open(resolvConf) if err != nil { err = fmt.Errorf("failed to read %s: %w", resolvConf, err) return } defer func() { _ = file.Close() }() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if len(line) > 0 && (line[0] == ';' || line[0] == '#') { // comment. continue } f := strings.Fields(line) if len(f) < 1 { continue } switch f[0] { case "nameserver": // add one name server if len(f) > 1 { if addr, err := netip.ParseAddr(f[1]); err == nil { servers = append(servers, addr.String()) } } } } return } ================================================ FILE: core/Clash.Meta/dns/system_windows.go ================================================ //go:build windows package dns import ( "net/netip" "os" "strconv" "syscall" "unsafe" "golang.org/x/exp/slices" "golang.org/x/sys/windows" ) func dnsReadConfig() (servers []string, err error) { aas, err := adapterAddresses() if err != nil { return } for _, aa := range aas { // Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs. if aa.OperStatus != windows.IfOperStatusUp { continue } // Only take interfaces which have at least one gateway if aa.FirstGatewayAddress == nil { continue } for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next { sa, err := dns.Address.Sockaddr.Sockaddr() if err != nil { continue } var ip netip.Addr switch sa := sa.(type) { case *syscall.SockaddrInet4: ip = netip.AddrFrom4(sa.Addr) case *syscall.SockaddrInet6: if sa.Addr[0] == 0xfe && sa.Addr[1] == 0xc0 { // Ignore these fec0/10 ones. Windows seems to // populate them as defaults on its misc rando // interfaces. continue } ip = netip.AddrFrom16(sa.Addr) if sa.ZoneId != 0 { ip = ip.WithZone(strconv.FormatInt(int64(sa.ZoneId), 10)) } //continue default: // Unexpected type. continue } ipStr := ip.String() if slices.Contains(servers, ipStr) { continue } servers = append(servers, ipStr) } } return } // adapterAddresses returns a list of IP adapter and address // structures. The structure contains an IP adapter and flattened // multiple IP addresses including unicast, anycast and multicast // addresses. 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: core/Clash.Meta/dns/util.go ================================================ package dns import ( "context" "errors" "fmt" "net/netip" "strconv" "strings" "time" "github.com/metacubex/mihomo/common/picker" "github.com/metacubex/mihomo/component/ech/echparser" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" "github.com/samber/lo" "golang.org/x/exp/slices" ) const ( MaxMsgSize = 65535 ) const serverFailureCacheTTL uint32 = 5 func minimalTTL(records []D.RR) uint32 { rr := lo.MinBy(records, func(r1 D.RR, r2 D.RR) bool { return r1.Header().Ttl < r2.Header().Ttl }) if rr == nil { return 0 } return rr.Header().Ttl } func updateTTL(records []D.RR, ttl uint32) { if len(records) == 0 { return } delta := minimalTTL(records) - ttl for i := range records { records[i].Header().Ttl = lo.Clamp(records[i].Header().Ttl-delta, 1, records[i].Header().Ttl) } } // getMsgFromCache returns a cached dns message if it exists, otherwise returns nil. // the returned msg is a copy of the original msg, so it can be modified without affecting the original msg. func getMsgFromCache(c dnsCache, q D.Question) (*D.Msg, time.Time, bool) { msg, expireTime, hit := c.GetWithExpire(q.String()) if msg != nil { msg = msg.Copy() // never modify the original msg } return msg, expireTime, hit } // putMsgToCache puts a dns message into the cache. // the msg is copied before being stored in the cache, so it can be modified without affecting the original msg. func putMsgToCache(c dnsCache, q D.Question, msg *D.Msg) { // skip dns cache for acme challenge if q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge.") { log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name) return } msg = msg.Copy() // never modify the original msg // OPT RRs MUST NOT be cached, forwarded, or stored in or loaded from master files. msg.Extra = lo.Filter(msg.Extra, func(rr D.RR, index int) bool { return rr.Header().Rrtype != D.TypeOPT }) var ttl uint32 if msg.Rcode == D.RcodeServerFailure { // [...] a resolver MAY cache a server failure response. // If it does so it MUST NOT cache it for longer than five (5) minutes [...] ttl = serverFailureCacheTTL } else { ttl = minimalTTL(lo.Concat(msg.Answer, msg.Ns, msg.Extra)) } if ttl == 0 { return } c.SetWithExpire(q.String(), msg, time.Now().Add(time.Duration(ttl)*time.Second)) } func setMsgTTL(msg *D.Msg, ttl uint32) { for _, answer := range msg.Answer { answer.Header().Ttl = ttl } for _, ns := range msg.Ns { ns.Header().Ttl = ttl } for _, extra := range msg.Extra { extra.Header().Ttl = ttl } } func updateMsgTTL(msg *D.Msg, ttl uint32) { updateTTL(msg.Answer, ttl) updateTTL(msg.Ns, ttl) updateTTL(msg.Extra, ttl) } func isIPRequest(q D.Question) bool { return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA || q.Qtype == D.TypeCNAME) } func transform(servers []NameServer, resolver *Resolver) []dnsClient { ret := make([]dnsClient, 0, len(servers)) for _, s := range servers { var c dnsClient switch s.Net { case "tls": c = newDoTClient(s.Addr, resolver, s.Params, s.ProxyAdapter, s.ProxyName) case "https": c = newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName) case "dhcp": c = newDHCPClient(s.Addr) case "system": c = newSystemClient() case "rcode": c = newRCodeClient(s.Addr) case "quic": c = newDoQ(s.Addr, resolver, s.Params, s.ProxyAdapter, s.ProxyName) default: c = newClient(s.Addr, resolver, s.Net, s.Params, s.ProxyAdapter, s.ProxyName) } c = warpClientWithEdns0Subnet(c, s.Params) c = warpClientWithDisableTypes(c, s.Params) ret = append(ret, c) } return ret } type clientWithDisableTypes struct { dnsClient disableTypes map[uint16]struct{} } func (c clientWithDisableTypes) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { // filter dns request if slices.ContainsFunc(m.Question, c.inQuestion) { // In fact, DNS requests are not allowed to contain multiple questions: // https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071 // so, when we find a question containing the type, we can simply discard the entire dns request. return handleMsgWithEmptyAnswer(m), nil } // do real exchange msg, err = c.dnsClient.ExchangeContext(ctx, m) if err != nil { return } // filter dns response msg.Answer = slices.DeleteFunc(msg.Answer, c.inRR) msg.Ns = slices.DeleteFunc(msg.Ns, c.inRR) msg.Extra = slices.DeleteFunc(msg.Extra, c.inRR) return } func (c clientWithDisableTypes) inQuestion(q D.Question) bool { _, ok := c.disableTypes[q.Qtype] return ok } func (c clientWithDisableTypes) inRR(rr D.RR) bool { _, ok := c.disableTypes[rr.Header().Rrtype] return ok } func warpClientWithDisableTypes(c dnsClient, params map[string]string) dnsClient { disableTypes := make(map[uint16]struct{}) if params["disable-ipv4"] == "true" { disableTypes[D.TypeA] = struct{}{} } if params["disable-ipv6"] == "true" { disableTypes[D.TypeAAAA] = struct{}{} } for key, value := range params { const prefix = "disable-qtype-" if strings.HasPrefix(key, prefix) && value == "true" { // eg: disable-qtype-65=true qType, err := strconv.ParseUint(key[len(prefix):], 10, 16) if err != nil { continue } if _, ok := D.TypeToRR[uint16(qType)]; !ok { // check valid RR_Header.Rrtype and Question.qtype continue } disableTypes[uint16(qType)] = struct{}{} } } if len(disableTypes) > 0 { return clientWithDisableTypes{c, disableTypes} } return c } type clientWithEdns0Subnet struct { dnsClient ecsPrefix netip.Prefix ecsOverride bool } func (c clientWithEdns0Subnet) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { m = m.Copy() setEdns0Subnet(m, c.ecsPrefix, c.ecsOverride) return c.dnsClient.ExchangeContext(ctx, m) } func warpClientWithEdns0Subnet(c dnsClient, params map[string]string) dnsClient { var ecsPrefix netip.Prefix var ecsOverride bool if ecs := params["ecs"]; ecs != "" { prefix, err := netip.ParsePrefix(ecs) if err != nil { addr, err := netip.ParseAddr(ecs) if err != nil { log.Warnln("DNS [%s] config with invalid ecs: %s", c.Address(), ecs) } else { ecsPrefix = netip.PrefixFrom(addr, addr.BitLen()) } } else { ecsPrefix = prefix } } if ecsPrefix.IsValid() { log.Debugln("DNS [%s] config with ecs: %s", c.Address(), ecsPrefix) if params["ecs-override"] == "true" { ecsOverride = true } return clientWithEdns0Subnet{c, ecsPrefix, ecsOverride} } return c } func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg { msg := &D.Msg{} msg.Answer = []D.RR{} msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true msg.RecursionAvailable = true return msg } func msgToIP(msg *D.Msg) (ips []netip.Addr) { for _, answer := range msg.Answer { var ip netip.Addr switch ans := answer.(type) { case *D.AAAA: ip, _ = netip.AddrFromSlice(ans.AAAA) case *D.A: ip, _ = netip.AddrFromSlice(ans.A) default: continue } if !ip.IsValid() { continue } ip = ip.Unmap() ips = append(ips, ip) } return } func msgToDomain(msg *D.Msg) string { if len(msg.Question) > 0 { return strings.TrimRight(msg.Question[0].Name, ".") } return "" } func msgToQtype(msg *D.Msg) (uint16, string) { if len(msg.Question) > 0 { qType := msg.Question[0].Qtype return qType, D.Type(qType).String() } return 0, "" } func msgToHTTPSRRInfo(msg *D.Msg) string { var alpns []string var publicName string var hasIPv4, hasIPv6 bool collect := func(rrs []D.RR) { for _, rr := range rrs { httpsRR, ok := rr.(*D.HTTPS) if !ok { continue } for _, kv := range httpsRR.Value { switch v := kv.(type) { case *D.SVCBAlpn: if len(alpns) == 0 && len(v.Alpn) > 0 { alpns = append(alpns, v.Alpn...) } case *D.SVCBIPv4Hint: if len(v.Hint) > 0 { hasIPv4 = true } case *D.SVCBIPv6Hint: if len(v.Hint) > 0 { hasIPv6 = true } case *D.SVCBECHConfig: if publicName == "" && len(v.ECH) > 0 { if cfgs, err := echparser.ParseECHConfigList(v.ECH); err == nil && len(cfgs) > 0 { publicName = string(cfgs[0].PublicName) } } } } } } collect(msg.Answer) //TODO: Do we need to process the data in msg.Extra? // If so, do we need to validate whether the domain names within it match our request? // To simplify the problem, let's ignore it for now. //collect(msg.Extra) if len(alpns) == 0 && publicName == "" && !hasIPv4 && !hasIPv6 { return "" } var parts []string if len(alpns) > 0 { parts = append(parts, "alpn:"+strings.Join(alpns, ",")) } if publicName != "" { parts = append(parts, "pn:"+publicName) } if hasIPv4 { parts = append(parts, "ipv4hint") } if hasIPv6 { parts = append(parts, "ipv6hint") } return strings.Join(parts, ";") } func msgToLogString(msg *D.Msg) string { qType, qTypeStr := msgToQtype(msg) switch qType { case D.TypeHTTPS: return fmt.Sprintf("[%s] %s", msgToHTTPSRRInfo(msg), qTypeStr) default: return fmt.Sprintf("%s %s", msgToIP(msg), qTypeStr) } } func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) { cache = true fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout) defer fast.Close() domain := msgToDomain(m) _, qTypeStr := msgToQtype(m) for _, client := range clients { if _, isRCodeClient := client.(rcodeClient); isRCodeClient { msg, err = client.ExchangeContext(ctx, m) return msg, false, err } client := client // shadow define client to ensure the value captured by the closure will not be changed in the next loop fast.Go(func() (*D.Msg, error) { log.Debugln("[DNS] resolve %s %s from %s", domain, qTypeStr, client.Address()) m, err := client.ExchangeContext(ctx, m) if err != nil { return nil, err } else if cache && (m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused) { // currently, cache indicates whether this msg was from a RCode client, // so we would ignore RCode errors from RCode clients. return nil, errors.New("server failure: " + D.RcodeToString[m.Rcode]) } log.Debugln("[DNS] %s --> %s from %s", domain, msgToLogString(m), client.Address()) return m, nil }) } msg = fast.Wait() if msg == nil { err = errors.New("all DNS requests failed") if fErr := fast.Error(); fErr != nil { err = fmt.Errorf("%w, first error: %w", err, fErr) } } return } ================================================ FILE: core/Clash.Meta/docker/file-name.sh ================================================ #!/bin/sh os="mihomo-linux-" case $TARGETPLATFORM in "linux/amd64") arch="amd64-v1" ;; "linux/386") arch="386" ;; "linux/arm64") arch="arm64" ;; "linux/arm/v7") arch="armv7" ;; "linux/riscv64") arch="riscv64" ;; *) echo "Unknown architecture" exit 1 ;; esac file_name="$os$arch-$(cat bin/version.txt)" echo $file_name ================================================ FILE: core/Clash.Meta/docs/config.yaml ================================================ # port: 7890 # HTTP(S) 代理服务器端口 # socks-port: 7891 # SOCKS5 代理端口 mixed-port: 10801 # HTTP(S) 和 SOCKS 代理混合端口 # redir-port: 7892 # 透明代理端口,用于 Linux 和 MacOS # Transparent proxy server port for Linux (TProxy TCP and TProxy UDP) # tproxy-port: 7893 allow-lan: true # 允许局域网连接 bind-address: "*" # 绑定 IP 地址,仅作用于 allow-lan 为 true,'*'表示所有地址 authentication: # http,socks 入口的验证用户名,密码 - "username:password" skip-auth-prefixes: # 设置跳过验证的 IP 段 - 127.0.0.1/8 - ::1/128 lan-allowed-ips: # 允许连接的 IP 地址段,仅作用于 allow-lan 为 true, 默认值为 0.0.0.0/0 和::/0 - 0.0.0.0/0 - ::/0 lan-disallowed-ips: # 禁止连接的 IP 地址段,黑名单优先级高于白名单,默认值为空 - 192.168.0.3/32 # find-process-mode has 3 values:always, strict, off # - always, 开启,强制匹配所有进程 # - strict, 默认,由 mihomo 判断是否开启 # - off, 不匹配进程,推荐在路由器上使用此模式 find-process-mode: strict mode: rule #自定义 geodata url geox-url: geoip: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat" geosite: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat" mmdb: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb" geo-auto-update: false # 是否自动更新 geodata geo-update-interval: 24 # 更新间隔,单位:小时 # Matcher implementation used by GeoSite, available implementations: # - succinct (default, same as rule-set) # - mph (from V2Ray, also `hybrid` in Xray) # geosite-matcher: succinct log-level: debug # 日志等级 silent/error/warning/info/debug ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 tls: certificate: string # 证书 PEM 格式,或者 证书的路径 private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- custom-certifactes: - | -----BEGIN CERTIFICATE----- format/pem... -----END CERTIFICATE----- external-controller: 0.0.0.0:9093 # RESTful API 监听地址 external-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址,需要配置 tls 部分配置文件 # secret: "123456" # `Authorization:Bearer ${secret}` # RESTful API CORS标头配置 external-controller-cors: allow-origins: - "*" allow-private-network: true # RESTful API Unix socket 监听地址( windows版本大于17063也可以使用,即大于等于1803/RS4版本即可使用 ) # !!!注意: 从Unix socket访问api接口不会验证secret, 如果开启请自行保证安全问题 !!! # 测试方法: curl -v --unix-socket "mihomo.sock" http://localhost/ external-controller-unix: mihomo.sock # RESTful API Windows namedpipe 监听地址 # !!!注意: 从Windows namedpipe访问api接口不会验证secret, 如果开启请自行保证安全问题 !!! external-controller-pipe: \\.\pipe\mihomo # tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP # 配置 WEB UI 目录,使用 http://{{external-controller}}/ui 访问 external-ui: /path/to/ui/folder/ external-ui-name: xd # 目前支持下载zip,tgz格式的压缩包 external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip" # 在RESTful API端口上开启DOH服务器 # !!!该URL不会验证secret, 如果开启请自行保证安全问题 !!! external-doh-server: /dns-query # interface-name: en0 # 设置出口网卡 # TCP keep alive interval # disable-keep-alive: false #目前在android端强制为true # keep-alive-idle: 15 # keep-alive-interval: 15 # routing-mark:6666 # 配置 fwmark 仅用于 Linux experimental: # Disable quic-go GSO support. This may result in reduced performance on Linux. # This is not recommended for most users. # Only users encountering issues with quic-go's internal implementation should enable this, # and they should disable it as soon as the issue is resolved. # This field will be removed when quic-go fixes all their issues in GSO. # This equivalent to the environment variable QUIC_GO_DISABLE_GSO=1. #quic-go-disable-gso: true # 类似于 /etc/hosts, 仅支持配置单个 IP hosts: # '*.mihomo.dev': 127.0.0.1 # '.dev': 127.0.0.1 # 'alpha.mihomo.dev': '::1' # test.com: [1.1.1.1, 2.2.2.2] # home.lan: lan # lan 为特别字段,将加入本地所有网卡的地址 # baidu.com: google.com # 只允许配置一个别名 profile: # 存储 select 选择记录 store-selected: false # 持久化 fake-ip store-fake-ip: true # Tun 配置 tun: enable: false stack: system # gvisor/mixed dns-hijack: - 0.0.0.0:53 # 需要劫持的 DNS # auto-detect-interface: true # 自动识别出口网卡 # auto-route: true # 配置路由表 # mtu: 9000 # 最大传输单元 # gso: false # 启用通用分段卸载,仅支持 Linux # gso-max-size: 65536 # 通用分段卸载包的最大大小 auto-redirect: false # 自动配置 iptables 以重定向 TCP 连接。仅支持 Linux。带有 auto-redirect 的 auto-route 现在可以在路由器上按预期工作,无需干预。 # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 # disable-icmp-forwarding: true # 禁用 ICMP 转发,防止某些情况下的 ICMP 环回问题,ping 将不会显示真实的延迟 route-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 不匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 - ruleset-1 - ruleset-2 route-exclude-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 - ruleset-3 - ruleset-4 route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 - 0.0.0.0/1 - 128.0.0.0/1 - "::/1" - "8000::/1" # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由(旧写法) # - 0.0.0.0/1 # - 128.0.0.0/1 # inet6-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由(旧写法) # - "::/1" # - "8000::/1" # endpoint-independent-nat: false # 启用独立于端点的 NAT # include-interface: # 限制被路由的接口。默认不限制,与 `exclude-interface` 冲突 # - "lan0" # exclude-interface: # 排除路由的接口,与 `include-interface` 冲突 # - "lan1" # include-uid: # UID 规则仅在 Linux 下被支持,并且需要 auto-route # - 0 # include-uid-range: # 限制被路由的的用户范围 # - 1000:9999 # exclude-uid: # 排除路由的的用户 #- 1000 # exclude-uid-range: # 排除路由的的用户范围 # - 1000:9999 # include-mac-address: # - 00:11:22:33:44:55 # exclude-mac-address: # - 00:11:22:33:44:55 # Android 用户和应用规则仅在 Android 下被支持 # 并且需要 auto-route # include-android-user: # 限制被路由的 Android 用户 # - 0 # - 10 # include-package: # 限制被路由的 Android 应用包名 # - com.android.chrome # exclude-package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin # 嗅探域名 可选配置 sniffer: enable: false ## 对 redir-host 类型识别的流量进行强制嗅探 ## 如:Tun、Redir 和 TProxy 并 DNS 为 redir-host 皆属于 # force-dns-mapping: false ## 对所有未获取到域名的流量进行强制嗅探 # parse-pure-ip: false # 是否使用嗅探结果作为实际访问,默认 true # 全局配置,优先级低于 sniffer.sniff 实际配置 override-destination: false sniff: # TLS 和 QUIC 默认如果不配置 ports 默认嗅探 443 QUIC: # ports: [ 443 ] TLS: # ports: [443, 8443] # 默认嗅探 80 HTTP: # 需要嗅探的端口 ports: [80, 8080-8880] # 可覆盖 sniffer.override-destination override-destination: true force-domain: - +.v2ex.com # skip-src-address: # 对于来源ip跳过嗅探 # - 192.168.0.3/32 # skip-dst-address: # 对于目标ip跳过嗅探 # - 192.168.0.3/32 ## 对嗅探结果进行跳过 # skip-domain: # - Mijia Cloud # 需要嗅探协议 # 已废弃,若 sniffer.sniff 配置则此项无效 sniffing: - tls - http # 强制对此域名进行嗅探 # 仅对白名单中的端口进行嗅探,默认为 443,80 # 已废弃,若 sniffer.sniff 配置则此项无效 port-whitelist: - "80" - "443" # - 8000-9999 tunnels: # one line config - tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy - tcp,127.0.0.1:6666,rds.mysql.com:3306,vpn # full yaml config - network: [tcp, udp] address: 127.0.0.1:7777 target: target.com proxy: proxy # DNS 配置 dns: cache-algorithm: arc enable: false # 关闭将使用系统 DNS prefer-h3: false # 是否开启 DoH 支持 HTTP/3,将并发尝试 listen: 0.0.0.0:53 # 开启 DNS 服务器监听 # ipv6: false # false 将返回 AAAA 的空结果 # ipv6-timeout: 300 # 单位:ms,内部双栈并发时,向上游查询 AAAA 时,等待 AAAA 的时间,默认 100ms # 用于解析 nameserver,fallback 以及其他 DNS 服务器配置的,DNS 服务域名 # 只能使用纯 IP 地址,可使用加密 DNS default-nameserver: - 114.114.114.114 - 8.8.8.8 - tls://1.12.12.12:853 - tls://223.5.5.5:853 - system # append DNS server from system configuration. If not found, it would print an error log and skip. enhanced-mode: fake-ip # or redir-host fake-ip-range: 198.18.0.1/16 # fake-ip 池设置 # fake-ip-range6: fdfe:dcba:9876::1/64 # fake-ip6 池设置 # 配置不使用 fake-ip 的域名 fake-ip-filter: - '*.lan' - localhost.ptlogin2.qq.com # fakeip-filter 为 rule-providers 中的名为 fakeip-filter 规则订阅, # 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 - rule-set:fakeip-filter # fakeip-filter 为 geosite 中名为 fakeip-filter 的分类(需要自行保证该分类存在) - geosite:fakeip-filter # 当 fake-ip-filter-mode: rule 时开启规则模式 # fake-ip 与路由 rules 匹配逻辑一致(自上而下),语法也一致,支持GEOSITE、RuleSet、DOMAIN*、MATCH - RULE-SET,reject-domain,fake-ip # 自定义 RuleSet behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 - RULE-SET,proxy-domain,fake-ip - GEOSITE,gfw,fake-ip - DOMAIN,www.baidu.com,real-ip - DOMAIN-SUFFIX,qq.com,real-ip - DOMAIN-SUFFIX,jd.com,fake-ip - MATCH,fake-ip # 最后 fake-ip or real-ip # 配置fake-ip-filter的匹配模式,默认为blacklist,即如果匹配成功不返回fake-ip # 可设置为whitelist,即只有匹配成功才返回fake-ip # 也可配置为rule,规则模式语法见fake-ip-filter说明 fake-ip-filter-mode: blacklist # 配置fakeip查询返回的TTL,非必要情况下请勿修改 fake-ip-ttl: 1 # use-hosts: true # 查询 hosts # 配置后面的nameserver、fallback和nameserver-policy向dns服务器的连接过程是否遵守遵守rules规则 # 如果为false(默认值)则这三部分的dns服务器在未特别指定的情况下会直连 # 如果为true,将会按照rules的规则匹配链接方式(走代理或直连),如果有特别指定则任然以指定值为准 # 仅当proxy-server-nameserver非空时可以开启此选项, 强烈不建议和prefer-h3一起使用 # 此外,这三者配置中的dns服务器如果出现域名会采用default-nameserver配置项解析,也请确保正确配置default-nameserver respect-rules: false # DNS 主要域名配置 # 支持 UDP,TCP,DoT,DoH,DoQ # 这部分为主要 DNS 配置,影响所有直连,确保使用对大陆解析精准的 DNS nameserver: - 114.114.114.114 # default value - 8.8.8.8 # default value - tls://223.5.5.5:853 # DNS over TLS - https://doh.pub/dns-query # DNS over HTTPS - https://dns.alidns.com/dns-query#h3=true # 强制 HTTP/3,与 perfer-h3 无关,强制开启 DoH 的 HTTP/3 支持,若不支持将无法使用 - https://mozilla.cloudflare-dns.com/dns-query#DNS&h3=true # 指定策略组和使用 HTTP/3 - dhcp://en0 # dns from dhcp - quic://dns.adguard.com:784 # DNS over QUIC # - '8.8.8.8#RULES' # 效果同respect-rules,但仅对该服务器生效 # - '8.8.8.8#en0' # 兼容指定 DNS 出口网卡 # 当配置 fallback 时,会查询 nameserver 中返回的 IP 是否为 CN,非必要配置 # 当不是 CN,则使用 fallback 中的 DNS 查询结果 # 确保配置 fallback 时能够正常查询 # fallback: # - tcp://1.1.1.1 # - 'tcp://1.1.1.1#ProxyGroupName' # 指定 DNS 过代理查询,ProxyGroupName 为策略组名或节点名,过代理配置优先于配置出口网卡,当找不到策略组或节点名则设置为出口网卡 # 专用于节点域名解析的 DNS 服务器,非必要配置项,如果不填则遵循nameserver-policy、nameserver和fallback的配置 # proxy-server-nameserver: # - https://doh.pub/dns-query # - tls://223.5.5.5:853 # proxy-server-nameserver-policy: # 格式同nameserver-policy,仅用于节点域名解析,当且仅当proxy-server-nameserver不为空时生效 # 'www.yournode.com': '114.114.114.114' # 专用于direct出口域名解析的 DNS 服务器,非必要配置项,如果不填则遵循nameserver-policy、nameserver和fallback的配置 # direct-nameserver: # - system:// # direct-nameserver-follow-policy: false # 是否遵循nameserver-policy,默认为不遵守,仅当direct-nameserver不为空时生效 # 配置 fallback 使用条件 # fallback-filter: # geoip: true # 配置是否使用 geoip # geoip-code: CN # 当 nameserver 域名的 IP 查询 geoip 库为 CN 时,不使用 fallback 中的 DNS 查询结果 # 配置强制 fallback,优先于 IP 判断,具体分类自行查看 geosite 库 # geosite: # - gfw # 如果不匹配 ipcidr 则使用 nameservers 中的结果 # ipcidr: # - 240.0.0.0/4 # domain: # - '+.google.com' # - '+.facebook.com' # - '+.youtube.com' # 配置查询域名使用的 DNS 服务器 nameserver-policy: # 'www.baidu.com': '114.114.114.114' # '+.internal.crop.com': '10.0.0.1' "geosite:cn,private,apple": - https://doh.pub/dns-query - https://dns.alidns.com/dns-query "geosite:category-ads-all": rcode://success "www.baidu.com,+.google.cn": [223.5.5.5, https://dns.alidns.com/dns-query] ## global,dns 为 rule-providers 中的名为 global 和 dns 规则订阅, ## 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 # "rule-set:global,dns": 8.8.8.8 proxies: # socks5 - name: "socks" type: socks5 server: server port: 443 # username: username # password: password # tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # skip-cert-verify: true # udp: true # ip-version: ipv6 # http - name: "http" type: http server: server port: 443 # username: username # password: password # tls: true # https # skip-cert-verify: true # sni: custom.com # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # ip-version: dual # Snell # Beware that there's currently no UDP support yet - name: "snell" type: snell server: server port: 44046 psk: yourpsk # version: 2 # obfs-opts: # mode: http # or tls # host: bing.com # Shadowsocks # cipher支持: # aes-128-gcm aes-192-gcm aes-256-gcm # aes-128-cfb aes-192-cfb aes-256-cfb # aes-128-ctr aes-192-ctr aes-256-ctr # rc4-md5 chacha20-ietf xchacha20 # chacha20-ietf-poly1305 xchacha20-ietf-poly1305 # 2022-blake3-aes-128-gcm 2022-blake3-aes-256-gcm 2022-blake3-chacha20-poly1305 - name: "ss1" type: ss server: server port: 443 cipher: chacha20-ietf-poly1305 password: "password" # udp: true # udp-over-tcp: false # ip-version: ipv4 # 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer。默认使用 dual # ipv4:仅使用 IPv4 ipv6:仅使用 IPv6 # ipv4-prefer:优先使用 IPv4 对于 TCP 会进行双栈解析,并发链接但是优先使用 IPv4 链接, # UDP 则为双栈解析,获取结果中的第一个 IPv4 # ipv6-prefer 同 ipv4-prefer # 现有协议都支持此参数,TCP 效果仅在开启 tcp-concurrent 生效 smux: enabled: false protocol: smux # smux/yamux/h2mux # max-connections: 4 # Maximum connections. Conflict with max-streams. # min-streams: 4 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. # padding: false # Enable padding. Requires sing-box server version 1.3-beta9 or later. # statistic: false # 控制是否将底层连接显示在面板中,方便打断底层连接 # only-tcp: false # 如果设置为 true, smux 的设置将不会对 udp 生效,udp 连接会直接走底层协议 - name: "ss2" type: ss server: server port: 443 cipher: chacha20-ietf-poly1305 password: "password" plugin: obfs plugin-opts: mode: tls # or http # host: bing.com - name: "ss3" type: ss server: server port: 443 cipher: chacha20-ietf-poly1305 password: "password" plugin: v2ray-plugin plugin-opts: mode: websocket # no QUIC now # tls: true # wss # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名 # skip-cert-verify: true # host: bing.com # path: "/" # mux: true # headers: # custom: value # v2ray-http-upgrade: false # v2ray-http-upgrade-fast-open: false - name: "ss4-shadow-tls" type: ss server: server port: 443 cipher: chacha20-ietf-poly1305 password: "password" plugin: shadow-tls client-fingerprint: chrome plugin-opts: host: "cloud.tencent.com" password: "shadow_tls_password" version: 2 # support 1/2/3 # alpn: ["h2","http/1.1"] - name: "ss5" type: ss server: server port: 443 cipher: chacha20-ietf-poly1305 password: "password" plugin: gost-plugin plugin-opts: mode: websocket # tls: true # wss # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # skip-cert-verify: true # host: bing.com # path: "/" # mux: true # headers: # custom: value - name: "ss-restls-tls13" type: ss server: [YOUR_SERVER_IP] port: 443 cipher: chacha20-ietf-poly1305 password: [YOUR_SS_PASSWORD] client-fingerprint: chrome # One of: chrome, ios, firefox or safari # 可以是 chrome, ios, firefox, safari 中的一个 plugin: restls plugin-opts: host: "www.microsoft.com" # Must be a TLS 1.3 server # 应当是一个 TLS 1.3 服务器 password: [YOUR_RESTLS_PASSWORD] version-hint: "tls13" # Control your post-handshake traffic through restls-script # Hide proxy behaviors like "tls in tls". # see https://github.com/3andne/restls/blob/main/Restls-Script:%20Hide%20Your%20Proxy%20Traffic%20Behavior.md # 用 restls 剧本来控制握手后的行为,隐藏"tls in tls"等特征 # 详情:https://github.com/3andne/restls/blob/main/Restls-Script:%20%E9%9A%90%E8%97%8F%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%90%86%E8%A1%8C%E4%B8%BA.md restls-script: "300?100<1,400~100,350~100,600~100,300~200,300~100" - name: "ss-restls-tls12" type: ss server: [YOUR_SERVER_IP] port: 443 cipher: chacha20-ietf-poly1305 password: [YOUR_SS_PASSWORD] client-fingerprint: chrome # One of: chrome, ios, firefox or safari # 可以是 chrome, ios, firefox, safari 中的一个 plugin: restls plugin-opts: host: "vscode.dev" # Must be a TLS 1.2 server # 应当是一个 TLS 1.2 服务器 password: [YOUR_RESTLS_PASSWORD] version-hint: "tls12" restls-script: "1000?100<1,500~100,350~100,600~100,400~200" - name: "ss-kcptun" type: ss server: [YOUR_SERVER_IP] port: 443 cipher: chacha20-ietf-poly1305 password: [YOUR_SS_PASSWORD] plugin: kcptun plugin-opts: key: it's a secrect # pre-shared secret between client and server crypt: aes # aes, aes-128, aes-128-gcm, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none, null mode: fast # profiles: fast3, fast2, fast, normal, manual conn: 1 # set num of UDP connections to server autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable scavengettl: 600 # set how long an expired connection can live (in seconds) mtu: 1350 # set maximum transmission unit for UDP packets ratelimit: 0 # set maximum outgoing speed (in bytes per second) for a single KCP connection, 0 to disable. Also known as packet pacing sndwnd: 128 # set send window size(num of packets) rcvwnd: 512 # set receive window size(num of packets) datashard: 10 # set reed-solomon erasure coding - datashard parityshard: 3 # set reed-solomon erasure coding - parityshard dscp: 0 # set DSCP(6bit) nocomp: false # disable compression acknodelay: false # flush ack immediately when a packet is received nodelay: 0 interval: 50 resend: 0 sockbuf: 4194304 # per-socket buffer in bytes smuxver: 1 # specify smux version, available 1,2 smuxbuf: 4194304 # the overall de-mux buffer in bytes framesize: 8192 # smux max frame size streambuf: 2097152 # per stream receive buffer in bytes, smux v2+ keepalive: 10 # seconds between heartbeats # vmess # cipher 支持 auto/aes-128-gcm/chacha20-poly1305/none - name: "vmess" type: vmess server: server port: 443 uuid: uuid alterId: 32 cipher: auto # udp: true # tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # client-fingerprint: chrome # Available: "chrome","firefox","safari","ios","random", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan. # skip-cert-verify: true # servername: example.com # priority over wss host # network: ws # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名 # ws-opts: # path: /path # headers: # Host: v2ray.com # max-early-data: 2048 # early-data-header-name: Sec-WebSocket-Protocol # v2ray-http-upgrade: false # v2ray-http-upgrade-fast-open: false - name: "vmess-h2" type: vmess server: server port: 443 uuid: uuid alterId: 32 cipher: auto network: h2 tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 h2-opts: host: - http.example.com - http-alt.example.com path: / - name: "vmess-http" type: vmess server: server port: 443 uuid: uuid alterId: 32 cipher: auto # udp: true # network: http # http-opts: # method: "GET" # path: # - '/' # - '/video' # headers: # Connection: # - keep-alive # ip-version: ipv4 # 设置使用 IP 类型偏好,可选:ipv4,ipv6,dual,默认值:dual - name: vmess-grpc server: server port: 443 type: vmess uuid: uuid alterId: 32 cipher: auto network: grpc tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 servername: example.com # skip-cert-verify: true grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 # max-connections: 1 # Maximum connections. Conflict with max-streams. # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. # ip-version: ipv4 # vless - name: "vless-tcp" type: vless server: server port: 443 uuid: uuid network: tcp servername: example.com # AKA SNI # skip-cert-verify: true # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名 - name: "vless-vision" type: vless server: server port: 443 uuid: uuid network: tcp tls: true udp: true flow: xtls-rprx-vision client-fingerprint: chrome # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # skip-cert-verify: true - name: "vless-encryption" type: vless server: server port: 443 uuid: uuid network: tcp # ------------------------- # vless encryption客户端配置: # (native/xorpub 的 XTLS Vision 可以 Splice。只使用 1-RTT 模式 / 若服务端发的 ticket 中秒数不为零则 0-RTT 复用) # / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号 # # Padding 是可选的参数,仅作用于 1-RTT 以消除握手的长度特征,双端默认值均为 "100-111-1111.75-0-111.50-0-3333": # 在 1-RTT client/server hello 后以 100% 的概率粘上随机 111 到 1111 字节的 padding # 以 75% 的概率等待随机 0 到 111 毫秒("probability-from-to") # 再次以 50% 的概率发送随机 0 到 3333 字节的 padding(若为 0 则不 Write()) # 服务端、客户端可以设置不同的 padding 参数,按 len、gap 的顺序无限串联,第一个 padding 需概率 100%、至少 35 字节 # ------------------------- encryption: "mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(padding len).(padding gap).(X25519 Password).(ML-KEM-768 Client)..." tls: false #可以不开启tls udp: true - name: "vless-reality-vision" type: vless server: server port: 443 uuid: uuid network: tcp tls: true udp: true flow: xtls-rprx-vision servername: www.microsoft.com # REALITY servername reality-opts: public-key: xxx short-id: xxx # optional support-x25519mlkem768: false # 如果服务端支持可手动设置为true client-fingerprint: chrome # cannot be empty - name: "vless-reality-grpc" type: vless server: server port: 443 uuid: uuid network: grpc tls: true udp: true flow: # skip-cert-verify: true client-fingerprint: chrome servername: testingcf.jsdelivr.net grpc-opts: grpc-service-name: "grpc" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 # max-connections: 1 # Maximum connections. Conflict with max-streams. # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE short-id: 10f897e26c4b9478 support-x25519mlkem768: false # 如果服务端支持可手动设置为true - name: "vless-ws" type: vless server: server port: 443 uuid: uuid udp: true tls: true network: ws # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" servername: example.com # priority over wss host # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 ws-opts: path: "/" headers: Host: example.com # v2ray-http-upgrade: false # v2ray-http-upgrade-fast-open: false - name: "vless-xhttp" type: vless server: server port: 443 uuid: uuid udp: true tls: true network: xhttp alpn: [h2] # 默认仅支持h2,如果开启h3模式需要设置alpn: [h3],如果开启http1.1模式需要设置alpn: [http/1.1] # ech-opts: ... # reality-opts: ... # skip-cert-verify: false # fingerprint: ... # certificate: ... # private-key: ... servername: xxx.com client-fingerprint: chrome encryption: "" xhttp-opts: path: "/" host: xxx.com # mode: "stream-one" # Available: "stream-one", "stream-up" or "packet-up" # headers: # X-Forwarded-For: "" # no-grpc-header: false # x-padding-bytes: "100-1000" # x-padding-obfs-mode: false # x-padding-key: x_padding # x-padding-header: Referer # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query # x-padding-method: repeat-x # Available: repeat-x, tokenish # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE # session-placement: path # Available: path, query, cookie, header # session-key: "" # seq-placement: path # Available: path, query, cookie, header # seq-key: "" # uplink-data-placement: body # Available: body, cookie, header # uplink-data-key: "" # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-each-post-bytes: 1000000 # sc-min-posts-interval-ms: 30 # reuse-settings: # aka XMUX # max-concurrency: "16-32" # max-connections: "0" # c-max-reuse-times: "0" # h-max-request-times: "600-900" # h-max-reusable-secs: "1800-3000" # h-keep-alive-period: 0 # download-settings: # ## xhttp part # path: "/" # host: xxx.com # headers: # X-Forwarded-For: "" # reuse-settings: # aka XMUX # max-concurrency: "16-32" # max-connections: "0" # c-max-reuse-times: "0" # h-max-request-times: "600-900" # h-max-reusable-secs: "1800-3000" # h-keep-alive-period: 0 # ## proxy part # server: server # port: 443 # tls: true # alpn: ... # ech-opts: ... # reality-opts: ... # skip-cert-verify: false # fingerprint: ... # certificate: ... # private-key: ... # servername: xxx.com # client-fingerprint: chrome # Trojan - name: "trojan" type: trojan server: server port: 443 password: yourpsk # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # udp: true # sni: example.com # aka server name # alpn: # - h2 # - http/1.1 # skip-cert-verify: true # ss-opts: # like trojan-go's `shadowsocks` config # enabled: false # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 # password: "example" # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名 - name: trojan-grpc server: server port: 443 type: trojan password: "example" network: grpc sni: example.com # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 udp: true grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 # max-connections: 1 # Maximum connections. Conflict with max-streams. # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. - name: trojan-ws server: server port: 443 type: trojan password: "example" network: ws sni: example.com # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 udp: true # ws-opts: # path: /path # headers: # Host: example.com # v2ray-http-upgrade: false # v2ray-http-upgrade-fast-open: false - name: "trojan-xtls" type: trojan server: server port: 443 password: yourpsk flow: "xtls-rprx-direct" # xtls-rprx-origin xtls-rprx-direct flow-show: true # udp: true # sni: example.com # aka server name # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 #hysteria - name: "hysteria" type: hysteria server: server.com port: 443 # ports: 1000,2000-3000,5000 # port 不可省略 auth-str: yourpassword # obfs: obfs_str # alpn: # - h3 protocol: udp # 支持 udp/wechat-video/faketcp up: "30 Mbps" # 若不写单位,默认为 Mbps down: "200 Mbps" # 若不写单位,默认为 Mbps # sni: server.com # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名 # skip-cert-verify: false # recv-window-conn: 12582912 # recv-window: 52428800 # disable-mtu-discovery: false # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # fast-open: true # 支持 TCP 快速打开,默认为 false #hysteria2 - name: "hysteria2" type: hysteria2 server: server.com port: 443 # ports: 1000,2000-3000,5000 # port 不可省略 # hop-interval: 15 # 支持填写"15-30"会每次随机选取其中一个值作为切换间隔,仅支持写一个范围(即不允许出现逗号) # up 和 down 均不写或为 0 则使用 BBR 流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" password: yourpassword # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander # obfs-password: yourpassword # sni: server.com # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名 # skip-cert-verify: false # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 下面两项如果填写则开启 mTLS(需要同时填写) # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # alpn: # - h3 ###quic-go特殊配置项,不要随意修改除非你知道你在干什么### # initial-stream-receive-window: 8388608 # max-stream-receive-window: 8388608 # initial-connection-receive-window: 20971520 # max-connection-receive-window: 20971520 # wireguard - name: "wg" type: wireguard server: 162.159.192.1 port: 2480 ip: 172.16.0.2 ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5 public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= # pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM= private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= udp: true reserved: "U4An" # 数组格式也是合法的 # reserved: [209,98,59] # persistent-keepalive: 0 # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" # remote-dns-resolve: true # 强制 dns 远程解析,默认值为 false # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效 # refresh-server-ip-interval: 60 # 重新解析server ip的间隔,单位为秒,默认值为0即仅第一次链接时解析server域名,仅应在server域名对应的IP会发生变化时启用该选项(如家宽ddns) # 如果 peers 不为空,该段落中的 allowed-ips 不可为空;前面段落的 server,port,public-key,pre-shared-key 均会被忽略,但 private-key 会被保留且只能在顶层指定 # peers: # - server: 162.159.192.1 # port: 2480 # public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= # # pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM= # allowed-ips: ['0.0.0.0/0'] # reserved: [209,98,59] # 如果存在则开启AmneziaWG功能 # amnezia-wg-option: # jc: 5 # jmin: 500 # jmax: 501 # s1: 30 # s2: 40 # s3: 50 # AmneziaWG v1.5 and v2 # s4: 5 # AmneziaWG v1.5 and v2 # h1: 123456 # AmneziaWG v1.0 and v1.5 # h2: 67543 # AmneziaWG v1.0 and v1.5 # h3: 123123 # AmneziaWG v1.0 and v1.5 # h4: 32345 # AmneziaWG v1.0 and v1.5 # h1: 123456-123500 # AmneziaWG v2.0 only # h2: 67543-67550 # AmneziaWG v2.0 only # h3: 123123-123200 # AmneziaWG v2.0 only # h4: 32345-32350 # AmneziaWG v2.0 only # i1: # AmneziaWG v1.5 and v2 # i2: # AmneziaWG v1.5 and v2 # i3: "" # AmneziaWG v1.5 and v2 # i4: "" # AmneziaWG v1.5 and v2 # i5: "" # AmneziaWG v1.5 and v2 # j1: # AmneziaWG v1.5 only (removed in v2) # j2: # AmneziaWG v1.5 only (removed in v2) # j3: # AmneziaWG v1.5 only (removed in v2) # itime: 60 # AmneziaWG v1.5 only (removed in v2) # masque - name: "masque" type: masque server: 162.159.198.1 port: 443 private-key: MHcCAQEEILI1eOtnbEIh89Fj4yNDuFR6UjayCKI3NdLl3DhetimWoAoGCCqGSM49AwEHoUQDQgAEgyXrE8v+hHsHy3ewSb3WcRjYgCrM9T9hiE0Uv6k2DZ1+4kefrDT9v1Q/8wdRigTf6t6gGNUV8W+IUMdrfUt+9g== public-key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIaU7MToJm9NKp8YfGxR6r+/h4mcG7SxI8tsW8OR1A5tv/zCzVbCRRh2t87/kxnP6lAy0lkr7qYwu+ox+k3dr6w== ip: 172.16.0.2 ipv6: 2606:4700:110:84c0:163a:4914:a0ad:3342 mtu: 1280 udp: true # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" # remote-dns-resolve: true # 强制 dns 远程解析,默认值为 false # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效 # congestion-controller: bbr # 默认不开启 # masque-h2 - name: "masque-h2" type: masque server: 162.159.198.2 port: 443 private-key: MHcCAQEEILI1eOtnbEIh89Fj4yNDuFR6UjayCKI3NdLl3DhetimWoAoGCCqGSM49AwEHoUQDQgAEgyXrE8v+hHsHy3ewSb3WcRjYgCrM9T9hiE0Uv6k2DZ1+4kefrDT9v1Q/8wdRigTf6t6gGNUV8W+IUMdrfUt+9g== public-key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIaU7MToJm9NKp8YfGxR6r+/h4mcG7SxI8tsW8OR1A5tv/zCzVbCRRh2t87/kxnP6lAy0lkr7qYwu+ox+k3dr6w== ip: 172.16.0.2 ipv6: 2606:4700:110:84c0:163a:4914:a0ad:3342 mtu: 1280 udp: true network: h2 # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" # remote-dns-resolve: true # 强制 dns 远程解析,默认值为 false # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效 # tuic - name: tuic server: www.example.com port: 10443 type: tuic # tuicV4 必须填写 token(不可同时填写 uuid 和 password) token: TOKEN # tuicV5 必须填写 uuid 和 password(不可同时填写 token) uuid: 00000000-0000-0000-0000-000000000001 password: PASSWORD_1 # ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server' # heartbeat-interval: 10000 # alpn: [h3] disable-sni: true reduce-rtt: true request-timeout: 8000 udp-relay-mode: native # Available: "native", "quic". Default: "native" # congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic" # cwnd: 10 # default: 32 # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" # max-udp-relay-packet-size: 1500 # fast-open: true # skip-cert-verify: true # max-open-streams: 20 # default 100, too many open streams may hurt performance # sni: example.com # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名 # # meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效 # 警告,与原版 tuic 不兼容!!! # udp-over-stream: false # udp-over-stream-version: 1 # ShadowsocksR # The supported ciphers (encryption methods): all stream ciphers in ss # The supported obfses: # plain http_simple http_post # random_head tls1.2_ticket_auth tls1.2_ticket_fastauth # The supported protocols: # origin auth_sha1_v4 auth_aes128_md5 # auth_aes128_sha1 auth_chain_a auth_chain_b - name: "ssr" type: ssr server: server port: 443 cipher: chacha20-ietf password: "password" obfs: tls1.2_ticket_auth protocol: auth_sha1_v4 # obfs-param: domain.tld # protocol-param: "#" # udp: true - name: "ssh-out" type: ssh server: 127.0.0.1 port: 22 username: root password: password privateKey: path # mieru - name: mieru type: mieru server: 1.2.3.4 port: 2999 # port-range: 2090-2099 #(不可同时填写 port 和 port-range) transport: TCP # 支持 TCP 或者 UDP udp: true # 支持 UDP over TCP username: user password: password # 可以使用的值包括 MULTIPLEXING_OFF, MULTIPLEXING_LOW, MULTIPLEXING_MIDDLE, MULTIPLEXING_HIGH。其中 MULTIPLEXING_OFF 会关闭多路复用功能。默认值为 MULTIPLEXING_LOW。 # multiplexing: MULTIPLEXING_LOW # 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT,否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD # handshake-mode: HANDSHAKE_STANDARD # 一个 base64 字符串用于微调网络行为 # traffic-pattern: "" # sudoku - name: sudoku type: sudoku server: server_ip/domain # 1.2.3.4 or domain port: 443 key: "" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;none 不提供 AEAD 保护) padding-min: 2 # 最小填充率(0-100) padding-max: 7 # 最大填充率(0-100,必须 >= padding-min) table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii # custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效 # custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),非空时覆盖 custom-table # 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段: httpmask: disable: false # true 禁用所有 HTTP 伪装/隧道 mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道) # tls: true # 可选:按需开启 HTTPS/WSS # host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 mode 为 stream/poll/auto/ws 时生效 # path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws # multiplex: "off" # 可选字符串:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 mode=stream/poll/auto 生效;ws 强制 off) enable-pure-downlink: false # 可选:false=带宽优化下行;true=纯 Sudoku 下行 # anytls - name: anytls type: anytls server: 1.2.3.4 port: 443 password: "" # client-fingerprint: chrome udp: true # idle-session-check-interval: 30 # seconds # idle-session-timeout: 30 # seconds # min-idle-session: 0 # sni: "example.com" # alpn: # - h2 # - http/1.1 # skip-cert-verify: true # trusttunnel - name: trusttunnel type: trusttunnel server: 1.2.3.4 port: 443 username: username password: password # client-fingerprint: chrome health-check: true udp: true # sni: "example.com" # alpn: # - h2 # skip-cert-verify: true ### quic options # quic: true # 默认为false # congestion-controller: bbr # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" ### reuse options # max-connections: 8 # Maximum connections. Conflict with max-streams. # min-streams: 5 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. # dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理 - name: "dns-out" type: dns # 配置指定 interface-name 和 fwmark 的 DIRECT - name: en1-direct type: direct interface-name: en1 routing-mark: 6667 proxy-groups: # 代理链,目前 relay 可以支持 udp 的只有 vmess/vless/trojan/ss/ssr/tuic # wireguard 目前不支持在 relay 中使用,请使用 proxy 中的 dialer-proxy 配置项 # Traffic: mihomo <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet - name: "relay" type: relay proxies: - http - vmess - ss1 - ss2 # url-test 将按照 url 测试结果使用延迟最低节点 - name: "auto" type: url-test proxies: - ss1 - ss2 - vmess1 # tolerance: 150 # lazy: true # expected-status: 204 # 当健康检查返回状态码与期望值不符时,认为节点不可用 url: "https://cp.cloudflare.com/generate_204" interval: 300 # fallback 将按照 url 测试结果按照节点顺序选择 - name: "fallback-auto" type: fallback proxies: - ss1 - ss2 - vmess1 url: "https://cp.cloudflare.com/generate_204" interval: 300 # load-balance 将按照算法随机选择节点 - name: "load-balance" type: load-balance proxies: - ss1 - ss2 - vmess1 url: "https://cp.cloudflare.com/generate_204" interval: 300 # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions # select 用户自行选择节点 - name: Proxy type: select # disable-udp: true proxies: - ss1 - ss2 - vmess1 - auto - name: UseProvider type: select filter: "HK|TW" # 正则表达式,过滤 provider1 中节点名包含 HK 或 TW use: - provider1 proxies: - Proxy - DIRECT # Mihomo 格式的节点或支持 *ray 的分享格式 proxy-providers: provider1: type: http # http 的 path 可空置,默认储存路径为 homedir 的 proxies 文件夹,文件名为 url 的 md5 url: "url" interval: 3600 path: ./provider1.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到其他位置,请通过设置 SAFE_PATHS 环境变量指定额外的安全路径。该环境变量的语法同本操作系统的PATH环境变量解析规则(即Windows下以分号分割,其他系统下以冒号分割) proxy: DIRECT # size-limit: 10240 # 限制下载文件最大为10kb,默认为0即不限制文件大小 header: User-Agent: - "Clash/v1.18.0" - "mihomo/1.18.3" # Accept: # - 'application/vnd.github.v3.raw' # Authorization: # - 'token 1231231' health-check: enable: true interval: 600 # lazy: true url: https://cp.cloudflare.com/generate_204 # expected-status: 204 # 当健康检查返回状态码与期望值不符时,认为节点不可用 override: # 覆写节点加载时的一些配置项 skip-cert-verify: true udp: true # down: "50 Mbps" # up: "10 Mbps" # dialer-proxy: proxy # interface-name: tailscale0 # routing-mark: 233 # ip-version: ipv4-prefer # additional-prefix: "[provider1]" # additional-suffix: "test" # # 名字替换,支持正则表达式 # proxy-name: # - pattern: "test" # target: "TEST" # - pattern: "IPLC-(.*?)倍" # target: "iplc x $1" provider2: type: inline dialer-proxy: proxy payload: - name: "ss1" type: ss server: server port: 443 cipher: chacha20-ietf-poly1305 password: "password" test: type: file path: /test.yaml health-check: enable: true interval: 36000 url: https://cp.cloudflare.com/generate_204 rule-providers: rule1: behavior: classical # domain ipcidr interval: 259200 path: /path/to/save/file.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到其他位置,请通过设置 SAFE_PATHS 环境变量指定额外的安全路径。该环境变量的语法同本操作系统的PATH环境变量解析规则(即Windows下以分号分割,其他系统下以冒号分割) type: http # http 的 path 可空置,默认储存路径为 homedir 的 rules 文件夹,文件名为 url 的 md5 url: "url" proxy: DIRECT # size-limit: 10240 # 限制下载文件最大为10kb,默认为0即不限制文件大小 rule2: behavior: classical interval: 259200 path: /path/to/save/file.yaml type: file rule3: # mrs类型ruleset,目前仅支持domain和ipcidr(即不支持classical), # # 对于behavior=domain: # - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式 # - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式 # - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式) # # 对于behavior=ipcidr: # - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式 # - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式 # - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式) # type: http url: "url" format: mrs behavior: domain path: /path/to/save/file.mrs rule4: type: inline behavior: domain # classical / ipcidr payload: - '.blogger.com' - '*.*.microsoft.com' - 'books.itunes.apple.com' rules: - RULE-SET,rule1,REJECT - IP-ASN,1,PROXY - DOMAIN-REGEX,^abc,DIRECT - DOMAIN-SUFFIX,baidu.com,DIRECT - DOMAIN-KEYWORD,google,ss1 - DOMAIN-WILDCARD,test.*.mihomo.com,ss1 - IP-CIDR,1.1.1.1/32,ss1 - IP-CIDR6,2409::/64,DIRECT # 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 的规则集 - SUB-RULE,(OR,((NETWORK,TCP),(NETWORK,UDP))),sub-rule-name1 - SUB-RULE,(AND,((NETWORK,UDP))),sub-rule-name2 # 定义多个子规则集,规则将以分叉匹配,使用 SUB-RULE 使用 # google.com(not match)--> baidu.com(match) # / | # / | # https://baidu.com --> rule1 --> rule2 --> sub-rule-name1(match tcp) 使用 DIRECT # # # google.com(not match)--> baidu.com(not match) # / | # / | # dns 1.1.1.1 --> rule1 --> rule2 --> sub-rule-name1(match udp) sub-rule-name2(match udp) # | # | # 使用 REJECT <-- 1.1.1.1/32(match) # sub-rules: sub-rule-name1: - DOMAIN,google.com,ss1 - DOMAIN,baidu.com,DIRECT sub-rule-name2: - IP-CIDR,1.1.1.1/32,REJECT - IP-CIDR,8.8.8.8/32,ss1 - DOMAIN,dns.alidns.com,REJECT # 流量入站 listeners: - name: socks5-in-1 type: socks port: 10808 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 #listen: 0.0.0.0 # 默认监听 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 # udp: false # 默认 true # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- - name: http-in-1 type: http port: 10809 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- - name: mixed-in-1 type: mixed # HTTP(S) 和 SOCKS 代理混合 port: 10810 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # udp: false # 默认 true # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- - name: redir-in-1 type: redir port: 10811 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - name: tproxy-in-1 type: tproxy port: 10812 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # udp: false # 默认 true - name: shadowsocks-in-1 type: shadowsocks port: 10813 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg= cipher: 2022-blake3-aes-256-gcm # shadow-tls: # enable: false # 设置为true时开启 # version: 3 # 支持v1/v2/v3 # password: password # v2设置项 # users: # v3设置项 # - name: 1 # password: password # handshake: # dest: test.com:443 # kcp-tun: # enable: false # key: it's a secrect # pre-shared secret between client and server # crypt: aes # aes, aes-128, aes-128-gcm, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none, null # mode: fast # profiles: fast3, fast2, fast, normal, manual # conn: 1 # set num of UDP connections to server # autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable # scavengettl: 600 # set how long an expired connection can live (in seconds) # ratelimit: 0 # set maximum outgoing speed (in bytes per second) for a single KCP connection, 0 to disable. Also known as packet pacing # mtu: 1350 # set maximum transmission unit for UDP packets # sndwnd: 128 # set send window size(num of packets) # rcvwnd: 512 # set receive window size(num of packets) # datashard: 10 # set reed-solomon erasure coding - datashard # parityshard: 3 # set reed-solomon erasure coding - parityshard # dscp: 0 # set DSCP(6bit) # nocomp: false # disable compression # acknodelay: false # flush ack immediately when a packet is received # nodelay: 0 # interval: 50 # resend: 0 # sockbuf: 4194304 # per-socket buffer in bytes # smuxver: 1 # specify smux version, available 1,2 # smuxbuf: 4194304 # the overall de-mux buffer in bytes # framesize: 8192 # smux max frame size # streambuf: 2097152 # per stream receive buffer in bytes, smux v2+ # keepalive: 10 # seconds between heartbeats - name: vmess-in-1 type: vmess port: 10814 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) users: - username: 1 uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 alterId: 1 # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) # reality-config: # dest: test.com:443 # private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 # short-id: # - 0123456789abcdef # server-names: # - test.com # #下列两个 limit 为选填,可对未通过验证的回落连接限速,bytesPerSec 默认为 0 即不启用 # #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化 # limit-fallback-upload: # after-bytes: 0 # 传输指定字节后开始限速 # bytes-per-sec: 0 # 基准速率(字节/秒) # burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效 # limit-fallback-download: # after-bytes: 0 # 传输指定字节后开始限速 # bytes-per-sec: 0 # 基准速率(字节/秒) # burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效 - name: tuic-in-1 type: tuic port: 10815 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # token: # tuicV4 填写(可以同时填写 users) # - TOKEN # users: # tuicV5 填写(可以同时填写 token) # 00000000-0000-0000-0000-000000000000: PASSWORD_0 # 00000000-0000-0000-0000-000000000001: PASSWORD_1 # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- # congestion-controller: bbr # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" # max-idle-time: 15000 # authentication-timeout: 1000 # alpn: # - h3 # max-udp-relay-packet-size: 1500 - name: tunnel-in-1 type: tunnel port: 10816 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) network: [tcp, udp] target: target.com - name: vless-in-1 type: vless port: 10817 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) users: - username: 1 uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 flow: xtls-rprx-vision # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # xhttp-config: # 如果不为空则开启 xhttp 传输层 # path: "/" # host: "" # mode: auto # Available: "stream-one", "stream-up" or "packet-up" # no-sse-header: false # x-padding-bytes: "100-1000" # x-padding-obfs-mode: false # x-padding-key: x_padding # x-padding-header: Referer # x-padding-placement: queryInHeader # Available: queryInHeader, cookie, header, query # x-padding-method: repeat-x # Available: repeat-x, tokenish # uplink-http-method: POST # Available: POST, PUT, PATCH, DELETE # session-placement: path # Available: path, query, cookie, header # session-key: "" # seq-placement: path # Available: path, query, cookie, header # seq-key: "" # uplink-data-placement: body # Available: body, cookie, header # uplink-data-key: "" # uplink-chunk-size: 0 # only applicable when uplink-data-placement is not body # sc-max-buffered-posts: 30 # sc-stream-up-server-secs: "20-80" # sc-max-each-post-bytes: 1000000 # ------------------------- # vless encryption服务端配置: # (原生外观 / 只 XOR 公钥 / 全随机数。1-RTT 每次下发随机 300 到 600 秒的 ticket 以便 0-RTT 复用 / 只允许 1-RTT) # 填写 "600s" 会每次随机取 50% 到 100%,即相当于填写 "300-600s" # / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号 # # Padding 是可选的参数,仅作用于 1-RTT 以消除握手的长度特征,双端默认值均为 "100-111-1111.75-0-111.50-0-3333": # 在 1-RTT client/server hello 后以 100% 的概率粘上随机 111 到 1111 字节的 padding # 以 75% 的概率等待随机 0 到 111 毫秒("probability-from-to") # 再次以 50% 的概率发送随机 0 到 3333 字节的 padding(若为 0 则不 Write()) # 服务端、客户端可以设置不同的 padding 参数,按 len、gap 的顺序无限串联,第一个 padding 需概率 100%、至少 35 字节 # ------------------------- # decryption: "mlkem768x25519plus.native/xorpub/random.600s(300-600s)/0s.(padding len).(padding gap).(X25519 PrivateKey).(ML-KEM-768 Seed)..." # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) reality-config: dest: test.com:443 private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 short-id: - 0123456789abcdef server-names: - test.com #下列两个 limit 为选填,可对未通过验证的回落连接限速,bytesPerSec 默认为 0 即不启用 #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化 limit-fallback-upload: after-bytes: 0 # 传输指定字节后开始限速 bytes-per-sec: 0 # 基准速率(字节/秒) burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效 limit-fallback-download: after-bytes: 0 # 传输指定字节后开始限速 bytes-per-sec: 0 # 基准速率(字节/秒) burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效 ### 注意,对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “decryption” 的其中一项 ### - name: anytls-in-1 type: anytls port: 10818 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 users: username1: password1 username2: password2 # "certificate" and "private-key" are required certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 private-key: ./server.key # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- # padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme - name: mieru-in-1 type: mieru port: 10818 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 transport: TCP # 支持 TCP 或者 UDP users: username1: password1 username2: password2 # 一个 base64 字符串用于微调网络行为 # traffic-pattern: "" # 如果开启,且客户端不发送用户提示,代理服务器将拒绝连接 # user-hint-is-mandatory: false - name: sudoku-in-1 type: sudoku port: 8443 # 仅支持单端口 listen: 0.0.0.0 key: "" # 如果你使用sudoku生成的ED25519密钥对,此处是密钥对中的公钥,当然,你也可以仅仅使用任意uuid充当key aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;none 不提供 AEAD 保护) padding-min: 1 # 最小填充率(0-100) padding-max: 15 # 最大填充率(0-100,必须 >= padding-min) table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii # custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效 # custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于多表轮换;非空时覆盖 custom-table handshake-timeout: 5 # 可选(秒) enable-pure-downlink: false # 可选:false=带宽优化下行;true=纯 Sudoku 下行 # 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段: httpmask: disable: false # true 禁用所有 HTTP 伪装/隧道 mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道) # path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws # # fallback: "127.0.0.1:80" # 可选:用于可连接请求的回落转发,可与其他服务共端口 - name: trojan-in-1 type: trojan port: 10819 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) users: - username: 1 password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) # reality-config: # dest: test.com:443 # private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 # short-id: # - 0123456789abcdef # server-names: # - test.com # #下列两个 limit 为选填,可对未通过验证的回落连接限速,bytesPerSec 默认为 0 即不启用 # #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化 # limit-fallback-upload: # after-bytes: 0 # 传输指定字节后开始限速 # bytes-per-sec: 0 # 基准速率(字节/秒) # burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效 # limit-fallback-download: # after-bytes: 0 # 传输指定字节后开始限速 # bytes-per-sec: 0 # 基准速率(字节/秒) # burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效 # ss-option: # like trojan-go's `shadowsocks` config # enabled: false # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 # password: "example" ### 注意,对于trojan listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “ss-option” 的其中一项 ### - name: hysteria2-in-1 type: hysteria2 port: 10820 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) users: 00000000-0000-0000-0000-000000000000: PASSWORD_0 00000000-0000-0000-0000-000000000001: PASSWORD_1 # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- ## up 和 down 均不写或为 0 则使用 BBR 流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander # obfs-password: yourpassword # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" # max-idle-time: 15000 # alpn: # - h3 # ignore-client-bandwidth: false # HTTP3 服务器认证失败时的行为 (URL 字符串配置),如果 masquerade 未配置,则返回 404 页 # masquerade: file:///var/www # 作为文件服务器 # masquerade: http://127.0.0.1:8080 #作为反向代理 # masquerade: https://127.0.0.1:8080 #作为反向代理 - name: trusttunnel-in-1 type: trusttunnel port: 10821 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) users: - username: 1 password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 network: ["tcp", "udp"] # http2+http3 congestion-controller: bbr # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz # dC5jb20AAA== # -----END ECH KEYS----- # 注意,listeners中的tun仅提供给高级用户使用,普通用户应使用顶层配置中的tun - name: tun-in-1 type: tun # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) stack: system # gvisor / mixed dns-hijack: - 0.0.0.0:53 # 需要劫持的 DNS # auto-detect-interface: false # 自动识别出口网卡 # auto-route: false # 配置路由表 # mtu: 9000 # 最大传输单元 inet4-address: # 必须手动设置 ipv4 地址段 - 198.19.0.1/30 inet6-address: # 必须手动设置 ipv6 地址段 - "fdfe:dcba:9877::1/126" # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 # - 0.0.0.0/1 # - 128.0.0.0/1 # inet6-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 # - "::/1" # - "8000::/1" # endpoint-independent-nat: false # 启用独立于端点的 NAT # include-uid: # UID 规则仅在 Linux 下被支持,并且需要 auto-route # - 0 # include-uid-range: # 限制被路由的的用户范围 # - 1000:99999 # exclude-uid: # 排除路由的的用户 # - 1000 # exclude-uid-range: # 排除路由的的用户范围 # - 1000:99999 # include-mac-address: # - 00:11:22:33:44:55 # exclude-mac-address: # - 00:11:22:33:44:55 # Android 用户和应用规则仅在 Android 下被支持 # 并且需要 auto-route # include-android-user: # 限制被路由的 Android 用户 # - 0 # - 10 # include-package: # 限制被路由的 Android 应用包名 # - com.android.chrome # exclude-package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin # disable-icmp-forwarding: true # 禁用 ICMP 转发,防止某些情况下的 ICMP 环回问题,ping 将不会显示真实的延迟 # 入口配置与 Listener 等价,传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理 # shadowsocks,vmess 入口配置(传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理) # ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456 # vmess-config: vmess://1:9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68@:12345 # tuic 服务器入口(传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理) # tuic-server: # enable: true # listen: 127.0.0.1:10443 # token: # tuicV4 填写(可以同时填写 users) # - TOKEN # users: # tuicV5 填写(可以同时填写 token) # 00000000-0000-0000-0000-000000000000: PASSWORD_0 # 00000000-0000-0000-0000-000000000001: PASSWORD_1 # certificate: ./server.crt # private-key: ./server.key # congestion-controller: bbr # bbr-profile: "" # Available: "standard", "conservative", "aggressive". Default: "standard" # max-idle-time: 15000 # authentication-timeout: 1000 # alpn: # - h3 # max-udp-relay-packet-size: 1500 ================================================ FILE: core/Clash.Meta/flake.nix ================================================ { description = "Another Mihomo Kernel"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/master"; inputs.utils.url = "github:numtide/flake-utils"; outputs = { self, nixpkgs, utils }: utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; }; in rec { packages.default = pkgs.mihomo-meta; } ) // ( let version = nixpkgs.lib.substring 0 8 self.lastModifiedDate or self.lastModified or "19700101"; in { overlay = final: prev: { mihomo-meta = final.buildGo119Module { pname = "mihomo-meta"; inherit version; src = ./.; vendorSha256 = "sha256-W5oiPtTRin0731QQWr98xZ2Vpk97HYcBtKoi1OKZz+w="; # Do not build testing suit excludedPackages = [ "./test" ]; CGO_ENABLED = 0; ldflags = [ "-s" "-w" "-X github.com/metacubex/mihomo/constant.Version=dev-${version}" "-X github.com/metacubex/mihomo/constant.BuildTime=${version}" ]; tags = [ "with_gvisor" ]; # Network required doCheck = false; postInstall = '' mv $out/bin/mihomo $out/bin/mihomo-meta ''; }; }; } ); } ================================================ FILE: core/Clash.Meta/go.mod ================================================ module github.com/metacubex/mihomo go 1.20 require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 github.com/dlclark/regexp2 v1.12.0 github.com/enfein/mieru/v3 v3.31.0 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/golang/snappy v1.0.0 github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d github.com/metacubex/bart v0.26.0 github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b github.com/metacubex/blake3 v0.1.0 github.com/metacubex/chacha v0.1.5 github.com/metacubex/chi v0.1.0 github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a github.com/metacubex/cpu v0.1.1 github.com/metacubex/edwards25519 v1.2.0 github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/http v0.1.6 github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 github.com/metacubex/mhurl v0.1.0 github.com/metacubex/mlkem v0.1.0 github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306 github.com/metacubex/randv2 v0.2.0 github.com/metacubex/restls-client-go v0.1.7 github.com/metacubex/sing v0.5.7 github.com/metacubex/sing-mux v0.3.9 github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a github.com/metacubex/sing-shadowsocks v0.2.12 github.com/metacubex/sing-shadowsocks2 v0.2.7 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 github.com/metacubex/sing-tun v0.4.18 github.com/metacubex/sing-vmess v0.2.5 github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947 github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 github.com/metacubex/ssh v0.1.0 github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 github.com/metacubex/tls v0.1.5 github.com/metacubex/utls v1.8.4 github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f github.com/mroth/weightedrand/v2 v2.1.0 github.com/openacid/low v0.1.21 github.com/samber/lo v1.53.0 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/yosida95/uritemplate/v3 v3.0.2 gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 go.uber.org/automaxprocs v1.6.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba gopkg.in/yaml.v3 v3.0.1 ) // lastest version compatible with golang1.20 require ( github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 github.com/klauspost/compress v1.17.9 github.com/mdlayher/netlink v1.7.2 github.com/miekg/dns v1.1.63 github.com/oschwald/maxminddb-golang v1.12.0 golang.org/x/crypto v0.33.0 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e golang.org/x/net v0.35.0 golang.org/x/sync v0.11.0 golang.org/x/sys v0.30.0 google.golang.org/protobuf v1.34.2 ) require ( github.com/RyuaNerin/go-krypto v1.3.0 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dunglas/httpsfv v1.0.2 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gaukas/godicttls v0.0.4 // 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/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect github.com/josharian/native v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/reedsolomon v1.12.3 // indirect github.com/kr/text v0.2.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/metacubex/ascon v0.1.0 // indirect github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect github.com/metacubex/hkdf v0.1.0 // indirect github.com/metacubex/hpke v0.1.0 // indirect github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb // indirect github.com/metacubex/qpack v0.6.0 // indirect github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.24.0 // indirect ) // for https://github.com/golang/protobuf/issues/1704 replace google.golang.org/protobuf => github.com/metacubex/protobuf-go v0.0.0-20260306035419-7ceee0674686 ================================================ FILE: core/Clash.Meta/go.sum ================================================ github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg= github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/enfein/mieru/v3 v3.31.0 h1:Fl2ocRCRXJzMygzdRjBHgqI996ZuIDHUmyQyovSf9sA= github.com/enfein/mieru/v3 v3.31.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= 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/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 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/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc= github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 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/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d h1:vAJ0ZT4aO803F1uw2roIA9yH7Sxzox34tVVyye1bz6c= github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY= github.com/metacubex/ascon v0.1.0 h1:6ZWxmXYszT1XXtwkf6nxfFhc/OTtQ9R3Vyj1jN32lGM= github.com/metacubex/ascon v0.1.0/go.mod h1:eV5oim4cVPPdEL8/EYaTZ0iIKARH9pnhAK/fcT5Kacc= github.com/metacubex/bart v0.26.0 h1:d/bBTvVatfVWGfQbiDpYKI1bXUJgjaabB2KpK1Tnk6w= github.com/metacubex/bart v0.26.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b h1:j7dadXD8I2KTmMt8jg1JcaP1ANL3JEObJPdANKcSYPY= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b/go.mod h1:+WmP0VJZDkDszvpa83HzfUp6QzARl/IKkMorH4+nODw= github.com/metacubex/blake3 v0.1.0 h1:KGnjh/56REO7U+cgZA8dnBhxdP7jByrG7hTP+bu6cqY= github.com/metacubex/blake3 v0.1.0/go.mod h1:CCkLdzFrqf7xmxCdhQFvJsRRV2mwOLDoSPg6vUTB9Uk= github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M= github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chi v0.1.0 h1:rjNDyDj50nRpicG43CNkIw4ssiCbmDL8d7wJXKlUCsg= github.com/metacubex/chi v0.1.0/go.mod h1:zM5u5oMQt8b2DjvDHvzadKrP6B2ztmasL1YHRMbVV+g= github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a h1:Ph5UfTWDsGruZ+v95Df1ycTflQFmpZBFg2LUvj2kx/M= github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ= github.com/metacubex/cpu v0.1.1 h1:rRV5HGmeuGzjiKI3hYbL0dCd0qGwM7VUtk4ICXD06mI= github.com/metacubex/cpu v0.1.1/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU= github.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4= github.com/metacubex/edwards25519 v1.2.0/go.mod h1:NCQF3J/Ki7382FJuokwsywEIIEI/gro/3smyXgQJsx0= github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU= github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ= github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA= github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4= github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ= github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U= github.com/metacubex/http v0.1.6 h1:xvXuvXMCMxCWMF5nEJF4yiKvXL+p2atWMzs37e80m1I= github.com/metacubex/http v0.1.6/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc= github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE= github.com/metacubex/mhurl v0.1.0/go.mod h1:2qpQImCbXoUs6GwJrjuEXKelPyoimsIXr07eNKZdS00= github.com/metacubex/mlkem v0.1.0 h1:wFClitonSFcmipzzQvax75beLQU+D7JuC+VK1RzSL8I= github.com/metacubex/mlkem v0.1.0/go.mod h1:amhaXZVeYNShuy9BILcR7P0gbeo/QLZsnqCdL8U2PDQ= github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb h1:wk6mHYPURSUvWcUv72gNP79oiylFsscBSDPJ6ieV6Iw= github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb/go.mod h1:73ZrCfhdkW4F2E2GAlta3km/S2RHhFNogCMtWZV2anQ= github.com/metacubex/protobuf-go v0.0.0-20260306035419-7ceee0674686 h1:PIXmYT2anQt9V8vdmwixtbIJxOpoPXJfIACHPjXEgnE= github.com/metacubex/protobuf-go v0.0.0-20260306035419-7ceee0674686/go.mod h1:eQV7juxFZIdRgjMxtVqP+6BssKoTZQ1RM0fc58BsCZY= github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw= github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA= github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306 h1:HlGLmLsWJMLSu0CMI9z/BmEnithB4oXM5Rom6/0Qxtg= github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM= github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing-mux v0.3.9 h1:/aoBD2+sK2qsXDlNDe3hkR0GZuFDtwIZhOeGUx9W0Yk= github.com/metacubex/sing-mux v0.3.9/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc= github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a h1:977o0ZYYbiQAGuOxql7Q6UN3rEy59OyAE0tELq4gZfI= github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A= github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= github.com/metacubex/sing-tun v0.4.18 h1:WRzAosG0YkT3aZq5RJWtF+RdCgeJ8EpooS5ZM1lkXo0= github.com/metacubex/sing-tun v0.4.18/go.mod h1:g4I/JNplDBhXLF+aQWgFbhNeJPSXQOWS9HvLeNvkgeA= github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE= github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q= github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947 h1:IB03BvRQtvjWScyOK5jSQVJYY8osmZXHL+4VCEFMWcM= github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80= github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk= github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg= github.com/metacubex/ssh v0.1.0 h1:iGfr99qk/eMHzUnQ/0bTxXT8+8SWqLSHBWDHoAhngzw= github.com/metacubex/ssh v0.1.0/go.mod h1:NUtl0d+/f2cG9ECEpMM8iCVOpmggQlC13oLeDUONDlU= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc= github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM= github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4= github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E= github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49/go.mod h1:MBeEa9IVBphH7vc3LNtW6ZujVXFizotPo3OEiHQ+TNU= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I= github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 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/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU= github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo= github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/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/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 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= ================================================ FILE: core/Clash.Meta/hub/executor/concurrent_load_limit.go ================================================ //go:build !386 && !amd64 && !arm64 && !arm64be && !mipsle && !mips package executor const concurrentCount = 5 ================================================ FILE: core/Clash.Meta/hub/executor/concurrent_load_single.go ================================================ //go:build mips || mipsle package executor const concurrentCount = 1 ================================================ FILE: core/Clash.Meta/hub/executor/concurrent_load_unlimit.go ================================================ //go:build 386 || amd64 || arm64 || arm64be package executor import "math" const concurrentCount = math.MaxInt ================================================ FILE: core/Clash.Meta/hub/executor/executor.go ================================================ package executor import ( "fmt" "net" "net/netip" "os" "runtime" "strconv" "sync" "time" _ "unsafe" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/component/auth" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/geodata" mihomoHttp "github.com/metacubex/mihomo/component/http" "github.com/metacubex/mihomo/component/iface" "github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/profile" "github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resource" "github.com/metacubex/mihomo/component/sniffer" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/component/trie" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/listener" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/inner" "github.com/metacubex/mihomo/listener/tproxy" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp/ntp" "github.com/metacubex/mihomo/tunnel" ) var mux sync.Mutex func readConfig(path string) ([]byte, error) { if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } data, err := os.ReadFile(path) if err != nil { return nil, err } if len(data) == 0 { return nil, fmt.Errorf("configuration file %s is empty", path) } return data, err } // Parse config with default config path func Parse() (*config.Config, error) { return ParseWithPath(C.Path.Config()) } // ParseWithPath parse config with custom config path func ParseWithPath(path string) (*config.Config, error) { buf, err := readConfig(path) if err != nil { return nil, err } return ParseWithBytes(buf) } // ParseWithBytes config with buffer func ParseWithBytes(buf []byte) (*config.Config, error) { return config.Parse(buf) } // ApplyConfig dispatch configure to all parts without ExternalController func ApplyConfig(cfg *config.Config, force bool) { mux.Lock() defer mux.Unlock() log.SetLevel(cfg.General.LogLevel) tunnel.OnSuspend() ca.ResetCertificate() for _, c := range cfg.TLS.CustomTrustCert { if err := ca.AddCertificate(c); err != nil { log.Warnln("%s\nadd error: %s", c, err.Error()) } } updateExperimental(cfg.Experimental) updateUsers(cfg.Users) updateProxies(cfg.Proxies, cfg.Providers) updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders) updateSniffer(cfg.Sniffer) updateHosts(cfg.Hosts) updateGeneral(cfg.General, true) updateNTP(cfg.NTP) updateDNS(cfg.DNS, cfg.General.IPv6) //updateListeners(cfg.General, cfg.Listeners, force) //updateTun(cfg.General) // tun should not care "force" updateIPTables(cfg) updateTunnels(cfg.Tunnels) tunnel.OnInnerLoading() initInnerTcp() loadProvider(cfg.Providers) updateProfile(cfg) loadProvider(cfg.RuleProviders) runtime.GC() tunnel.OnRunning() updateUpdater(cfg) resolver.ResetConnection() } func initInnerTcp() { inner.New(tunnel.Tunnel) } func GetGeneral() *config.General { ports := listener.GetPorts() var authenticator []string if auth := authStore.Default.Authenticator(); auth != nil { authenticator = auth.Users() } general := &config.General{ Inbound: config.Inbound{ Port: ports.Port, SocksPort: ports.SocksPort, RedirPort: ports.RedirPort, TProxyPort: ports.TProxyPort, MixedPort: ports.MixedPort, Tun: listener.GetTunConf(), TuicServer: listener.GetTuicConf(), ShadowSocksConfig: ports.ShadowSocksConfig, VmessConfig: ports.VmessConfig, Authentication: authenticator, SkipAuthPrefixes: inbound.SkipAuthPrefixes(), LanAllowedIPs: inbound.AllowedIPs(), LanDisAllowedIPs: inbound.DisAllowedIPs(), AllowLan: listener.AllowLan(), BindAddress: listener.BindAddress(), InboundTfo: inbound.Tfo(), InboundMPTCP: inbound.MPTCP(), }, Mode: tunnel.Mode(), UnifiedDelay: adapter.UnifiedDelay.Load(), LogLevel: log.Level(), IPv6: !resolver.DisableIPv6, Interface: dialer.DefaultInterface.Load(), RoutingMark: int(dialer.DefaultRoutingMark.Load()), GeoXUrl: config.GeoXUrl{ GeoIp: geodata.GeoIpUrl(), Mmdb: geodata.MmdbUrl(), ASN: geodata.ASNUrl(), GeoSite: geodata.GeoSiteUrl(), }, GeoAutoUpdate: updater.GeoAutoUpdate(), GeoUpdateInterval: updater.GeoUpdateInterval(), GeodataMode: geodata.GeodataMode(), GeodataLoader: geodata.LoaderName(), GeositeMatcher: geodata.SiteMatcherName(), TCPConcurrent: dialer.GetTcpConcurrent(), FindProcessMode: tunnel.FindProcessMode(), Sniffing: tunnel.IsSniffing(), GlobalClientFingerprint: tlsC.GetGlobalFingerprint(), GlobalUA: mihomoHttp.UA(), ETagSupport: resource.ETag(), KeepAliveInterval: int(keepalive.KeepAliveInterval() / time.Second), KeepAliveIdle: int(keepalive.KeepAliveIdle() / time.Second), DisableKeepAlive: keepalive.DisableKeepAlive(), } return general } func updateListeners(general *config.General, listeners map[string]C.InboundListener, force bool) { listener.PatchInboundListeners(listeners, tunnel.Tunnel, true) if !force { return } allowLan := general.AllowLan listener.SetAllowLan(allowLan) inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) inbound.SetAllowedIPs(general.LanAllowedIPs) inbound.SetDisAllowedIPs(general.LanDisAllowedIPs) bindAddress := general.BindAddress listener.SetBindAddress(bindAddress) listener.ReCreateHTTP(general.Port, tunnel.Tunnel) listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel) listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel) listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel) listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel) listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) } func updateTun(general *config.General) { listener.ReCreateTun(general.Tun, tunnel.Tunnel) } func updateExperimental(c *config.Experimental) { if c.QUICGoDisableGSO { _ = os.Setenv("QUIC_GO_DISABLE_GSO", strconv.FormatBool(true)) } if c.QUICGoDisableECN { _ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true)) } resolver.SetIP4PEnable(c.IP4PEnable) } func updateNTP(c *config.NTP) { if c.Enable { ntp.ReCreateNTPService( net.JoinHostPort(c.Server, strconv.Itoa(c.Port)), time.Duration(c.Interval), c.DialerProxy, c.WriteToSystem, ) } else { ntp.ReCreateNTPService("", 0, "", false) } } func updateDNS(c *config.DNS, generalIPv6 bool) { if !c.Enable { resolver.DefaultResolver = nil resolver.DefaultHostMapper = nil resolver.DefaultService = nil resolver.ProxyServerHostResolver = nil resolver.DirectHostResolver = nil dns.ReCreateServer("", nil) return } ipv6 := c.IPv6 && generalIPv6 r := dns.NewResolver(dns.Config{ Main: c.NameServer, Fallback: c.Fallback, IPv6: ipv6, IPv6Timeout: c.IPv6Timeout, FallbackIPFilter: c.FallbackIPFilter, FallbackDomainFilter: c.FallbackDomainFilter, Default: c.DefaultNameserver, Policy: c.NameServerPolicy, ProxyServer: c.ProxyServerNameserver, ProxyServerPolicy: c.ProxyServerPolicy, DirectServer: c.DirectNameServer, DirectFollowPolicy: c.DirectFollowPolicy, CacheAlgorithm: c.CacheAlgorithm, CacheMaxSize: c.CacheMaxSize, }) m := dns.NewEnhancer(dns.EnhancerConfig{ IPv6: ipv6, EnhancedMode: c.EnhancedMode, FakeIPPool: c.FakeIPPool, FakeIPPool6: c.FakeIPPool6, FakeIPSkipper: c.FakeIPSkipper, FakeIPTTL: c.FakeIPTTL, UseHosts: c.UseHosts, }) // reuse cache of old host mapper if old := resolver.DefaultHostMapper; old != nil { m.PatchFrom(old.(*dns.ResolverEnhancer)) } s := dns.NewService(r.Resolver, m) resolver.DefaultResolver = r resolver.DefaultHostMapper = m resolver.DefaultService = s resolver.UseSystemHosts = c.UseSystemHosts if r.ProxyResolver.Invalid() { resolver.ProxyServerHostResolver = r.ProxyResolver } else { resolver.ProxyServerHostResolver = r.Resolver } if r.DirectResolver.Invalid() { resolver.DirectHostResolver = r.DirectResolver } else { resolver.DirectHostResolver = r.Resolver } dns.ReCreateServer(c.Listen, s) } func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) { resolver.DefaultHosts = resolver.NewHosts(tree) } func updateProxies(proxies map[string]C.Proxy, providers map[string]P.ProxyProvider) { tunnel.UpdateProxies(proxies, providers) } func updateRules(rules []C.Rule, subRules map[string][]C.Rule, ruleProviders map[string]P.RuleProvider) { tunnel.UpdateRules(rules, subRules, ruleProviders) } func loadProvider[T P.Provider](providers map[string]T) { load := func(pv T) { name := pv.Name() if pv.VehicleType() == P.Compatible { log.Infoln("Start initial compatible provider %s", name) } else { log.Infoln("Start initial provider %s", name) } if err := pv.Initial(); err != nil { switch pv.Type() { case P.Proxy: { log.Warnln("initial proxy provider %s error: %v", name, err) } case P.Rule: { log.Warnln("initial rule provider %s error: %v", name, err) } } } else { if DefaultProviderLoadedHook != nil { DefaultProviderLoadedHook(name) } } } wg := sync.WaitGroup{} ch := make(chan struct{}, concurrentCount) for _, pv := range providers { pv := pv wg.Add(1) ch <- struct{}{} go func() { defer func() { <-ch; wg.Done() }() load(pv) }() } wg.Wait() } func updateSniffer(snifferConfig *sniffer.Config) { dispatcher, err := sniffer.NewDispatcher(snifferConfig) if err != nil { log.Warnln("initial sniffer failed, err:%v", err) } tunnel.UpdateSniffer(dispatcher) if snifferConfig.Enable { log.Infoln("Sniffer is loaded and working") } else { log.Infoln("Sniffer is closed") } } func updateTunnels(tunnels []LC.Tunnel) { listener.PatchTunnel(tunnels, tunnel.Tunnel) } func updateUpdater(cfg *config.Config) { general := cfg.General updater.SetGeoAutoUpdate(general.GeoAutoUpdate) updater.SetGeoUpdateInterval(general.GeoUpdateInterval) controller := cfg.Controller updater.DefaultUiUpdater = updater.NewUiUpdater(controller.ExternalUI, controller.ExternalUIURL, controller.ExternalUIName) updater.DefaultUiUpdater.AutoDownloadUI() } //go:linkname temporaryUpdateGeneral github.com/metacubex/mihomo/config.temporaryUpdateGeneral func temporaryUpdateGeneral(general *config.General) func() { oldGeneral := GetGeneral() updateGeneral(general, false) return func() { updateGeneral(oldGeneral, false) } } func updateGeneral(general *config.General, logging bool) { tunnel.SetMode(general.Mode) tunnel.SetFindProcessMode(general.FindProcessMode) resolver.DisableIPv6 = !general.IPv6 dialer.SetTcpConcurrent(general.TCPConcurrent) if logging && general.TCPConcurrent { log.Infoln("Use tcp concurrent") } inbound.SetTfo(general.InboundTfo) inbound.SetMPTCP(general.InboundMPTCP) keepalive.SetKeepAliveIdle(time.Duration(general.KeepAliveIdle) * time.Second) keepalive.SetKeepAliveInterval(time.Duration(general.KeepAliveInterval) * time.Second) keepalive.SetDisableKeepAlive(general.DisableKeepAlive) adapter.UnifiedDelay.Store(general.UnifiedDelay) dialer.DefaultInterface.Store(general.Interface) dialer.DefaultRoutingMark.Store(int32(general.RoutingMark)) if logging && general.RoutingMark > 0 { log.Infoln("Use routing mark: %#x", general.RoutingMark) } iface.FlushCache() geodata.SetGeodataMode(general.GeodataMode) geodata.SetLoader(general.GeodataLoader) geodata.SetSiteMatcher(general.GeositeMatcher) geodata.SetGeoIpUrl(general.GeoXUrl.GeoIp) geodata.SetGeoSiteUrl(general.GeoXUrl.GeoSite) geodata.SetMmdbUrl(general.GeoXUrl.Mmdb) geodata.SetASNUrl(general.GeoXUrl.ASN) mihomoHttp.SetUA(general.GlobalUA) resource.SetETag(general.ETagSupport) if general.GlobalClientFingerprint != "" { log.Warnln("The `global-client-fingerprint` configuration is deprecated, please set `client-fingerprint` directly on the proxy instead") } tlsC.SetGlobalFingerprint(general.GlobalClientFingerprint) } func updateUsers(users []auth.AuthUser) { authenticator := auth.NewAuthenticator(users) authStore.Default.SetAuthenticator(authenticator) if authenticator != nil { log.Infoln("Authentication of local server updated") } } func updateProfile(cfg *config.Config) { profileCfg := cfg.Profile profile.StoreSelected.Store(profileCfg.StoreSelected) if profileCfg.StoreSelected { patchSelectGroup(cfg.Proxies) } } func patchSelectGroup(proxies map[string]C.Proxy) { mapping := cachefile.Cache().SelectedMap() if mapping == nil { return } for name, outbound := range proxies { selector, ok := outbound.Adapter().(outboundgroup.SelectAble) if !ok { continue } selected, exist := mapping[name] if !exist { continue } if outbound.Type() == C.URLTest { cachefile.Cache().SetSelected(name, "") selector.ForceSet("") continue } selector.ForceSet(selected) } } func updateIPTables(cfg *config.Config) { tproxy.CleanupTProxyIPTables() iptables := cfg.IPTables if runtime.GOOS != "linux" || !iptables.Enable { return } var err error defer func() { if err != nil { log.Errorln("[IPTABLES] setting iptables failed: %s", err.Error()) os.Exit(2) } }() if cfg.General.Tun.Enable { err = fmt.Errorf("when tun is enabled, iptables cannot be set automatically") return } var ( inboundInterface = "lo" bypass = iptables.Bypass tProxyPort = cfg.General.TProxyPort dnsCfg = cfg.DNS DnsRedirect = iptables.DnsRedirect dnsPort netip.AddrPort ) if tProxyPort == 0 { err = fmt.Errorf("tproxy-port must be greater than zero") return } if DnsRedirect { if !dnsCfg.Enable { err = fmt.Errorf("DNS server must be enable") return } dnsPort, err = netip.ParseAddrPort(dnsCfg.Listen) if err != nil { err = fmt.Errorf("DNS server must be correct") return } } if iptables.InboundInterface != "" { inboundInterface = iptables.InboundInterface } dialer.DefaultRoutingMark.CompareAndSwap(0, 2158) err = tproxy.SetTProxyIPTables(inboundInterface, bypass, uint16(tProxyPort), DnsRedirect, dnsPort.Port()) if err != nil { return } log.Infoln("[IPTABLES] Setting iptables completed") } func Shutdown() { listener.Cleanup() tproxy.CleanupTProxyIPTables() resolver.StoreFakePoolState() log.Warnln("Mihomo shutting down") } ================================================ FILE: core/Clash.Meta/hub/executor/patch.go ================================================ package executor type ProviderLoadedHook func(providerName string) var DefaultProviderLoadedHook ProviderLoadedHook ================================================ FILE: core/Clash.Meta/hub/hub.go ================================================ package hub import ( "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/route" "github.com/metacubex/mihomo/log" ) type Option func(*config.Config) func WithExternalUI(externalUI string) Option { return func(cfg *config.Config) { cfg.Controller.ExternalUI = externalUI } } func WithExternalController(externalController string) Option { return func(cfg *config.Config) { cfg.Controller.ExternalController = externalController } } func WithExternalControllerUnix(externalControllerUnix string) Option { return func(cfg *config.Config) { cfg.Controller.ExternalControllerUnix = externalControllerUnix } } func WithExternalControllerPipe(externalControllerPipe string) Option { return func(cfg *config.Config) { cfg.Controller.ExternalControllerPipe = externalControllerPipe } } func WithSecret(secret string) Option { return func(cfg *config.Config) { cfg.Controller.Secret = secret } } // ApplyConfig dispatch configure to all parts include ExternalController func ApplyConfig(cfg *config.Config) { applyRoute(cfg) executor.ApplyConfig(cfg, true) } func applyRoute(cfg *config.Config) { if cfg.Controller.ExternalUI != "" { route.SetUIPath(cfg.Controller.ExternalUI) } route.ReCreateServer(&route.Config{ Addr: cfg.Controller.ExternalController, TLSAddr: cfg.Controller.ExternalControllerTLS, UnixAddr: cfg.Controller.ExternalControllerUnix, PipeAddr: cfg.Controller.ExternalControllerPipe, Secret: cfg.Controller.Secret, Certificate: cfg.TLS.Certificate, PrivateKey: cfg.TLS.PrivateKey, ClientAuthType: cfg.TLS.ClientAuthType, ClientAuthCert: cfg.TLS.ClientAuthCert, EchKey: cfg.TLS.EchKey, DohServer: cfg.Controller.ExternalDohServer, IsDebug: cfg.General.LogLevel == log.DEBUG, Cors: route.Cors{ AllowOrigins: cfg.Controller.Cors.AllowOrigins, AllowPrivateNetwork: cfg.Controller.Cors.AllowPrivateNetwork, }, }) } // Parse call at the beginning of mihomo func Parse(configBytes []byte, options ...Option) error { var cfg *config.Config var err error if len(configBytes) != 0 { cfg, err = executor.ParseWithBytes(configBytes) } else { cfg, err = executor.Parse() } if err != nil { return err } for _, option := range options { option(cfg) } ApplyConfig(cfg) return nil } ================================================ FILE: core/Clash.Meta/hub/route/cache.go ================================================ package route import ( "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func cacheRouter() http.Handler { r := chi.NewRouter() r.Post("/fakeip/flush", flushFakeIPPool) r.Post("/dns/flush", flushDnsCache) return r } func flushFakeIPPool(w http.ResponseWriter, r *http.Request) { err := resolver.FlushFakeIP() if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(err.Error())) return } render.NoContent(w, r) } func flushDnsCache(w http.ResponseWriter, r *http.Request) { resolver.ClearCache() render.NoContent(w, r) } ================================================ FILE: core/Clash.Meta/hub/route/common.go ================================================ package route import ( "bufio" "encoding/binary" "errors" "io" "net" "net/url" "strconv" "strings" "time" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/chi" "github.com/metacubex/http" ) // 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 } // wsUpgrade upgrades http connection to the websocket connection. // // It hijacks net.Conn from w and returns received net.Conn and // bufio.ReadWriter. func wsUpgrade(r *http.Request, w http.ResponseWriter) (conn net.Conn, rw *bufio.ReadWriter, err error) { // See https://tools.ietf.org/html/rfc6455#section-4.1 // The method of the request MUST be GET, and the HTTP version MUST be at least 1.1. var nonce string if r.Method != http.MethodGet { err = errors.New("handshake error: bad HTTP request method") body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte(body)) return nil, nil, err } else if r.ProtoMajor < 1 || (r.ProtoMajor == 1 && r.ProtoMinor < 1) { err = errors.New("handshake error: bad HTTP protocol version") body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(http.StatusHTTPVersionNotSupported) w.Write([]byte(body)) return nil, nil, err } else if r.Host == "" { err = errors.New("handshake error: bad Host header") body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(body)) return nil, nil, err } else if u := r.Header.Get("Upgrade"); u != "websocket" && !strings.EqualFold(u, "websocket") { err = errors.New("handshake error: bad Upgrade header") body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(body)) return nil, nil, err } else if c := r.Header.Get("Connection"); c != "Upgrade" && !strings.Contains(strings.ToLower(c), "upgrade") { err = errors.New("handshake error: bad Connection header") body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(body)) return nil, nil, err } else if nonce = r.Header.Get("Sec-WebSocket-Key"); len(nonce) != 24 { err = errors.New("handshake error: bad Sec-WebSocket-Key header") body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(body)) return nil, nil, err } else if v := r.Header.Get("Sec-WebSocket-Version"); v != "13" { err = errors.New("handshake error: bad Sec-WebSocket-Version header") body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) if v != "" { // According to RFC6455: // If this version does not match a version understood by the server, the // server MUST abort the WebSocket handshake described in this section and // instead send an appropriate HTTP error code (such as 426 Upgrade Required) // and a |Sec-WebSocket-Version| header field indicating the version(s) the // server is capable of understanding. w.Header().Set("Sec-WebSocket-Version", "13") w.WriteHeader(http.StatusUpgradeRequired) } else { w.WriteHeader(http.StatusBadRequest) } w.Write([]byte(body)) return nil, nil, err } conn, rw, err = http.NewResponseController(w).Hijack() if err != nil { body := err.Error() w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(body)) return nil, nil, err } // Clear deadlines set by server. conn.SetDeadline(time.Time{}) rw.Writer.WriteString("HTTP/1.1 101 Switching Protocols\r\n") header := http.Header{} header.Set("Upgrade", "websocket") header.Set("Connection", "Upgrade") header.Set("Sec-WebSocket-Accept", N.GetWebSocketSecAccept(nonce)) header.Write(rw.Writer) rw.Writer.WriteString("\r\n") err = rw.Writer.Flush() return conn, rw, err } // wsWriteServerMessage writes message to w, considering that caller represents server side. func wsWriteServerMessage(w io.Writer, op byte, p []byte) error { dataLen := len(p) // Make slice of bytes with capacity 14 that could hold any header. bts := make([]byte, 14) bts[0] |= 0x80 //FIN bts[0] |= 0 << 4 //RSV bts[0] |= op //OPCODE var n int switch { case dataLen < 126: bts[1] = byte(dataLen) n = 2 case dataLen < 65536: bts[1] = 126 binary.BigEndian.PutUint16(bts[2:4], uint16(dataLen)) n = 4 default: bts[1] = 127 binary.BigEndian.PutUint64(bts[2:10], uint64(dataLen)) n = 10 } _, err := w.Write(bts[:n]) if err != nil { return err } _, err = w.Write(p) return err } // wsWriteServerText is the same as wsWriteServerMessage with ws.OpText. func wsWriteServerText(w io.Writer, p []byte) error { const opText = 0x1 return wsWriteServerMessage(w, opText, p) } // wsWriteServerBinary is the same as wsWriteServerMessage with ws.OpBinary. func wsWriteServerBinary(w io.Writer, p []byte) error { const opBinary = 0x2 return wsWriteServerMessage(w, opBinary, p) } ================================================ FILE: core/Clash.Meta/hub/route/configs.go ================================================ package route import ( "net/netip" "path/filepath" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/listener" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func configRouter() http.Handler { r := chi.NewRouter() r.Get("/", getConfigs) if !embedMode { // disallow update/patch configs in embed mode r.Put("/", updateConfigs) r.Post("/geo", updateGeoDatabases) r.Patch("/", patchConfigs) } 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"` Tun *tunSchema `json:"tun"` TuicServer *tuicServerSchema `json:"tuic-server"` ShadowSocksConfig *string `json:"ss-config"` VmessConfig *string `json:"vmess-config"` TcptunConfig *string `json:"tcptun-config"` UdptunConfig *string `json:"udptun-config"` AllowLan *bool `json:"allow-lan"` SkipAuthPrefixes *[]netip.Prefix `json:"skip-auth-prefixes"` LanAllowedIPs *[]netip.Prefix `json:"lan-allowed-ips"` LanDisAllowedIPs *[]netip.Prefix `json:"lan-disallowed-ips"` BindAddress *string `json:"bind-address"` Mode *tunnel.TunnelMode `json:"mode"` LogLevel *log.LogLevel `json:"log-level"` IPv6 *bool `json:"ipv6"` Sniffing *bool `json:"sniffing"` TcpConcurrent *bool `json:"tcp-concurrent"` FindProcessMode *process.FindProcessMode `json:"find-process-mode"` InterfaceName *string `json:"interface-name"` } type tunSchema struct { Enable bool `yaml:"enable" json:"enable"` Device *string `yaml:"device" json:"device"` Stack *C.TUNStack `yaml:"stack" json:"stack"` DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"` AutoRoute *bool `yaml:"auto-route" json:"auto-route"` AutoDetectInterface *bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` MTU *uint32 `yaml:"mtu" json:"mtu,omitempty"` GSO *bool `yaml:"gso" json:"gso,omitempty"` GSOMaxSize *uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` //Inet4Address *[]netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` Inet6Address *[]netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` IPRoute2TableIndex *int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` IPRoute2RuleIndex *int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` AutoRedirect *bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` AutoRedirectInputMark *uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` AutoRedirectOutputMark *uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` AutoRedirectIPRoute2FallbackRuleIndex *int `yaml:"auto-redirect-iproute2-fallback-rule-index" json:"auto-redirect-iproute2-fallback-rule-index,omitempty"` LoopbackAddress *[]netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"` StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` RouteAddressSet *[]string `yaml:"route-address-set" json:"route-address-set,omitempty"` RouteExcludeAddress *[]netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` RouteExcludeAddressSet *[]string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` IncludeInterface *[]string `yaml:"include-interface" json:"include-interface,omitempty"` ExcludeInterface *[]string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` IncludeUID *[]uint32 `yaml:"include-uid" json:"include-uid,omitempty"` IncludeUIDRange *[]string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` ExcludeUID *[]uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` ExcludeUIDRange *[]string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` IncludeAndroidUser *[]int `yaml:"include-android-user" json:"include-android-user,omitempty"` IncludePackage *[]string `yaml:"include-package" json:"include-package,omitempty"` ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"` IncludeMACAddress *[]string `yaml:"include-mac-address" json:"include-mac-address,omitempty"` ExcludeMACAddress *[]string `yaml:"exclude-mac-address" json:"exclude-mac-address,omitempty"` EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress *[]netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` Inet6RouteAddress *[]netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` Inet4RouteExcludeAddress *[]netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` Inet6RouteExcludeAddress *[]netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` // darwin special config RecvMsgX *bool `yaml:"recvmsgx" json:"recvmsgx,omitempty"` SendMsgX *bool `yaml:"sendmsgx" json:"sendmsgx,omitempty"` } type tuicServerSchema struct { Enable bool `yaml:"enable" json:"enable"` Listen *string `yaml:"listen" json:"listen"` Token *[]string `yaml:"token" json:"token"` Users *map[string]string `yaml:"users" json:"users,omitempty"` Certificate *string `yaml:"certificate" json:"certificate"` PrivateKey *string `yaml:"private-key" json:"private-key"` CongestionController *string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` MaxIdleTime *int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` AuthenticationTimeout *int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` ALPN *[]string `yaml:"alpn" json:"alpn,omitempty"` MaxUdpRelayPacketSize *int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` CWND *int `yaml:"cwnd" json:"cwnd,omitempty"` BBRProfile *string `yaml:"bbr-profile" json:"bbr-profile,omitempty"` } func getConfigs(w http.ResponseWriter, r *http.Request) { general := executor.GetGeneral() render.JSON(w, r, general) } func pointerOrDefault[T any](p *T, def T) T { if p != nil { return *p } return def } func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { if p != nil { def.Enable = p.Enable if p.Device != nil { def.Device = *p.Device } if p.Stack != nil { def.Stack = *p.Stack } if p.DNSHijack != nil { def.DNSHijack = *p.DNSHijack } if p.AutoRoute != nil { def.AutoRoute = *p.AutoRoute } if p.AutoDetectInterface != nil { def.AutoDetectInterface = *p.AutoDetectInterface } if p.MTU != nil { def.MTU = *p.MTU } if p.GSO != nil { def.GSO = *p.GSO } if p.GSOMaxSize != nil { def.GSOMaxSize = *p.GSOMaxSize } //if p.Inet4Address != nil { // def.Inet4Address = *p.Inet4Address //} if p.Inet6Address != nil { def.Inet6Address = *p.Inet6Address } if p.IPRoute2TableIndex != nil { def.IPRoute2TableIndex = *p.IPRoute2TableIndex } if p.IPRoute2RuleIndex != nil { def.IPRoute2RuleIndex = *p.IPRoute2RuleIndex } if p.AutoRedirect != nil { def.AutoRedirect = *p.AutoRedirect } if p.AutoRedirectInputMark != nil { def.AutoRedirectInputMark = *p.AutoRedirectInputMark } if p.AutoRedirectOutputMark != nil { def.AutoRedirectOutputMark = *p.AutoRedirectOutputMark } if p.AutoRedirectIPRoute2FallbackRuleIndex != nil { def.AutoRedirectIPRoute2FallbackRuleIndex = *p.AutoRedirectIPRoute2FallbackRuleIndex } if p.LoopbackAddress != nil { def.LoopbackAddress = *p.LoopbackAddress } if p.StrictRoute != nil { def.StrictRoute = *p.StrictRoute } if p.RouteAddress != nil { def.RouteAddress = *p.RouteAddress } if p.RouteAddressSet != nil { def.RouteAddressSet = *p.RouteAddressSet } if p.RouteExcludeAddress != nil { def.RouteExcludeAddress = *p.RouteExcludeAddress } if p.RouteExcludeAddressSet != nil { def.RouteExcludeAddressSet = *p.RouteExcludeAddressSet } if p.Inet4RouteAddress != nil { def.Inet4RouteAddress = *p.Inet4RouteAddress } if p.Inet6RouteAddress != nil { def.Inet6RouteAddress = *p.Inet6RouteAddress } if p.Inet4RouteExcludeAddress != nil { def.Inet4RouteExcludeAddress = *p.Inet4RouteExcludeAddress } if p.Inet6RouteExcludeAddress != nil { def.Inet6RouteExcludeAddress = *p.Inet6RouteExcludeAddress } if p.IncludeInterface != nil { def.IncludeInterface = *p.IncludeInterface } if p.ExcludeInterface != nil { def.ExcludeInterface = *p.ExcludeInterface } if p.IncludeUID != nil { def.IncludeUID = *p.IncludeUID } if p.IncludeUIDRange != nil { def.IncludeUIDRange = *p.IncludeUIDRange } if p.ExcludeUID != nil { def.ExcludeUID = *p.ExcludeUID } if p.ExcludeUIDRange != nil { def.ExcludeUIDRange = *p.ExcludeUIDRange } if p.IncludeAndroidUser != nil { def.IncludeAndroidUser = *p.IncludeAndroidUser } if p.IncludePackage != nil { def.IncludePackage = *p.IncludePackage } if p.ExcludePackage != nil { def.ExcludePackage = *p.ExcludePackage } if p.IncludeMACAddress != nil { def.IncludeMACAddress = *p.IncludeMACAddress } if p.ExcludeMACAddress != nil { def.ExcludeMACAddress = *p.ExcludeMACAddress } if p.EndpointIndependentNat != nil { def.EndpointIndependentNat = *p.EndpointIndependentNat } if p.UDPTimeout != nil { def.UDPTimeout = *p.UDPTimeout } if p.FileDescriptor != nil { def.FileDescriptor = *p.FileDescriptor } if p.RecvMsgX != nil { def.RecvMsgX = *p.RecvMsgX } if p.SendMsgX != nil { def.SendMsgX = *p.SendMsgX } } return def } func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicServer { if p != nil { def.Enable = p.Enable if p.Listen != nil { def.Listen = *p.Listen } if p.Token != nil { def.Token = *p.Token } if p.Users != nil { def.Users = *p.Users } if p.Certificate != nil { def.Certificate = *p.Certificate } if p.PrivateKey != nil { def.PrivateKey = *p.PrivateKey } if p.CongestionController != nil { def.CongestionController = *p.CongestionController } if p.MaxIdleTime != nil { def.MaxIdleTime = *p.MaxIdleTime } if p.AuthenticationTimeout != nil { def.AuthenticationTimeout = *p.AuthenticationTimeout } if p.ALPN != nil { def.ALPN = *p.ALPN } if p.MaxUdpRelayPacketSize != nil { def.MaxUdpRelayPacketSize = *p.MaxUdpRelayPacketSize } if p.CWND != nil { def.CWND = *p.CWND } if p.BBRProfile != nil { def.BBRProfile = *p.BBRProfile } } return def } func patchConfigs(w http.ResponseWriter, r *http.Request) { general := &configSchema{} if err := render.DecodeJSON(r.Body, &general); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } if general.AllowLan != nil { listener.SetAllowLan(*general.AllowLan) } if general.SkipAuthPrefixes != nil { inbound.SetSkipAuthPrefixes(*general.SkipAuthPrefixes) } if general.LanAllowedIPs != nil { inbound.SetAllowedIPs(*general.LanAllowedIPs) } if general.LanDisAllowedIPs != nil { inbound.SetDisAllowedIPs(*general.LanDisAllowedIPs) } if general.BindAddress != nil { listener.SetBindAddress(*general.BindAddress) } if general.Sniffing != nil { tunnel.SetSniffing(*general.Sniffing) } if general.TcpConcurrent != nil { dialer.SetTcpConcurrent(*general.TcpConcurrent) } if general.InterfaceName != nil { dialer.DefaultInterface.Store(*general.InterfaceName) } ports := listener.GetPorts() listener.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port), tunnel.Tunnel) listener.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort), tunnel.Tunnel) listener.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tunnel.Tunnel) listener.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tunnel.Tunnel) listener.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tunnel.Tunnel) listener.ReCreateTun(pointerOrDefaultTun(general.Tun, listener.LastTunConf), tunnel.Tunnel) listener.ReCreateShadowSocks(pointerOrDefault(general.ShadowSocksConfig, ports.ShadowSocksConfig), tunnel.Tunnel) listener.ReCreateVmess(pointerOrDefault(general.VmessConfig, ports.VmessConfig), tunnel.Tunnel) listener.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, listener.LastTuicConf), tunnel.Tunnel) if general.Mode != nil { tunnel.SetMode(*general.Mode) } if general.FindProcessMode != nil { tunnel.SetFindProcessMode(*general.FindProcessMode) } if general.LogLevel != nil { log.SetLevel(*general.LogLevel) } if general.IPv6 != nil { resolver.DisableIPv6 = !*general.IPv6 } render.NoContent(w, r) } func updateConfigs(w http.ResponseWriter, r *http.Request) { req := struct { Path string `json:"path"` Payload string `json:"payload"` }{} if err := render.DecodeJSON(r.Body, &req); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } force := r.URL.Query().Get("force") == "true" var cfg *config.Config var err error if req.Payload != "" { cfg, err = executor.ParseWithBytes([]byte(req.Payload)) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(err.Error())) return } } else { if req.Path == "" { // default path unneeded any safe check req.Path = C.Path.Config() } else { if !filepath.IsAbs(req.Path) { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("path is not a absolute path")) return } if !C.Path.IsSafePath(req.Path) { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(C.Path.ErrNotSafePath(req.Path).Error())) return } } cfg, err = executor.ParseWithPath(req.Path) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(err.Error())) return } } executor.ApplyConfig(cfg, force) render.NoContent(w, r) } func updateGeoDatabases(w http.ResponseWriter, r *http.Request) { err := updater.UpdateGeoDatabases() if err != nil { log.Errorln("[GEO] update GEO databases failed: %v", err) render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } render.NoContent(w, r) } ================================================ FILE: core/Clash.Meta/hub/route/connections.go ================================================ package route import ( "bytes" "encoding/json" "strconv" "time" "github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func connectionRouter() http.Handler { r := chi.NewRouter() r.Get("/", getConnections) r.Delete("/", closeAllConnections) r.Delete("/{id}", closeConnection) return r } func getConnections(w http.ResponseWriter, r *http.Request) { if !(r.Header.Get("Upgrade") == "websocket") { snapshot := statistic.DefaultManager.Snapshot() render.JSON(w, r, snapshot) return } conn, _, err := wsUpgrade(r, w) if err != nil { return } 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 := statistic.DefaultManager.Snapshot() if err := json.NewEncoder(buf).Encode(snapshot); err != nil { return err } return wsWriteServerText(conn, buf.Bytes()) } if err := sendSnapshot(); err != nil { return } tick := time.NewTicker(time.Millisecond * time.Duration(interval)) defer tick.Stop() for range tick.C { if err := sendSnapshot(); err != nil { break } } } func closeConnection(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if c := statistic.DefaultManager.Get(id); c != nil { _ = c.Close() } render.NoContent(w, r) } func closeAllConnections(w http.ResponseWriter, r *http.Request) { statistic.DefaultManager.Range(func(c statistic.Tracker) bool { _ = c.Close() return true }) render.NoContent(w, r) } ================================================ FILE: core/Clash.Meta/hub/route/ctxkeys.go ================================================ package route var ( CtxKeyProxyName = contextKey("proxy name") CtxKeyProviderName = contextKey("provider name") CtxKeyProxy = contextKey("proxy") CtxKeyProvider = contextKey("provider") ) type contextKey string func (c contextKey) String() string { return "mihomo context key " + string(c) } ================================================ FILE: core/Clash.Meta/hub/route/dns.go ================================================ package route import ( "context" "math" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" "github.com/miekg/dns" "github.com/samber/lo" ) func dnsRouter() http.Handler { r := chi.NewRouter() r.Get("/query", queryDNS) return r } func queryDNS(w http.ResponseWriter, r *http.Request) { if resolver.DefaultResolver == nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError("DNS section is disabled")) return } name := r.URL.Query().Get("name") qTypeStr, _ := lo.Coalesce(r.URL.Query().Get("type"), "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(), resolver.DefaultDNSTimeout) defer cancel() msg := dns.Msg{} msg.SetQuestion(dns.Fqdn(name), qType) resp, err := resolver.DefaultResolver.ExchangeContext(ctx, &msg) 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, "TC": resp.Truncated, "RD": resp.RecursionDesired, "RA": resp.RecursionAvailable, "AD": resp.AuthenticatedData, "CD": resp.CheckingDisabled, } rr2Json := func(rr dns.RR, _ int) render.M { header := rr.Header() return render.M{ "name": header.Name, "type": header.Rrtype, "TTL": header.Ttl, "data": lo.Substring(rr.String(), len(header.String()), math.MaxUint), } } if len(resp.Answer) > 0 { responseData["Answer"] = lo.Map(resp.Answer, rr2Json) } if len(resp.Ns) > 0 { responseData["Authority"] = lo.Map(resp.Ns, rr2Json) } if len(resp.Extra) > 0 { responseData["Additional"] = lo.Map(resp.Extra, rr2Json) } render.JSON(w, r, responseData) } ================================================ FILE: core/Clash.Meta/hub/route/doh.go ================================================ package route import ( "context" "encoding/base64" "io" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func dohRouter() http.Handler { return http.HandlerFunc(dohHandler) } func dohHandler(w http.ResponseWriter, r *http.Request) { if resolver.DefaultResolver == nil { render.Status(r, http.StatusInternalServerError) render.PlainText(w, r, "DNS section is disabled") return } var dnsData []byte var err error switch r.Method { case "GET": dnsData, err = base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns")) case "POST": if r.Header.Get("Content-Type") != "application/dns-message" { render.Status(r, http.StatusInternalServerError) render.PlainText(w, r, "invalid content-type") return } reader := io.LimitReader(r.Body, 65535) // according to rfc8484, the maximum size of the DNS message is 65535 bytes dnsData, err = io.ReadAll(reader) _ = r.Body.Close() default: render.Status(r, http.StatusMethodNotAllowed) render.PlainText(w, r, "method not allowed") return } if err != nil { render.Status(r, http.StatusInternalServerError) render.PlainText(w, r, err.Error()) return } ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) defer cancel() dnsData, err = resolver.RelayDnsPacket(ctx, dnsData, dnsData) if err != nil { render.Status(r, http.StatusInternalServerError) render.PlainText(w, r, err.Error()) return } w.Header().Set("Content-Type", "application/dns-message") w.WriteHeader(http.StatusOK) _, _ = w.Write(dnsData) } ================================================ FILE: core/Clash.Meta/hub/route/errors.go ================================================ package route 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: core/Clash.Meta/hub/route/external.go ================================================ package route import "github.com/metacubex/chi" type externalRouter func(r chi.Router) var externalRouters = make([]externalRouter, 0) func Register(route ...externalRouter) { externalRouters = append(externalRouters, route...) } func addExternalRouters(r chi.Router) { if len(externalRouters) == 0 { return } for _, caller := range externalRouters { caller(r) } } ================================================ FILE: core/Clash.Meta/hub/route/groups.go ================================================ package route import ( "context" "strconv" "time" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/profile/cachefile" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func groupRouter() http.Handler { r := chi.NewRouter() r.Get("/", getGroups) r.Route("/{name}", func(r chi.Router) { r.Use(parseProxyName, findProxyByName) r.Get("/", getGroup) r.Get("/delay", getGroupDelay) }) return r } func getGroups(w http.ResponseWriter, r *http.Request) { var gs []C.Proxy for _, p := range tunnel.Proxies() { if _, ok := p.Adapter().(outboundgroup.ProxyGroup); ok { gs = append(gs, p) } } render.JSON(w, r, render.M{ "proxies": gs, }) } func getGroup(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) if _, ok := proxy.Adapter().(outboundgroup.ProxyGroup); ok { render.JSON(w, r, proxy) return } render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) } func getGroupDelay(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) group, ok := proxy.Adapter().(outboundgroup.ProxyGroup) if !ok { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) return } if selectAble, ok := proxy.Adapter().(outboundgroup.SelectAble); ok && proxy.Type() != C.Selector { selectAble.ForceSet("") cachefile.Cache().SetSelected(proxy.Name(), "") } query := r.URL.Query() url := query.Get("url") timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } expectedStatus, err := utils.NewUnsignedRanges[uint16](query.Get("expected")) 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() dm, err := group.URLTest(ctx, url, expectedStatus) if err != nil { render.Status(r, http.StatusGatewayTimeout) render.JSON(w, r, newError(err.Error())) return } render.JSON(w, r, dm) } ================================================ FILE: core/Clash.Meta/hub/route/patch_android.go ================================================ //go:build android && cmfa package route func init() { SetEmbedMode(true) // set embed mode default } ================================================ FILE: core/Clash.Meta/hub/route/provider.go ================================================ package route import ( "context" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" "github.com/samber/lo" ) func proxyProviderRouter() http.Handler { r := chi.NewRouter() r.Get("/", getProviders) r.Route("/{providerName}", func(r chi.Router) { r.Use(parseProviderName, findProviderByName) r.Get("/", getProvider) r.Put("/", updateProvider) r.Get("/healthcheck", healthCheckProvider) r.Mount("/", proxyProviderProxyRouter()) }) return r } func proxyProviderProxyRouter() http.Handler { r := chi.NewRouter() r.Route("/{name}", func(r chi.Router) { r.Use(parseProxyName, findProviderProxyByName) r.Get("/", getProxy) r.Get("/healthcheck", getProxyDelay) }) return r } func getProviders(w http.ResponseWriter, r *http.Request) { providers := tunnel.Providers() render.JSON(w, r, render.M{ "providers": providers, }) } func getProvider(w http.ResponseWriter, r *http.Request) { provider := r.Context().Value(CtxKeyProvider).(P.ProxyProvider) render.JSON(w, r, provider) } func updateProvider(w http.ResponseWriter, r *http.Request) { provider := r.Context().Value(CtxKeyProvider).(P.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).(P.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, "providerName") 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.Providers() 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)) }) } func findProviderProxyByName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ( name = r.Context().Value(CtxKeyProxyName).(string) pd = r.Context().Value(CtxKeyProvider).(P.ProxyProvider) ) proxy, exist := lo.Find(pd.Proxies(), func(proxy C.Proxy) bool { return proxy.Name() == 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 ruleProviderRouter() http.Handler { r := chi.NewRouter() r.Get("/", getRuleProviders) r.Route("/{name}", func(r chi.Router) { r.Use(parseRuleProviderName, findRuleProviderByName) r.Put("/", updateRuleProvider) }) return r } func getRuleProviders(w http.ResponseWriter, r *http.Request) { ruleProviders := tunnel.RuleProviders() render.JSON(w, r, render.M{ "providers": ruleProviders, }) } func updateRuleProvider(w http.ResponseWriter, r *http.Request) { provider := r.Context().Value(CtxKeyProvider).(P.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 parseRuleProviderName(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 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: core/Clash.Meta/hub/route/proxies.go ================================================ package route import ( "context" "fmt" "strconv" "time" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/profile/cachefile" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) var ( SwitchProxiesCallback func(sGroup string, sProxy string) ) func proxyRouter() http.Handler { r := chi.NewRouter() r.Get("/", getProxies) r.Route("/{name}", func(r chi.Router) { r.Use(parseProxyName, findProxyByName) r.Get("/", getProxy) r.Get("/delay", getProxyDelay) r.Put("/", updateProxy) r.Delete("/", unfixedProxy) }) 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(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := r.Context().Value(CtxKeyProxyName).(string) proxies := proxiesWithProviders() proxy, exist := proxies[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 getProxies(w http.ResponseWriter, r *http.Request) { proxies := proxiesWithProviders() render.JSON(w, r, render.M{ "proxies": proxies, }) } func getProxy(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) render.JSON(w, r, proxy) } func updateProxy(w http.ResponseWriter, r *http.Request) { req := struct { Name string `json:"name"` }{} 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).(C.Proxy) selector, ok := proxy.Adapter().(outboundgroup.SelectAble) if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("Must be a Selector")) return } if err := selector.Set(req.Name); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error()))) return } cachefile.Cache().SetSelected(proxy.Name(), req.Name) if SwitchProxiesCallback != nil { // refresh tray menu go SwitchProxiesCallback(proxy.Name(), req.Name) } render.NoContent(w, r) } func getProxyDelay(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() url := query.Get("url") timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } expectedStatus, err := utils.NewUnsignedRanges[uint16](query.Get("expected")) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) defer cancel() delay, err := proxy.URLTest(ctx, url, expectedStatus) 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) if err != nil && delay != 0 { render.JSON(w, r, err) } else { render.JSON(w, r, newError("An error occurred in the delay test")) } return } render.JSON(w, r, render.M{ "delay": delay, }) } func unfixedProxy(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) if selectAble, ok := proxy.Adapter().(outboundgroup.SelectAble); ok && proxy.Type() != C.Selector { selectAble.ForceSet("") cachefile.Cache().SetSelected(proxy.Name(), "") render.NoContent(w, r) return } render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) } // proxiesWithProviders merges all proxies from tunnel // // Deprecated: This function is poorly implemented and should not be called by any new code. // It is left here only to ensure the compatibility of the output of the existing RESTful API. func proxiesWithProviders() map[string]C.Proxy { allProxies := make(map[string]C.Proxy) for name, proxy := range tunnel.Proxies() { allProxies[name] = proxy } for _, p := range tunnel.Providers() { for _, proxy := range p.Proxies() { name := proxy.Name() allProxies[name] = proxy } } return allProxies } ================================================ FILE: core/Clash.Meta/hub/route/restart.go ================================================ package route import ( "fmt" "os" "os/exec" "runtime" "syscall" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/log" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func restartRouter() http.Handler { r := chi.NewRouter() r.Post("/", func(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusForbidden) render.JSON(w, r, newError("Not supported")) }) return r } func restart(w http.ResponseWriter, r *http.Request) { // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L108 execPath, err := os.Executable() if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(fmt.Sprintf("getting path: %s", err))) return } render.JSON(w, r, render.M{"status": "ok"}) if f, ok := w.(http.Flusher); ok { f.Flush() } // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L180 // The background context is used because the underlying functions wrap it // with timeout and shut down the server, which handles current request. It // also should be done in a separate goroutine for the same reason. go restartExecutable(execPath) } func restartExecutable(execPath string) { var err error executor.Shutdown() if runtime.GOOS == "windows" { cmd := exec.Command(execPath, os.Args[1:]...) log.Infoln("restarting: %q %q", execPath, os.Args[1:]) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Start() if err != nil { log.Fatalln("restarting: %s", err) } os.Exit(0) } log.Infoln("restarting: %q %q", execPath, os.Args[1:]) err = syscall.Exec(execPath, os.Args, os.Environ()) if err != nil { log.Fatalln("restarting: %s", err) } } ================================================ FILE: core/Clash.Meta/hub/route/rules.go ================================================ package route import ( "time" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func ruleRouter() http.Handler { r := chi.NewRouter() r.Get("/", getRules) if !embedMode { // disallow update/patch rules in embed mode r.Patch("/disable", disableRules) } return r } type Rule struct { Index int `json:"index"` Type string `json:"type"` Payload string `json:"payload"` Proxy string `json:"proxy"` Size int `json:"size"` // Extra contains information from RuleWrapper Extra *RuleExtra `json:"extra,omitempty"` } type RuleExtra struct { Disabled bool `json:"disabled"` HitCount uint64 `json:"hitCount"` HitAt time.Time `json:"hitAt"` MissCount uint64 `json:"missCount"` MissAt time.Time `json:"missAt"` } func getRules(w http.ResponseWriter, r *http.Request) { rawRules := tunnel.Rules() rules := make([]Rule, 0, len(rawRules)) for index, rule := range rawRules { r := Rule{ Index: index, Type: rule.RuleType().String(), Payload: rule.Payload(), Proxy: rule.Adapter(), Size: -1, } if ruleWrapper, ok := rule.(constant.RuleWrapper); ok { r.Extra = &RuleExtra{ Disabled: ruleWrapper.IsDisabled(), HitCount: ruleWrapper.HitCount(), HitAt: ruleWrapper.HitAt(), MissCount: ruleWrapper.MissCount(), MissAt: ruleWrapper.MissAt(), } rule = ruleWrapper.Unwrap() // unwrap RuleWrapper } if rule.RuleType() == constant.GEOIP || rule.RuleType() == constant.GEOSITE { r.Size = rule.(constant.RuleGroup).GetRecodeSize() } rules = append(rules, r) } render.JSON(w, r, render.M{ "rules": rules, }) } // disableRules disable or enable rules by their indexes. func disableRules(w http.ResponseWriter, r *http.Request) { // key: rule index, value: disabled var payload map[int]bool if err := render.DecodeJSON(r.Body, &payload); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } if len(payload) != 0 { rules := tunnel.Rules() for index, disabled := range payload { if index < 0 || index >= len(rules) { continue } rule := rules[index] if ruleWrapper, ok := rule.(constant.RuleWrapper); ok { ruleWrapper.SetDisabled(disabled) } } } render.NoContent(w, r) } ================================================ FILE: core/Clash.Meta/hub/route/server.go ================================================ package route import ( "bytes" "crypto/subtle" "encoding/json" "net" "os" "path/filepath" "runtime/debug" "strings" "syscall" "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/chi" "github.com/metacubex/chi/cors" "github.com/metacubex/chi/middleware" "github.com/metacubex/chi/render" "github.com/metacubex/http" "github.com/metacubex/tls" ) var ( uiPath = "" httpServer *http.Server tlsServer *http.Server unixServer *http.Server pipeServer *http.Server embedMode = false ) func SetEmbedMode(embed bool) { embedMode = embed } type Traffic struct { Up int64 `json:"up"` Down int64 `json:"down"` UpTotal int64 `json:"upTotal"` DownTotal int64 `json:"downTotal"` } type Memory struct { Inuse uint64 `json:"inuse"` OSLimit uint64 `json:"oslimit"` // maybe we need it in the future } type Config struct { Addr string TLSAddr string UnixAddr string PipeAddr string Secret string Certificate string PrivateKey string ClientAuthType string ClientAuthCert string EchKey string DohServer string IsDebug bool Cors Cors } type Cors struct { AllowOrigins []string AllowPrivateNetwork bool } func (c Cors) Apply(r chi.Router) { r.Use(cors.New(cors.Options{ AllowedOrigins: c.AllowOrigins, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, AllowedHeaders: []string{"Content-Type", "Authorization"}, AllowPrivateNetwork: c.AllowPrivateNetwork, MaxAge: 300, }).Handler) } func ReCreateServer(cfg *Config) { go start(cfg) go startTLS(cfg) go startUnix(cfg) if inbound.SupportNamedPipe { go startPipe(cfg) } } func SetUIPath(path string) { uiPath = C.Path.Resolve(path) } func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux { r := chi.NewRouter() cors.Apply(r) if isDebug { r.Mount("/debug", func() http.Handler { r := chi.NewRouter() r.Put("/gc", func(w http.ResponseWriter, r *http.Request) { debug.FreeOSMemory() }) handler := middleware.Profiler r.Mount("/", handler()) return r }()) } r.Group(func(r chi.Router) { if secret != "" { r.Use(authentication(secret)) } r.Get("/", hello) r.Get("/logs", getLogs) r.Get("/traffic", traffic) r.Get("/memory", memory) r.Get("/version", version) r.Mount("/configs", configRouter()) r.Mount("/proxies", proxyRouter()) r.Mount("/group", groupRouter()) r.Mount("/rules", ruleRouter()) r.Mount("/connections", connectionRouter()) r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/cache", cacheRouter()) r.Mount("/dns", dnsRouter()) r.Mount("/storage", storageRouter()) if !embedMode { // disallow restart in embed mode r.Mount("/restart", restartRouter()) } r.Mount("/upgrade", upgradeRouter()) addExternalRouters(r) }) if uiPath != "" { r.Group(func(r chi.Router) { fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath))) r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP) r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) { fs.ServeHTTP(w, r) }) }) } if len(dohServer) > 0 && dohServer[0] == '/' { r.Mount(dohServer, dohRouter()) } return r } func start(cfg *Config) { // first stop existing server if httpServer != nil { _ = httpServer.Close() httpServer = nil } // handle addr if len(cfg.Addr) > 0 { l, err := inbound.Listen("tcp", cfg.Addr) if err != nil { log.Errorln("External controller listen error: %s", err) return } log.Infoln("RESTful API listening at: %s", l.Addr().String()) server := &http.Server{ Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), } httpServer = server if err = server.Serve(l); err != nil { log.Infoln("External controller serve error: %s", err) } } } func startTLS(cfg *Config) { // first stop existing server if tlsServer != nil { _ = tlsServer.Close() tlsServer = nil } // handle tlsAddr if len(cfg.TLSAddr) > 0 { certLoader, err := ca.NewTLSKeyPairLoader(cfg.Certificate, cfg.PrivateKey) if err != nil { log.Errorln("External controller tls listen error: %s", err) return } l, err := inbound.Listen("tcp", cfg.TLSAddr) if err != nil { log.Errorln("External controller tls listen error: %s", err) return } log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) tlsConfig := &tls.Config{Time: ntp.Now} tlsConfig.NextProtos = []string{"h2", "http/1.1"} tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(cfg.ClientAuthType) if len(cfg.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(cfg.ClientAuthCert) if err != nil { log.Errorln("External controller tls listen error: %s", err) return } tlsConfig.ClientCAs = pool } if cfg.EchKey != "" { err = ech.LoadECHKey(cfg.EchKey, tlsConfig) if err != nil { log.Errorln("External controller tls serve error: %s", err) return } } server := &http.Server{ Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), } tlsServer = server if err = server.Serve(tls.NewListener(l, tlsConfig)); err != nil { log.Errorln("External controller tls serve error: %s", err) } } } func startUnix(cfg *Config) { // first stop existing server if unixServer != nil { _ = unixServer.Close() unixServer = nil } // handle addr if len(cfg.UnixAddr) > 0 { addr := C.Path.Resolve(cfg.UnixAddr) dir := filepath.Dir(addr) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0o755); err != nil { log.Errorln("External controller unix listen error: %s", err) return } } // https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ // // Note: As mentioned above in the ‘security’ section, when a socket binds a socket to a valid pathname address, // a socket file is created within the filesystem. On Linux, the application is expected to unlink // (see the notes section in the man page for AF_UNIX) before any other socket can be bound to the same address. // The same applies to Windows unix sockets, except that, DeleteFile (or any other file delete API) // should be used to delete the socket file prior to calling bind with the same path. _ = syscall.Unlink(addr) l, err := inbound.Listen("unix", addr) if err != nil { log.Errorln("External controller unix listen error: %s", err) return } _ = os.Chmod(addr, 0o666) log.Infoln("RESTful API unix listening at: %s", l.Addr().String()) server := &http.Server{ Handler: router(cfg.IsDebug, "", cfg.DohServer, cfg.Cors), } unixServer = server if err = server.Serve(l); err != nil { log.Errorln("External controller unix serve error: %s", err) } } } func startPipe(cfg *Config) { // first stop existing server if pipeServer != nil { _ = pipeServer.Close() pipeServer = nil } // handle addr if len(cfg.PipeAddr) > 0 { if !strings.HasPrefix(cfg.PipeAddr, "\\\\.\\pipe\\") { // windows namedpipe must start with "\\.\pipe\" log.Errorln("External controller pipe listen error: windows namedpipe must start with \"\\\\.\\pipe\\\"") return } l, err := inbound.ListenNamedPipe(cfg.PipeAddr) if err != nil { log.Errorln("External controller pipe listen error: %s", err) return } log.Infoln("RESTful API pipe listening at: %s", l.Addr().String()) server := &http.Server{ Handler: router(cfg.IsDebug, "", cfg.DohServer, cfg.Cors), } pipeServer = server if err = server.Serve(l); err != nil { log.Errorln("External controller pipe serve error: %s", err) } } } func safeEqual(a, b string) bool { aBuf := utils.ImmutableBytesFromString(a) bBuf := utils.ImmutableBytesFromString(b) return subtle.ConstantTimeCompare(aBuf, bBuf) == 1 } func authentication(secret string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { // Browser websocket not support custom header if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { token := r.URL.Query().Get("token") if !safeEqual(token, secret) { 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 || !safeEqual(token, secret) if hasInvalidHeader || hasInvalidSecret { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) return } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } } func hello(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, render.M{"hello": "mihomo"}) } func traffic(w http.ResponseWriter, r *http.Request) { var wsConn net.Conn if r.Header.Get("Upgrade") == "websocket" { var err error wsConn, _, err = wsUpgrade(r, w) if err != nil { return } } if wsConn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } tick := time.NewTicker(time.Second) defer tick.Stop() t := statistic.DefaultManager buf := &bytes.Buffer{} var err error for range tick.C { buf.Reset() up, down := t.Now() upTotal, downTotal := t.Total() if err := json.NewEncoder(buf).Encode(Traffic{ Up: up, Down: down, UpTotal: upTotal, DownTotal: downTotal, }); err != nil { break } if wsConn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsWriteServerText(wsConn, buf.Bytes()) } if err != nil { break } } } func memory(w http.ResponseWriter, r *http.Request) { var wsConn net.Conn if r.Header.Get("Upgrade") == "websocket" { var err error wsConn, _, err = wsUpgrade(r, w) if err != nil { return } } if wsConn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } tick := time.NewTicker(time.Second) defer tick.Stop() t := statistic.DefaultManager buf := &bytes.Buffer{} var err error first := true for range tick.C { buf.Reset() inuse := t.Memory() // make chat.js begin with zero // this is shit var,but we need output 0 for first time if first { inuse = 0 first = false } if err := json.NewEncoder(buf).Encode(Memory{ Inuse: inuse, OSLimit: 0, }); err != nil { break } if wsConn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsWriteServerText(wsConn, buf.Bytes()) } if err != nil { break } } } type Log struct { Type string `json:"type"` Payload string `json:"payload"` } type LogStructuredField struct { Key string `json:"key"` Value string `json:"value"` } type LogStructured struct { Time string `json:"time"` Level string `json:"level"` Message string `json:"message"` Fields []LogStructuredField `json:"fields"` } func getLogs(w http.ResponseWriter, r *http.Request) { levelText := r.URL.Query().Get("level") if levelText == "" { levelText = "info" } formatText := r.URL.Query().Get("format") isStructured := false if formatText == "structured" { isStructured = true } level, ok := log.LogLevelMapping[levelText] if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } var wsConn net.Conn if r.Header.Get("Upgrade") == "websocket" { var err error wsConn, _, err = wsUpgrade(r, w) if err != nil { return } } if wsConn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } ch := make(chan log.Event, 1024) sub := log.Subscribe() defer log.UnSubscribe(sub) buf := &bytes.Buffer{} go func() { for logM := range sub { select { case ch <- logM: default: } } close(ch) }() for logM := range ch { if logM.LogLevel < level { continue } buf.Reset() if !isStructured { if err := json.NewEncoder(buf).Encode(Log{ Type: logM.Type(), Payload: logM.Payload, }); err != nil { break } } else { newLevel := logM.Type() if newLevel == "warning" { newLevel = "warn" } if err := json.NewEncoder(buf).Encode(LogStructured{ Time: time.Now().Format(time.TimeOnly), Level: newLevel, Message: logM.Payload, Fields: []LogStructuredField{}, }); err != nil { break } } var err error if wsConn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsWriteServerText(wsConn, buf.Bytes()) } if err != nil { break } } } func version(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, render.M{"meta": C.Meta, "version": C.Version}) } ================================================ FILE: core/Clash.Meta/hub/route/storage.go ================================================ package route import ( "encoding/json" "io" "github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func storageRouter() http.Handler { r := chi.NewRouter() r.Get("/{key}", getStorage) r.Put("/{key}", setStorage) r.Delete("/{key}", deleteStorage) return r } func getStorage(w http.ResponseWriter, r *http.Request) { key := getEscapeParam(r, "key") data := cachefile.Cache().GetStorage(key) w.Header().Set("Content-Type", "application/json") if len(data) == 0 { w.Write([]byte("null")) return } w.Write(data) } func setStorage(w http.ResponseWriter, r *http.Request) { key := getEscapeParam(r, "key") data, err := io.ReadAll(r.Body) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(err.Error())) return } if !json.Valid(data) { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } if len(data) > 1024*1024 { render.Status(r, http.StatusRequestEntityTooLarge) render.JSON(w, r, newError("payload exceeds 1MB limit")) return } cachefile.Cache().SetStorage(key, data) render.NoContent(w, r) } func deleteStorage(w http.ResponseWriter, r *http.Request) { key := getEscapeParam(r, "key") cachefile.Cache().DeleteStorage(key) render.NoContent(w, r) } ================================================ FILE: core/Clash.Meta/hub/route/upgrade.go ================================================ package route import ( "fmt" "os" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/log" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" ) func upgradeRouter() http.Handler { r := chi.NewRouter() r.Post("/ui", updateUI) r.Post("/", func(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusForbidden) render.JSON(w, r, newError("Not supported")) }) if !embedMode { r.Post("/geo", updateGeoDatabases) } return r } func upgradeCore(w http.ResponseWriter, r *http.Request) { // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L108 log.Infoln("start update") execPath, err := os.Executable() if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(fmt.Sprintf("getting path: %s", err))) return } query := r.URL.Query() channel := query.Get("channel") force := query.Get("force") == "true" err = updater.DefaultCoreUpdater.Update(execPath, channel, force) if err != nil { log.Warnln("%s", err) render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(fmt.Sprintf("%s", err))) return } render.JSON(w, r, render.M{"status": "ok"}) if f, ok := w.(http.Flusher); ok { f.Flush() } go restartExecutable(execPath) } func updateUI(w http.ResponseWriter, r *http.Request) { err := updater.DefaultUiUpdater.DownloadUI() if err != nil { log.Warnln("%s", err) render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(fmt.Sprintf("%s", err))) return } render.JSON(w, r, render.M{"status": "ok"}) if f, ok := w.(http.Flusher); ok { f.Flush() } } ================================================ FILE: core/Clash.Meta/listener/anytls/server.go ================================================ package anytls import ( "context" "crypto/sha256" "encoding/binary" "errors" "net" "strings" "sync/atomic" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/anytls/padding" "github.com/metacubex/mihomo/transport/anytls/session" "github.com/metacubex/sing/common/auth" "github.com/metacubex/sing/common/bufio" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/tls" ) type Listener struct { closed bool config LC.AnyTLSServer listeners []net.Listener tlsConfig *tls.Config userMap map[[32]byte]string padding atomic.Pointer[padding.PaddingFactory] } func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-ANYTLS"), inbound.WithSpecialRules(""), } } tlsConfig := &tls.Config{Time: ntp.Now} if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } sl = &Listener{ config: config, tlsConfig: tlsConfig, userMap: make(map[[32]byte]string), } for user, password := range config.Users { sl.userMap[sha256.Sum256([]byte(password))] = user } if len(config.PaddingScheme) > 0 { if !padding.UpdatePaddingScheme([]byte(config.PaddingScheme), &sl.padding) { return nil, errors.New("incorrect padding scheme format") } } else { padding.UpdatePaddingScheme(padding.DefaultPaddingScheme, &sl.padding) } // Using sing handler can automatically handle UoT h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.ANYTLS, Additions: additions, }) if err != nil { return nil, err } for _, addr := range strings.Split(config.Listen, ",") { addr := addr //TCP l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else { return nil, errors.New("disallow using AnyTLS without certificates config") } sl.listeners = append(sl.listeners, l) go func() { for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } go sl.HandleConn(c, h) } }() } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, lis := range l.listeners { err := lis.Close() if err != nil { retErr = err } } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.listeners { addrList = append(addrList, lis.Addr()) } return } func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) { ctx := context.TODO() defer conn.Close() b := buf.NewPacket() defer b.Release() _, err := b.ReadOnceFrom(conn) if err != nil { return } conn = bufio.NewCachedConn(conn, b) by, err := b.ReadBytes(32) if err != nil { return } var passwordSha256 [32]byte copy(passwordSha256[:], by) if user, ok := l.userMap[passwordSha256]; ok { ctx = auth.ContextWithUser(ctx, user) } else { return } by, err = b.ReadBytes(2) if err != nil { return } paddingLen := binary.BigEndian.Uint16(by) if paddingLen > 0 { _, err = b.ReadBytes(int(paddingLen)) if err != nil { return } } session := session.NewServerSession(conn, func(stream *session.Stream) { defer stream.Close() destination, err := M.SocksaddrSerializer.ReadAddrPort(stream) if err != nil { return } // It seems that mihomo does not implement a connection error reporting mechanism, so we report success directly. err = stream.HandshakeSuccess() if err != nil { return } h.NewConnection(ctx, stream, M.Metadata{ Source: M.SocksaddrFromNet(conn.RemoteAddr()), Destination: destination, }) }, &l.padding) session.Run() session.Close() } ================================================ FILE: core/Clash.Meta/listener/auth/auth.go ================================================ package auth import ( "github.com/metacubex/mihomo/component/auth" ) type authStore struct { authenticator auth.Authenticator } func (a *authStore) Authenticator() auth.Authenticator { return a.authenticator } func (a *authStore) SetAuthenticator(authenticator auth.Authenticator) { a.authenticator = authenticator } func NewAuthStore(authenticator auth.Authenticator) auth.AuthStore { return &authStore{authenticator} } var Default auth.AuthStore = NewAuthStore(nil) type nilAuthStore struct{} func (a *nilAuthStore) Authenticator() auth.Authenticator { return nil } func (a *nilAuthStore) SetAuthenticator(authenticator auth.Authenticator) {} var Nil auth.AuthStore = (*nilAuthStore)(nil) // always return nil, even call SetAuthenticator() with a non-nil authenticator ================================================ FILE: core/Clash.Meta/listener/config/anytls.go ================================================ package config import ( "encoding/json" ) type AnyTLSServer struct { Enable bool `yaml:"enable" json:"enable"` Listen string `yaml:"listen" json:"listen"` Users map[string]string `yaml:"users" json:"users,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` EchKey string `yaml:"ech-key" json:"ech-key"` PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` } func (t AnyTLSServer) String() string { b, _ := json.Marshal(t) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/auth.go ================================================ package config import ( "github.com/metacubex/mihomo/component/auth" "github.com/metacubex/mihomo/listener/reality" ) // AuthServer for http/socks/mixed server type AuthServer struct { Enable bool Listen string AuthStore auth.AuthStore Certificate string PrivateKey string ClientAuthType string ClientAuthCert string EchKey string RealityConfig reality.Config } ================================================ FILE: core/Clash.Meta/listener/config/hysteria2.go ================================================ package config import ( "github.com/metacubex/mihomo/listener/sing" "encoding/json" ) type Hysteria2Server struct { Enable bool `yaml:"enable" json:"enable"` Listen string `yaml:"listen" json:"listen"` Users map[string]string `yaml:"users" json:"users,omitempty"` Obfs string `yaml:"obfs" json:"obfs,omitempty"` ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` EchKey string `yaml:"ech-key" json:"ech-key,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"` Up string `yaml:"up" json:"up,omitempty"` Down string `yaml:"down" json:"down,omitempty"` IgnoreClientBandwidth bool `yaml:"ignore-client-bandwidth" json:"ignore-client-bandwidth,omitempty"` Masquerade string `yaml:"masquerade" json:"masquerade,omitempty"` CWND int `yaml:"cwnd" json:"cwnd,omitempty"` BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"` UdpMTU int `yaml:"udp-mtu" json:"udp-mtu,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` // quic-go special config InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window" json:"initial-stream-receive-window,omitempty"` MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window" json:"max-stream-receive-window,omitempty"` InitialConnectionReceiveWindow uint64 `yaml:"initial-connection-receive-window" json:"initial-connection-receive-window,omitempty"` MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window" json:"max-connection-receive-window,omitempty"` } func (h Hysteria2Server) String() string { b, _ := json.Marshal(h) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/kcptun.go ================================================ package config import "github.com/metacubex/mihomo/transport/kcptun" type KcpTun struct { Enable bool `json:"enable"` kcptun.Config `json:",inline"` } ================================================ FILE: core/Clash.Meta/listener/config/shadowsocks.go ================================================ package config import ( "github.com/metacubex/mihomo/listener/sing" "encoding/json" ) type ShadowsocksServer struct { Enable bool Listen string Password string Cipher string Udp bool MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` KcpTun KcpTun `yaml:"kcp-tun" json:"kcp-tun,omitempty"` } func (t ShadowsocksServer) String() string { b, _ := json.Marshal(t) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/shadowtls.go ================================================ package config type ShadowTLS struct { Enable bool Version int Password string Users []ShadowTLSUser Handshake ShadowTLSHandshakeOptions HandshakeForServerName map[string]ShadowTLSHandshakeOptions StrictMode bool WildcardSNI string } type ShadowTLSUser struct { Name string Password string } type ShadowTLSHandshakeOptions struct { Dest string Proxy string } ================================================ FILE: core/Clash.Meta/listener/config/sudoku.go ================================================ package config import ( "encoding/json" "github.com/metacubex/mihomo/listener/sing" ) // SudokuServer describes a Sudoku inbound server configuration. // It is internal to the listener layer and mainly used for logging and wiring. type SudokuServer struct { Enable bool `json:"enable"` Listen string `json:"listen"` Key string `json:"key"` AEADMethod string `json:"aead-method,omitempty"` PaddingMin *int `json:"padding-min,omitempty"` PaddingMax *int `json:"padding-max,omitempty"` TableType string `json:"table-type,omitempty"` HandshakeTimeoutSecond *int `json:"handshake-timeout,omitempty"` EnablePureDownlink *bool `json:"enable-pure-downlink,omitempty"` CustomTable string `json:"custom-table,omitempty"` CustomTables []string `json:"custom-tables,omitempty"` DisableHTTPMask bool `json:"disable-http-mask,omitempty"` HTTPMaskMode string `json:"http-mask-mode,omitempty"` PathRoot string `json:"path-root,omitempty"` Fallback string `json:"fallback,omitempty"` // mihomo private extension (not the part of standard Sudoku protocol) MuxOption sing.MuxOption `json:"mux-option,omitempty"` } func (s SudokuServer) String() string { b, _ := json.Marshal(s) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/trojan.go ================================================ package config import ( "encoding/json" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" ) type TrojanUser struct { Username string Password string } type TrojanServer struct { Enable bool Listen string Users []TrojanUser WsPath string GrpcServiceName string Certificate string PrivateKey string ClientAuthType string ClientAuthCert string EchKey string RealityConfig reality.Config MuxOption sing.MuxOption TrojanSSOption TrojanSSOption } // TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 type TrojanSSOption struct { Enabled bool Method string Password string } func (t TrojanServer) String() string { b, _ := json.Marshal(t) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/trusttunnel.go ================================================ package config import ( "encoding/json" ) type TrustTunnelServer struct { Enable bool `yaml:"enable" json:"enable"` Listen string `yaml:"listen" json:"listen"` Users map[string]string `yaml:"users" json:"users,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` EchKey string `yaml:"ech-key" json:"ech-key"` Network []string `yaml:"network" json:"network,omitempty"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` CWND int `yaml:"cwnd" json:"cwnd,omitempty"` BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"` } func (t TrustTunnelServer) String() string { b, _ := json.Marshal(t) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/tuic.go ================================================ package config import ( "github.com/metacubex/mihomo/listener/sing" "encoding/json" ) type TuicServer struct { Enable bool `yaml:"enable" json:"enable"` Listen string `yaml:"listen" json:"listen"` Token []string `yaml:"token" json:"token,omitempty"` Users map[string]string `yaml:"users" json:"users,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` EchKey string `yaml:"ech-key" json:"ech-key"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"` MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` MaxDatagramFrameSize int `yaml:"max-datagram-frame-size" json:"max-datagram-frame-size,omitempty"` CWND int `yaml:"cwnd" json:"cwnd,omitempty"` BBRProfile string `yaml:"bbr-profile" json:"bbr-profile,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } func (t TuicServer) String() string { b, _ := json.Marshal(t) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/tun.go ================================================ package config import ( "net/netip" C "github.com/metacubex/mihomo/constant" "go4.org/netipx" "golang.org/x/exp/slices" ) type Tun struct { Enable bool `yaml:"enable" json:"enable"` Device string `yaml:"device" json:"device"` Stack C.TUNStack `yaml:"stack" json:"stack"` DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` AutoRoute bool `yaml:"auto-route" json:"auto-route"` AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` GSO bool `yaml:"gso" json:"gso,omitempty"` GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` IPRoute2TableIndex int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` IPRoute2RuleIndex int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` AutoRedirectIPRoute2FallbackRuleIndex int `yaml:"auto-redirect-iproute2-fallback-rule-index" json:"auto-redirect-iproute2-fallback-rule-index,omitempty"` LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"` StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` RouteExcludeAddress []netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` RouteExcludeAddressSet []string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` ExcludeSrcPort []uint16 `yaml:"exclude-src-port" json:"exclude-src-port,omitempty"` ExcludeSrcPortRange []string `yaml:"exclude-src-port-range" json:"exclude-src-port-range,omitempty"` ExcludeDstPort []uint16 `yaml:"exclude-dst-port" json:"exclude-dst-port,omitempty"` ExcludeDstPortRange []string `yaml:"exclude-dst-port-range" json:"exclude-dst-port-range,omitempty"` IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` IncludeMACAddress []string `yaml:"include-mac-address" json:"include-mac-address,omitempty"` ExcludeMACAddress []string `yaml:"exclude-mac-address" json:"exclude-mac-address,omitempty"` EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` // darwin special config RecvMsgX bool `yaml:"recvmsgx" json:"recvmsgx,omitempty"` SendMsgX bool `yaml:"sendmsgx" json:"sendmsgx,omitempty"` } func (t *Tun) Sort() { slices.Sort(t.DNSHijack) slices.SortFunc(t.Inet4Address, netipx.ComparePrefix) slices.SortFunc(t.Inet6Address, netipx.ComparePrefix) slices.SortFunc(t.RouteAddress, netipx.ComparePrefix) slices.Sort(t.RouteAddressSet) slices.SortFunc(t.RouteExcludeAddress, netipx.ComparePrefix) slices.Sort(t.RouteExcludeAddressSet) slices.Sort(t.IncludeInterface) slices.Sort(t.ExcludeInterface) slices.Sort(t.IncludeUID) slices.Sort(t.IncludeUIDRange) slices.Sort(t.ExcludeUID) slices.Sort(t.ExcludeUIDRange) slices.Sort(t.IncludeAndroidUser) slices.Sort(t.IncludePackage) slices.Sort(t.ExcludePackage) slices.Sort(t.IncludeMACAddress) slices.Sort(t.ExcludeMACAddress) slices.SortFunc(t.Inet4RouteAddress, netipx.ComparePrefix) slices.SortFunc(t.Inet6RouteAddress, netipx.ComparePrefix) slices.SortFunc(t.Inet4RouteExcludeAddress, netipx.ComparePrefix) slices.SortFunc(t.Inet6RouteExcludeAddress, netipx.ComparePrefix) } func (t *Tun) Equal(other Tun) bool { if t.Enable != other.Enable { return false } if t.Device != other.Device { return false } if t.Stack != other.Stack { return false } if !slices.Equal(t.DNSHijack, other.DNSHijack) { return false } if t.AutoRoute != other.AutoRoute { return false } if t.AutoDetectInterface != other.AutoDetectInterface { return false } if t.MTU != other.MTU { return false } if t.GSO != other.GSO { return false } if t.GSOMaxSize != other.GSOMaxSize { return false } if !slices.Equal(t.Inet4Address, other.Inet4Address) { return false } if !slices.Equal(t.Inet6Address, other.Inet6Address) { return false } if t.IPRoute2TableIndex != other.IPRoute2TableIndex { return false } if t.IPRoute2RuleIndex != other.IPRoute2RuleIndex { return false } if t.AutoRedirect != other.AutoRedirect { return false } if t.AutoRedirectInputMark != other.AutoRedirectInputMark { return false } if t.AutoRedirectOutputMark != other.AutoRedirectOutputMark { return false } if t.AutoRedirectIPRoute2FallbackRuleIndex != other.AutoRedirectIPRoute2FallbackRuleIndex { return false } if !slices.Equal(t.RouteAddress, other.RouteAddress) { return false } if t.StrictRoute != other.StrictRoute { return false } if !slices.Equal(t.RouteAddress, other.RouteAddress) { return false } if !slices.Equal(t.RouteAddressSet, other.RouteAddressSet) { return false } if !slices.Equal(t.RouteExcludeAddress, other.RouteExcludeAddress) { return false } if !slices.Equal(t.RouteExcludeAddressSet, other.RouteExcludeAddressSet) { return false } if !slices.Equal(t.IncludeInterface, other.IncludeInterface) { return false } if !slices.Equal(t.ExcludeInterface, other.ExcludeInterface) { return false } if !slices.Equal(t.IncludeUID, other.IncludeUID) { return false } if !slices.Equal(t.IncludeUIDRange, other.IncludeUIDRange) { return false } if !slices.Equal(t.ExcludeUID, other.ExcludeUID) { return false } if !slices.Equal(t.ExcludeUIDRange, other.ExcludeUIDRange) { return false } if !slices.Equal(t.IncludeAndroidUser, other.IncludeAndroidUser) { return false } if !slices.Equal(t.IncludePackage, other.IncludePackage) { return false } if !slices.Equal(t.ExcludePackage, other.ExcludePackage) { return false } if !slices.Equal(t.IncludeMACAddress, other.IncludeMACAddress) { return false } if !slices.Equal(t.ExcludeMACAddress, other.ExcludeMACAddress) { return false } if t.EndpointIndependentNat != other.EndpointIndependentNat { return false } if t.UDPTimeout != other.UDPTimeout { return false } if t.DisableICMPForwarding != other.DisableICMPForwarding { return false } if t.FileDescriptor != other.FileDescriptor { return false } if !slices.Equal(t.Inet4RouteAddress, other.Inet4RouteAddress) { return false } if !slices.Equal(t.Inet6RouteAddress, other.Inet6RouteAddress) { return false } if !slices.Equal(t.Inet4RouteExcludeAddress, other.Inet4RouteExcludeAddress) { return false } if !slices.Equal(t.Inet6RouteExcludeAddress, other.Inet6RouteExcludeAddress) { return false } if t.RecvMsgX != other.RecvMsgX { return false } if t.SendMsgX != other.SendMsgX { return false } return true } ================================================ FILE: core/Clash.Meta/listener/config/tunnel.go ================================================ package config import ( "fmt" "net" "strings" "github.com/samber/lo" ) type tunnel struct { Network []string `yaml:"network"` Address string `yaml:"address"` Target string `yaml:"target"` Proxy string `yaml:"proxy"` } type Tunnel tunnel // UnmarshalYAML implements yaml.Unmarshaler func (t *Tunnel) UnmarshalYAML(unmarshal func(any) error) error { var tp string if err := unmarshal(&tp); err != nil { var inner tunnel if err := unmarshal(&inner); err != nil { return err } *t = Tunnel(inner) return nil } // parse udp/tcp,address,target,proxy parts := lo.Map(strings.Split(tp, ","), func(s string, _ int) string { return strings.TrimSpace(s) }) if len(parts) != 3 && len(parts) != 4 { return fmt.Errorf("invalid tunnel config %s", tp) } network := strings.Split(parts[0], "/") // validate network for _, n := range network { switch n { case "tcp", "udp": default: return fmt.Errorf("invalid tunnel network %s", n) } } // validate address and target address := parts[1] target := parts[2] for _, addr := range []string{address, target} { if _, _, err := net.SplitHostPort(addr); err != nil { return fmt.Errorf("invalid tunnel target or address %s", addr) } } *t = Tunnel(tunnel{ Network: network, Address: address, Target: target, }) if len(parts) == 4 { t.Proxy = parts[3] } return nil } ================================================ FILE: core/Clash.Meta/listener/config/vless.go ================================================ package config import ( "encoding/json" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" ) type VlessUser struct { Username string UUID string Flow string } type VlessServer struct { Enable bool Listen string Users []VlessUser Decryption string WsPath string XHTTPConfig XHTTPConfig GrpcServiceName string Certificate string PrivateKey string ClientAuthType string ClientAuthCert string EchKey string RealityConfig reality.Config MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } type XHTTPConfig struct { Path string Host string Mode string XPaddingBytes string XPaddingObfsMode bool XPaddingKey string XPaddingHeader string XPaddingPlacement string XPaddingMethod string UplinkHTTPMethod string SessionPlacement string SessionKey string SeqPlacement string SeqKey string UplinkDataPlacement string UplinkDataKey string UplinkChunkSize string NoSSEHeader bool ScStreamUpServerSecs string ScMaxBufferedPosts string ScMaxEachPostBytes string } func (t VlessServer) String() string { b, _ := json.Marshal(t) return string(b) } ================================================ FILE: core/Clash.Meta/listener/config/vmess.go ================================================ package config import ( "encoding/json" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" ) type VmessUser struct { Username string UUID string AlterID int } type VmessServer struct { Enable bool Listen string Users []VmessUser WsPath string GrpcServiceName string Certificate string PrivateKey string ClientAuthType string ClientAuthCert string EchKey string RealityConfig reality.Config MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } func (t VmessServer) String() string { b, _ := json.Marshal(t) return string(b) } ================================================ FILE: core/Clash.Meta/listener/http/client.go ================================================ package http import ( "context" "errors" "net" "time" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/http" ) func newClient(srcConn net.Conn, tunnel C.Tunnel, additions []inbound.Addition) *http.Client { // additions using slice let caller can change its value (without size) after newClient return return &http.Client{ Transport: &http.Transport{ // from http.DefaultTransport MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DisableCompression: true, // prevents the Transport add "Accept-Encoding: gzip" DialContext: func(context context.Context, network, address string) (net.Conn, error) { if network != "tcp" && network != "tcp4" && network != "tcp6" { return nil, errors.New("unsupported network " + network) } dstAddr := socks5.ParseAddr(address) if dstAddr == nil { return nil, socks5.ErrAddressNotSupported } left, right := N.Pipe() go tunnel.HandleTCPConn(inbound.NewHTTP(dstAddr, srcConn, right, additions...)) return left, nil }, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } } ================================================ FILE: core/Clash.Meta/listener/http/hack.go ================================================ package http import ( "bufio" _ "unsafe" "github.com/metacubex/http" ) //go:linkname ReadRequest github.com/metacubex/http.readRequest func ReadRequest(b *bufio.Reader) (req *http.Request, err error) ================================================ FILE: core/Clash.Meta/listener/http/patch_android.go ================================================ //go:build android package http import "net" func (l *Listener) Listener() net.Listener { return l.listener } ================================================ FILE: core/Clash.Meta/listener/http/proxy.go ================================================ package http import ( "context" "fmt" "io" "net" "strings" "sync" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/http" ) type bodyWrapper struct { io.ReadCloser once sync.Once onHitEOF func() } func (b *bodyWrapper) Read(p []byte) (n int, err error) { n, err = b.ReadCloser.Read(p) if err == io.EOF && b.onHitEOF != nil { b.once.Do(b.onHitEOF) } return n, err } func HandleConn(c net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser inUserIdx := len(additions) - 1 client := newClient(c, tunnel, additions) defer client.CloseIdleConnections() ctx, cancel := context.WithCancel(context.Background()) defer cancel() peekMutex := sync.Mutex{} conn := N.NewBufferedConn(c) authenticator := store.Authenticator() trusted := authenticator == nil // disable authenticate if lru is nil lastUser := "" for { peekMutex.Lock() request, err := ReadRequest(conn.Reader()) peekMutex.Unlock() if err != nil { break } request.RemoteAddr = conn.RemoteAddr().String() keepAlive := strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive" resp, user := authenticate(request, authenticator) // always call authenticate function to get user if resp == nil { trusted = true } additions[inUserIdx] = inbound.WithInUser(user) if trusted { if request.Method == http.MethodConnect { // Manual writing to support CONNECT for http 1.0 (workaround for uplay client) if _, err = fmt.Fprintf(conn, "HTTP/%d.%d %03d %s\r\n\r\n", request.ProtoMajor, request.ProtoMinor, http.StatusOK, "Connection established"); err != nil { break // close connection } tunnel.HandleTCPConn(inbound.NewHTTPS(request, conn, additions...)) return // hijack connection } host := request.Header.Get("Host") if host != "" { request.Host = host } request.RequestURI = "" if isUpgradeRequest(request) { handleUpgrade(conn, request, tunnel, additions...) return // hijack connection } // ensure there is a client with correct additions // when the authenticated user changed, outbound client should close idle connections if user != lastUser { client.CloseIdleConnections() lastUser = user } removeHopByHopHeaders(request.Header) removeExtraHTTPHostPort(request) if request.URL.Scheme == "" || request.URL.Host == "" { resp = responseWith(request, http.StatusBadRequest) } else { request = request.WithContext(ctx) startBackgroundRead := func() { go func() { peekMutex.Lock() defer peekMutex.Unlock() _, err := conn.Peek(1) if err != nil { cancel() } }() } if request.Body == nil || request.Body == http.NoBody { startBackgroundRead() } else { request.Body = &bodyWrapper{ReadCloser: request.Body, onHitEOF: startBackgroundRead} } resp, err = client.Do(request) if err != nil { resp = responseWith(request, http.StatusBadGateway) } } removeHopByHopHeaders(resp.Header) } if !keepAlive { resp.Close = true // close connection if keep-alive is not set } if keepAlive && resp.ContentLength > 0 { resp.Close = false // don't need to close connection if content length is positive numbers } if !resp.Close { resp.Header.Set("Proxy-Connection", "keep-alive") resp.Header.Set("Connection", "keep-alive") resp.Header.Set("Keep-Alive", "timeout=4") } err = resp.Write(conn) if err != nil || resp.Close { break // close connection } } _ = conn.Close() } func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) { credential := parseBasicProxyAuthorization(request) if credential == "" && authenticator != nil { resp = responseWith(request, http.StatusProxyAuthRequired) resp.Header.Set("Proxy-Authenticate", "Basic") return } user, pass, err := decodeBasicProxyAuthorization(credential) authed := authenticator == nil || (err == nil && authenticator.Verify(user, pass)) if !authed { log.Infoln("Auth failed from %s", request.RemoteAddr) return responseWith(request, http.StatusForbidden), user } log.Debugln("Auth success from %s -> %s", request.RemoteAddr, user) return } func responseWith(request *http.Request, statusCode int) *http.Response { return &http.Response{ StatusCode: statusCode, Status: http.StatusText(statusCode), Proto: request.Proto, ProtoMajor: request.ProtoMajor, ProtoMinor: request.ProtoMinor, Header: http.Header{}, } } ================================================ FILE: core/Clash.Meta/listener/http/server.go ================================================ package http import ( "errors" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/tls" ) type Listener struct { listener net.Listener addr string closed bool } // RawAddress implements C.Listener func (l *Listener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *Listener) Address() string { return l.listener.Addr().String() } // Close implements C.Listener func (l *Listener) Close() error { l.closed = true return l.listener.Close() } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: authStore.Default}, tunnel, additions...) } // NewWithAuthenticate // never change type traits because it's used in CMFA func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { store := authStore.Default if !authenticate { store = authStore.Nil } return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: store}, tunnel, additions...) } func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { isDefault := false if len(additions) == 0 { isDefault = true additions = []inbound.Addition{ inbound.WithInName("DEFAULT-HTTP"), inbound.WithSpecialRules(""), } } l, err := inbound.Listen("tcp", config.Listen) if err != nil { return nil, err } tlsConfig := &tls.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { return nil, errors.New("client-auth is unavailable in reality") } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err } } if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } hl := &Listener{ listener: l, addr: config.Listen, } go func() { for { conn, err := hl.listener.Accept() if err != nil { if hl.closed { break } continue } store := config.AuthStore if isDefault || store == authStore.Default { // only apply on default listener if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { _ = conn.Close() continue } if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) { store = authStore.Nil } } go HandleConn(conn, tunnel, store, additions...) } }() return hl, nil } ================================================ FILE: core/Clash.Meta/listener/http/upgrade.go ================================================ package http import ( "context" "net" "strings" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/http" "github.com/metacubex/tls" ) func isUpgradeRequest(req *http.Request) bool { for _, header := range req.Header["Connection"] { for _, elm := range strings.Split(header, ",") { if strings.EqualFold(strings.TrimSpace(elm), "Upgrade") { return true } } } return false } func handleUpgrade(conn net.Conn, request *http.Request, tunnel C.Tunnel, additions ...inbound.Addition) { defer conn.Close() removeProxyHeaders(request.Header) removeExtraHTTPHostPort(request) address := request.Host if _, _, err := net.SplitHostPort(address); err != nil { address = net.JoinHostPort(address, "80") } dstAddr := socks5.ParseAddr(address) if dstAddr == nil { return } left, right := N.Pipe() go tunnel.HandleTCPConn(inbound.NewHTTP(dstAddr, conn, right, additions...)) var bufferedLeft *N.BufferedConn if request.TLS != nil { tlsConn := tls.Client(left, &tls.Config{ ServerName: request.URL.Hostname(), }) ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel() if tlsConn.HandshakeContext(ctx) != nil { _ = left.Close() return } bufferedLeft = N.NewBufferedConn(tlsConn) } else { bufferedLeft = N.NewBufferedConn(left) } defer func() { _ = bufferedLeft.Close() }() err := request.Write(bufferedLeft) if err != nil { return } resp, err := http.ReadResponse(bufferedLeft.Reader(), request) if err != nil { return } removeProxyHeaders(resp.Header) err = resp.Write(conn) if err != nil { return } if resp.StatusCode == http.StatusSwitchingProtocols { N.Relay(bufferedLeft, conn) } } ================================================ FILE: core/Clash.Meta/listener/http/utils.go ================================================ package http import ( "encoding/base64" "errors" "net" "net/netip" "strings" "github.com/metacubex/http" ) // removeHopByHopHeaders remove Proxy-* headers func removeProxyHeaders(header http.Header) { header.Del("Proxy-Connection") header.Del("Proxy-Authenticate") header.Del("Proxy-Authorization") } // removeHopByHopHeaders remove hop-by-hop header func removeHopByHopHeaders(header http.Header) { // Strip hop-by-hop header based on RFC: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 // https://www.mnot.net/blog/2011/07/11/what_proxies_must_do removeProxyHeaders(header) header.Del("TE") header.Del("Trailers") header.Del("Transfer-Encoding") header.Del("Upgrade") connections := header.Get("Connection") header.Del("Connection") if len(connections) == 0 { return } for _, h := range strings.Split(connections, ",") { header.Del(strings.TrimSpace(h)) } } // removeExtraHTTPHostPort remove extra host port (example.com:80 --> example.com) // It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com) func removeExtraHTTPHostPort(req *http.Request) { host := req.Host if host == "" { host = req.URL.Host } if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" { host = pHost if ip, err := netip.ParseAddr(pHost); err == nil && ip.Is6() { // RFC 2617 Sec 3.2.2, for IPv6 literal // addresses the Host header needs to follow the RFC 2732 grammar for "host" host = "[" + host + "]" } } req.Host = host req.URL.Host = host } // parseBasicProxyAuthorization parse header Proxy-Authorization and return base64-encoded credential func parseBasicProxyAuthorization(request *http.Request) string { value := request.Header.Get("Proxy-Authorization") const prefix = "Basic " // According to RFC7617, the scheme should be case-insensitive. // In practice, some implementations do use different case styles, causing authentication to fail // eg: https://github.com/algesten/ureq/blob/381fd42cfcb80a5eb709d64860aa0ae726f17b8e/src/unversioned/transport/connect.rs#L118 if len(value) < len(prefix) || !strings.EqualFold(value[:len(prefix)], prefix) { return "" } return value[6:] // value[len("Basic "):] } // decodeBasicProxyAuthorization decode base64-encoded credential func decodeBasicProxyAuthorization(credential string) (string, string, error) { plain, err := base64.StdEncoding.DecodeString(credential) if err != nil { return "", "", err } user, pass, found := strings.Cut(string(plain), ":") if !found { return "", "", errors.New("invalid login") } return user, pass, nil } ================================================ FILE: core/Clash.Meta/listener/inbound/anytls.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/anytls" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/log" ) type AnyTLSOption struct { BaseOption Users map[string]string `inbound:"users,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` PaddingScheme string `inbound:"padding-scheme,omitempty"` } func (o AnyTLSOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type AnyTLS struct { *Base config *AnyTLSOption l C.MultiAddrListener vs LC.AnyTLSServer } func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &AnyTLS{ Base: base, config: options, vs: LC.AnyTLSServer{ Enable: true, Listen: base.RawAddress(), Users: options.Users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, PaddingScheme: options.PaddingScheme, }, }, nil } // Config implements constant.InboundListener func (v *AnyTLS) Config() C.InboundConfig { return v.config } // Address implements constant.InboundListener func (v *AnyTLS) Address() string { var addrList []string if v.l != nil { for _, addr := range v.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (v *AnyTLS) Listen(tunnel C.Tunnel) error { var err error v.l, err = anytls.New(v.vs, tunnel, v.Additions()...) if err != nil { return err } log.Infoln("AnyTLS[%s] proxy listening at: %s", v.Name(), v.Address()) return nil } // Close implements constant.InboundListener func (v *AnyTLS) Close() error { return v.l.Close() } var _ C.InboundListener = (*AnyTLS)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/anytls_test.go ================================================ package inbound_test import ( "net/netip" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/stretchr/testify/assert" ) func testInboundAnyTLS(t *testing.T, inboundOptions inbound.AnyTLSOption, outboundOptions outbound.AnyTLSOption) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "anytls_inbound", Listen: "127.0.0.1", Port: "0", } inboundOptions.Users = map[string]string{"test": userUUID} in, err := inbound.NewAnyTLS(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "anytls_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewAnyTLS(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) } func TestInboundAnyTLS_TLS(t *testing.T) { inboundOptions := inbound.AnyTLSOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, } outboundOptions := outbound.AnyTLSOption{ Fingerprint: tlsFingerprint, } testInboundAnyTLS(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundAnyTLS(t, inboundOptions, outboundOptions) }) t.Run("mTLS", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey testInboundAnyTLS(t, inboundOptions, outboundOptions) }) t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundAnyTLS(t, inboundOptions, outboundOptions) }) } ================================================ FILE: core/Clash.Meta/listener/inbound/auth.go ================================================ package inbound import ( "github.com/metacubex/mihomo/component/auth" authStore "github.com/metacubex/mihomo/listener/auth" ) type AuthUser struct { Username string `inbound:"username"` Password string `inbound:"password"` } type AuthUsers []AuthUser func (a AuthUsers) GetAuthStore() auth.AuthStore { if a != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array if len(a) == 0 { return authStore.Nil } users := make([]auth.AuthUser, len(a)) for i, user := range a { users[i] = auth.AuthUser{ User: user.Username, Pass: user.Password, } } authenticator := auth.NewAuthenticator(users) return authStore.NewAuthStore(authenticator) } return authStore.Default } ================================================ FILE: core/Clash.Meta/listener/inbound/base.go ================================================ package inbound import ( "encoding/json" "net" "net/netip" "strconv" "strings" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" ) type Base struct { config *BaseOption name string specialRules string listenAddr netip.Addr ports utils.IntRanges[uint16] } func NewBase(options *BaseOption) (*Base, error) { if options.Listen == "" { options.Listen = "0.0.0.0" } addr, err := netip.ParseAddr(options.Listen) if err != nil { return nil, err } ports, err := utils.NewUnsignedRanges[uint16](options.Port) if err != nil { return nil, err } return &Base{ name: options.Name(), listenAddr: addr, specialRules: options.SpecialRules, ports: ports, config: options, }, nil } // Config implements constant.InboundListener func (b *Base) Config() C.InboundConfig { return b.config } // Address implements constant.InboundListener func (b *Base) Address() string { return b.RawAddress() } // Close implements constant.InboundListener func (*Base) Close() error { return nil } // Name implements constant.InboundListener func (b *Base) Name() string { return b.name } // RawAddress implements constant.InboundListener func (b *Base) RawAddress() string { if len(b.ports) == 0 { return net.JoinHostPort(b.listenAddr.String(), "0") } address := make([]string, 0, len(b.ports)) b.ports.Range(func(port uint16) bool { address = append(address, net.JoinHostPort(b.listenAddr.String(), strconv.Itoa(int(port)))) return true }) return strings.Join(address, ",") } // Listen implements constant.InboundListener func (*Base) Listen(tunnel C.Tunnel) error { return nil } func (b *Base) Additions() []inbound.Addition { return b.config.Additions() } var _ C.InboundListener = (*Base)(nil) type BaseOption struct { NameStr string `inbound:"name"` Listen string `inbound:"listen,omitempty"` Port string `inbound:"port,omitempty"` SpecialRules string `inbound:"rule,omitempty"` SpecialProxy string `inbound:"proxy,omitempty"` } func (o BaseOption) Name() string { return o.NameStr } func (o BaseOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } func (o BaseOption) Additions() []inbound.Addition { return []inbound.Addition{ inbound.WithInName(o.NameStr), inbound.WithSpecialRules(o.SpecialRules), inbound.WithSpecialProxy(o.SpecialProxy), } } var _ C.InboundConfig = (*BaseOption)(nil) func optionToString(option any) string { str, _ := json.Marshal(option) return string(str) } ================================================ FILE: core/Clash.Meta/listener/inbound/common_test.go ================================================ package inbound_test import ( "bytes" "context" "crypto/rand" "encoding/base64" "fmt" "io" "net" "net/netip" "os" "strconv" "sync" "sync/atomic" "testing" "time" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/component/generator" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/chi" "github.com/metacubex/chi/render" "github.com/metacubex/http" "github.com/metacubex/tls" "github.com/stretchr/testify/assert" ) var httpPath = "/inbound_test" var httpData = make([]byte, 2*pool.RelayBufferSize) var remoteAddr = netip.MustParseAddr("1.2.3.4") var userUUID = utils.NewUUIDV4().String() var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) var tlsAuthCertificate, tlsAuthPrivateKey, _, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey)) var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}} var tlsClientConfig, _ = ca.GetTLSConfig(ca.Option{Fingerprint: tlsFingerprint}) var realityPrivateKey, realityPublickey string var realityDest = "itunes.apple.com" var realityShortid = "10f897e26c4b9478" var realityRealDial = false var echPublicSni = "public.sni" var echConfigBase64, echKeyPem, _ = ech.GenECHConfig(echPublicSni) func init() { rand.Read(httpData) privateKey, err := generator.GenX25519PrivateKey() if err != nil { panic(err) } realityPrivateKey = base64.RawURLEncoding.EncodeToString(privateKey.Bytes()) realityPublickey = base64.RawURLEncoding.EncodeToString(privateKey.PublicKey().Bytes()) } type TestDialer struct { dialer C.Dialer ctx context.Context } func (t *TestDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { start: conn, err := t.dialer.DialContext(ctx, network, address) if err != nil && ctx.Err() == nil && t.ctx.Err() == nil { // We are conducting tests locally, and they shouldn't fail. // However, a large number of requests in a short period during concurrent testing can exhaust system ports. // This can lead to various errors such as WSAECONNREFUSED and WSAENOBUFS. // So we just retry if the context is not canceled. goto start } return conn, err } func (t *TestDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { return t.dialer.ListenPacket(ctx, network, address, rAddrPort) } var _ C.Dialer = (*TestDialer)(nil) type TestTunnel struct { HandleTCPConnFn func(conn net.Conn, metadata *C.Metadata) HandleUDPPacketFn func(packet C.UDPPacket, metadata *C.Metadata) NatTableFn func() C.NatTable CloseFn func() error DoSequentialTestFn func(t *testing.T, proxy C.ProxyAdapter) DoConcurrentTestFn func(t *testing.T, proxy C.ProxyAdapter) NewDialerFn func() C.Dialer } func (tt *TestTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { tt.HandleTCPConnFn(conn, metadata) } func (tt *TestTunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) { tt.HandleUDPPacketFn(packet, metadata) } func (tt *TestTunnel) NatTable() C.NatTable { return tt.NatTableFn() } func (tt *TestTunnel) Close() error { return tt.CloseFn() } func (tt *TestTunnel) DoTest(t *testing.T, proxy C.ProxyAdapter) { tt.DoSequentialTestFn(t, proxy) tt.DoConcurrentTestFn(t, proxy) } func (tt *TestTunnel) DoSequentialTest(t *testing.T, proxy C.ProxyAdapter) { tt.DoSequentialTestFn(t, proxy) } func (tt *TestTunnel) DoConcurrentTest(t *testing.T, proxy C.ProxyAdapter) { tt.DoConcurrentTestFn(t, proxy) } func (tt *TestTunnel) NewDialer() C.Dialer { return tt.NewDialerFn() } type TestTunnelListener struct { ch chan net.Conn ctx context.Context cancel context.CancelFunc addr net.Addr } func (t *TestTunnelListener) Accept() (net.Conn, error) { select { case conn, ok := <-t.ch: if !ok { return nil, net.ErrClosed } return conn, nil case <-t.ctx.Done(): return nil, t.ctx.Err() } } func (t *TestTunnelListener) Close() error { t.cancel() return nil } func (t *TestTunnelListener) Addr() net.Addr { return t.addr } type WaitCloseConn struct { net.Conn ch chan struct{} once sync.Once } func (c *WaitCloseConn) Close() error { err := c.Conn.Close() c.once.Do(func() { close(c.ch) }) return err } var _ C.Tunnel = (*TestTunnel)(nil) var _ net.Listener = (*TestTunnelListener)(nil) func NewHttpTestTunnel() *TestTunnel { ctx, cancel := context.WithCancel(context.Background()) ln := &TestTunnelListener{ch: make(chan net.Conn), ctx: ctx, cancel: cancel, addr: net.TCPAddrFromAddrPort(netip.AddrPortFrom(remoteAddr, 0))} r := chi.NewRouter() r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() size, err := strconv.Atoi(query.Get("size")) if err != nil { render.Status(r, http.StatusBadRequest) render.PlainText(w, r, err.Error()) return } io.Copy(io.Discard, r.Body) render.Data(w, r, httpData[:size]) }) //h2Server := &http.Http2Server{} server := http.Server{Handler: r} //_ = http.Http2ConfigureServer(&server, h2Server) go server.Serve(ln) testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string, size int) { req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s?size=%d", proto, remoteAddr, httpPath, size), bytes.NewReader(httpData[:size])) if !assert.NoError(t, err) { return } req = req.WithContext(ctx) var dstPort uint16 = 80 if proto == "https" { dstPort = 443 } metadata := &C.Metadata{ NetWork: C.TCP, DstIP: remoteAddr, DstPort: dstPort, } instance, err := proxy.DialContext(ctx, metadata) if !assert.NoError(t, err) { return } defer instance.Close() var dialNum atomic.Int32 var extraConns []net.Conn var extraConnsMu sync.Mutex defer func() { extraConnsMu.Lock() extraConns := append([]net.Conn{}, extraConns...) // clone conn list avoid race condition extraConnsMu.Unlock() for _, conn := range extraConns { _ = conn.Close() } }() transport := &http.Transport{ DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { dianNum := dialNum.Add(1) if dianNum == 1 { // first dial, return instance return instance, nil } t.Logf("transport dial time %d more than once in: %s", dianNum, t.Name()) conn, err := proxy.DialContext(ctx, metadata) if err != nil { return nil, err } extraConnsMu.Lock() extraConns = append(extraConns, conn) extraConnsMu.Unlock() return conn, nil }, //// from http.DefaultTransport //MaxIdleConns: 100, //IdleConnTimeout: 90 * time.Second, //TLSHandshakeTimeout: 10 * time.Second, //ExpectContinueTimeout: 1 * time.Second, // for our self-signed cert TLSClientConfig: tlsClientConfig.Clone(), // open http2 ForceAttemptHTTP2: true, } client := http.Client{ Timeout: 60 * time.Second, Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } defer client.CloseIdleConnections() resp, err := client.Do(req) if !assert.NoError(t, err) { return } defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) if proto == "https" { // ensure server using http2 assert.Equal(t, 2, resp.ProtoMajor) } data, err := io.ReadAll(resp.Body) if !assert.NoError(t, err) { return } assert.Equal(t, httpData[:size], data) } sequentialTestFn := func(t *testing.T, proxy C.ProxyAdapter) { // Sequential testing for debugging t.Run("Sequential", func(t *testing.T) { testFn(t, proxy, "http", len(httpData)) testFn(t, proxy, "https", len(httpData)) }) } concurrentTestFn := func(t *testing.T, proxy C.ProxyAdapter) { // Concurrent testing to detect stress t.Run("Concurrent", func(t *testing.T) { if skip, _ := strconv.ParseBool(os.Getenv("SKIP_CONCURRENT_TEST")); skip { t.Skip("skip concurrent test") } wg := sync.WaitGroup{} num := len(httpData) / 1024 for i := 1; i <= num; i++ { i := i wg.Add(1) go func() { testFn(t, proxy, "https", i*1024) defer wg.Done() }() } for i := 1; i <= num; i++ { i := i wg.Add(1) go func() { testFn(t, proxy, "http", i*1024) defer wg.Done() }() } wg.Wait() }) } tunnel := &TestTunnel{ HandleTCPConnFn: func(conn net.Conn, metadata *C.Metadata) { defer conn.Close() if metadata.DstIP != remoteAddr && metadata.Host != realityDest { return // not match, just return } c := &WaitCloseConn{ Conn: conn, ch: make(chan struct{}), } if metadata.DstPort == 443 { tlsConn := tls.Server(c, tlsConfig) if metadata.Host == realityDest { // ignore the tls handshake error for realityDest if realityRealDial { rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress()) if err != nil { panic(err) } N.Relay(rconn, conn) return } } //ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) //defer cancel() if err := tlsConn.HandshakeContext(ctx); err != nil { return } //if tlsConn.ConnectionState().NegotiatedProtocol == http.Http2NextProtoTLS { // h2Server.ServeConn(tlsConn, &http.Http2ServeConnOpts{BaseConfig: &server}) //} else { // ln.ch <- tlsConn //} ln.ch <- tlsConn } else { ln.ch <- c } <-c.ch }, CloseFn: ln.Close, DoSequentialTestFn: sequentialTestFn, DoConcurrentTestFn: concurrentTestFn, NewDialerFn: func() C.Dialer { return &TestDialer{dialer: dialer.NewDialer(), ctx: ctx} }, } return tunnel } ================================================ FILE: core/Clash.Meta/listener/inbound/http.go ================================================ package inbound import ( "errors" "fmt" "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/log" ) type HTTPOption struct { BaseOption Users AuthUsers `inbound:"users,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o HTTPOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type HTTP struct { *Base config *HTTPOption l []*http.Listener } func NewHTTP(options *HTTPOption) (*HTTP, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &HTTP{ Base: base, config: options, }, nil } // Config implements constant.InboundListener func (h *HTTP) Config() C.InboundConfig { return h.config } // Address implements constant.InboundListener func (h *HTTP) Address() string { var addrList []string for _, l := range h.l { addrList = append(addrList, l.Address()) } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (h *HTTP) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(h.RawAddress(), ",") { l, err := http.NewWithConfig( LC.AuthServer{ Enable: true, Listen: addr, AuthStore: h.config.Users.GetAuthStore(), Certificate: h.config.Certificate, PrivateKey: h.config.PrivateKey, ClientAuthType: h.config.ClientAuthType, ClientAuthCert: h.config.ClientAuthCert, EchKey: h.config.EchKey, RealityConfig: h.config.RealityConfig.Build(), }, tunnel, h.Additions()..., ) if err != nil { return err } h.l = append(h.l, l) } log.Infoln("HTTP[%s] proxy listening at: %s", h.Name(), h.Address()) return nil } // Close implements constant.InboundListener func (h *HTTP) Close() error { var errs []error for _, l := range h.l { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } var _ C.InboundListener = (*HTTP)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/hysteria2.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing_hysteria2" "github.com/metacubex/mihomo/log" ) type Hysteria2Option struct { BaseOption Users map[string]string `inbound:"users,omitempty"` Obfs string `inbound:"obfs,omitempty"` ObfsPassword string `inbound:"obfs-password,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` ALPN []string `inbound:"alpn,omitempty"` Up string `inbound:"up,omitempty"` Down string `inbound:"down,omitempty"` IgnoreClientBandwidth bool `inbound:"ignore-client-bandwidth,omitempty"` Masquerade string `inbound:"masquerade,omitempty"` CWND int `inbound:"cwnd,omitempty"` BBRProfile string `inbound:"bbr-profile,omitempty"` UdpMTU int `inbound:"udp-mtu,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` // quic-go special config InitialStreamReceiveWindow uint64 `inbound:"initial-stream-receive-window,omitempty"` MaxStreamReceiveWindow uint64 `inbound:"max-stream-receive-window,omitempty"` InitialConnectionReceiveWindow uint64 `inbound:"initial-connection-receive-window,omitempty"` MaxConnectionReceiveWindow uint64 `inbound:"max-connection-receive-window,omitempty"` } func (o Hysteria2Option) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Hysteria2 struct { *Base config *Hysteria2Option l *sing_hysteria2.Listener ts LC.Hysteria2Server } func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &Hysteria2{ Base: base, config: options, ts: LC.Hysteria2Server{ Enable: true, Listen: base.RawAddress(), Users: options.Users, Obfs: options.Obfs, ObfsPassword: options.ObfsPassword, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, MaxIdleTime: options.MaxIdleTime, ALPN: options.ALPN, Up: options.Up, Down: options.Down, IgnoreClientBandwidth: options.IgnoreClientBandwidth, Masquerade: options.Masquerade, CWND: options.CWND, BBRProfile: options.BBRProfile, UdpMTU: options.UdpMTU, MuxOption: options.MuxOption.Build(), // quic-go special config InitialStreamReceiveWindow: options.InitialStreamReceiveWindow, MaxStreamReceiveWindow: options.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: options.InitialConnectionReceiveWindow, MaxConnectionReceiveWindow: options.MaxConnectionReceiveWindow, }, }, nil } // Config implements constant.InboundListener func (t *Hysteria2) Config() C.InboundConfig { return t.config } // Address implements constant.InboundListener func (t *Hysteria2) Address() string { var addrList []string if t.l != nil { for _, addr := range t.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (t *Hysteria2) Listen(tunnel C.Tunnel) error { var err error t.l, err = sing_hysteria2.New(t.ts, tunnel, t.Additions()...) if err != nil { return err } log.Infoln("Hysteria2[%s] proxy listening at: %s", t.Name(), t.Address()) return nil } // Close implements constant.InboundListener func (t *Hysteria2) Close() error { return t.l.Close() } var _ C.InboundListener = (*Hysteria2)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/hysteria2_test.go ================================================ package inbound_test import ( "net/netip" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/stretchr/testify/assert" ) func testInboundHysteria2(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions outbound.Hysteria2Option) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "hysteria2_inbound", Listen: "127.0.0.1", Port: "0", } inboundOptions.Users = map[string]string{"test": userUUID} in, err := inbound.NewHysteria2(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "hysteria2_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewHysteria2(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) } func testInboundHysteria2TLS(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions outbound.Hysteria2Option) { testInboundHysteria2(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundHysteria2(t, inboundOptions, outboundOptions) }) t.Run("mTLS", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey testInboundHysteria2(t, inboundOptions, outboundOptions) }) t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundHysteria2(t, inboundOptions, outboundOptions) }) } func TestInboundHysteria2_TLS(t *testing.T) { inboundOptions := inbound.Hysteria2Option{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, } outboundOptions := outbound.Hysteria2Option{ Fingerprint: tlsFingerprint, } testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } func TestInboundHysteria2_Salamander(t *testing.T) { inboundOptions := inbound.Hysteria2Option{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, Obfs: "salamander", ObfsPassword: userUUID, } outboundOptions := outbound.Hysteria2Option{ Fingerprint: tlsFingerprint, Obfs: "salamander", ObfsPassword: userUUID, } testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } func TestInboundHysteria2_Brutal(t *testing.T) { inboundOptions := inbound.Hysteria2Option{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, Up: "30 Mbps", Down: "200 Mbps", } outboundOptions := outbound.Hysteria2Option{ Fingerprint: tlsFingerprint, Up: "30 Mbps", Down: "200 Mbps", } testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } ================================================ FILE: core/Clash.Meta/listener/inbound/kcptun.go ================================================ package inbound import ( LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/transport/kcptun" ) type KcpTun struct { Enable bool `inbound:"enable"` Key string `inbound:"key,omitempty"` Crypt string `inbound:"crypt,omitempty"` Mode string `inbound:"mode,omitempty"` Conn int `inbound:"conn,omitempty"` AutoExpire int `inbound:"autoexpire,omitempty"` ScavengeTTL int `inbound:"scavengettl,omitempty"` MTU int `inbound:"mtu,omitempty"` RateLimit int `inbound:"ratelimit,omitempty"` SndWnd int `inbound:"sndwnd,omitempty"` RcvWnd int `inbound:"rcvwnd,omitempty"` DataShard int `inbound:"datashard,omitempty"` ParityShard int `inbound:"parityshard,omitempty"` DSCP int `inbound:"dscp,omitempty"` NoComp bool `inbound:"nocomp,omitempty"` AckNodelay bool `inbound:"acknodelay,omitempty"` NoDelay int `inbound:"nodelay,omitempty"` Interval int `inbound:"interval,omitempty"` Resend int `inbound:"resend,omitempty"` NoCongestion int `inbound:"nc,omitempty"` SockBuf int `inbound:"sockbuf,omitempty"` SmuxVer int `inbound:"smuxver,omitempty"` SmuxBuf int `inbound:"smuxbuf,omitempty"` FrameSize int `inbound:"framesize,omitempty"` StreamBuf int `inbound:"streambuf,omitempty"` KeepAlive int `inbound:"keepalive,omitempty"` } func (c KcpTun) Build() LC.KcpTun { return LC.KcpTun{ Enable: c.Enable, Config: kcptun.Config{ Key: c.Key, Crypt: c.Crypt, Mode: c.Mode, Conn: c.Conn, AutoExpire: c.AutoExpire, ScavengeTTL: c.ScavengeTTL, MTU: c.MTU, RateLimit: c.RateLimit, SndWnd: c.SndWnd, RcvWnd: c.RcvWnd, DataShard: c.DataShard, ParityShard: c.ParityShard, DSCP: c.DSCP, NoComp: c.NoComp, AckNodelay: c.AckNodelay, NoDelay: c.NoDelay, Interval: c.Interval, Resend: c.Resend, NoCongestion: c.NoCongestion, SockBuf: c.SockBuf, SmuxVer: c.SmuxVer, SmuxBuf: c.SmuxBuf, FrameSize: c.FrameSize, StreamBuf: c.StreamBuf, KeepAlive: c.KeepAlive, }, } } ================================================ FILE: core/Clash.Meta/listener/inbound/mieru.go ================================================ package inbound import ( "context" "fmt" "net" "sync" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/mieru" "github.com/metacubex/mihomo/log" "google.golang.org/protobuf/proto" mieruserver "github.com/enfein/mieru/v3/apis/server" mierutp "github.com/enfein/mieru/v3/apis/trafficpattern" mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb" ) type Mieru struct { *Base option *MieruOption server mieruserver.Server mu sync.Mutex } type MieruOption struct { BaseOption Transport string `inbound:"transport"` Users map[string]string `inbound:"users"` TrafficPattern string `inbound:"traffic-pattern,omitempty"` UserHintIsMandatory bool `inbound:"user-hint-is-mandatory,omitempty"` } type mieruListenerFactory struct{} func (mieruListenerFactory) Listen(ctx context.Context, network, address string) (net.Listener, error) { return inbound.ListenContext(ctx, network, address) } func (mieruListenerFactory) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) { return inbound.ListenPacketContext(ctx, network, address) } func NewMieru(option *MieruOption) (*Mieru, error) { base, err := NewBase(&option.BaseOption) if err != nil { return nil, err } config, err := buildMieruServerConfig(option, base.ports) if err != nil { return nil, fmt.Errorf("failed to build mieru server config: %w", err) } s := mieruserver.NewServer() if err := s.Store(config); err != nil { return nil, fmt.Errorf("failed to store mieru server config: %w", err) } // Server is started lazily when Listen() is called for the first time. return &Mieru{ Base: base, option: option, server: s, }, nil } func (m *Mieru) Config() C.InboundConfig { return m.option } func (m *Mieru) Listen(tunnel C.Tunnel) error { m.mu.Lock() defer m.mu.Unlock() if !m.server.IsRunning() { if err := m.server.Start(); err != nil { return fmt.Errorf("failed to start mieru server: %w", err) } } additions := m.config.Additions() if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-MIERU"), inbound.WithSpecialRules(""), } } go func() { for { c, req, err := m.server.Accept() if err != nil { if !m.server.IsRunning() { break } else { continue } } go mieru.Handle(c, tunnel, req, additions...) } }() log.Infoln("Mieru[%s] proxy listening at: %s", m.Name(), m.Address()) return nil } func (m *Mieru) Close() error { m.mu.Lock() defer m.mu.Unlock() if m.server.IsRunning() { return m.server.Stop() } return nil } var _ C.InboundListener = (*Mieru)(nil) func (o MieruOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } func buildMieruServerConfig(option *MieruOption, ports utils.IntRanges[uint16]) (*mieruserver.ServerConfig, error) { if err := validateMieruOption(option); err != nil { return nil, fmt.Errorf("failed to validate mieru option: %w", err) } if len(ports) == 0 { return nil, fmt.Errorf("port is not set") } var transportProtocol *mierupb.TransportProtocol switch option.Transport { case "TCP": transportProtocol = mierupb.TransportProtocol_TCP.Enum() case "UDP": transportProtocol = mierupb.TransportProtocol_UDP.Enum() } var portBindings []*mierupb.PortBinding for _, portRange := range ports { if portRange.Start() == portRange.End() { portBindings = append(portBindings, &mierupb.PortBinding{ Port: proto.Int32(int32(portRange.Start())), Protocol: transportProtocol, }) } else { portBindings = append(portBindings, &mierupb.PortBinding{ PortRange: proto.String(fmt.Sprintf("%d-%d", portRange.Start(), portRange.End())), Protocol: transportProtocol, }) } } var users []*mierupb.User for username, password := range option.Users { users = append(users, &mierupb.User{ Name: proto.String(username), Password: proto.String(password), }) } var trafficPattern *mierupb.TrafficPattern trafficPattern, _ = mierutp.Decode(option.TrafficPattern) var advancedSettings *mierupb.ServerAdvancedSettings if option.UserHintIsMandatory { advancedSettings = &mierupb.ServerAdvancedSettings{ UserHintIsMandatory: proto.Bool(true), } } return &mieruserver.ServerConfig{ Config: &mierupb.ServerConfig{ PortBindings: portBindings, Users: users, TrafficPattern: trafficPattern, AdvancedSettings: advancedSettings, }, StreamListenerFactory: mieruListenerFactory{}, PacketListenerFactory: mieruListenerFactory{}, }, nil } func validateMieruOption(option *MieruOption) error { if option.Transport != "TCP" && option.Transport != "UDP" { return fmt.Errorf("transport must be TCP or UDP") } if len(option.Users) == 0 { return fmt.Errorf("users is empty") } for username, password := range option.Users { if username == "" { return fmt.Errorf("username is empty") } if password == "" { return fmt.Errorf("password is empty") } } if option.TrafficPattern != "" { trafficPattern, err := mierutp.Decode(option.TrafficPattern) if err != nil { return fmt.Errorf("failed to decode traffic pattern %q: %w", option.TrafficPattern, err) } if err := mierutp.Validate(trafficPattern); err != nil { return fmt.Errorf("invalid traffic pattern %q: %w", option.TrafficPattern, err) } } return nil } ================================================ FILE: core/Clash.Meta/listener/inbound/mieru_test.go ================================================ package inbound_test import ( "net" "net/netip" "strconv" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/stretchr/testify/assert" ) func TestNewMieru(t *testing.T) { type args struct { option *inbound.MieruOption } tests := []struct { name string args args wantErr bool }{ { name: "valid with port", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Transport: "TCP", Users: map[string]string{"user": "pass"}, }, }, wantErr: false, }, { name: "valid with port range", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8090-8099", }, Transport: "UDP", Users: map[string]string{"user": "pass"}, }, }, wantErr: false, }, { name: "valid mix of port and port-range", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080,8090-8099", }, Transport: "TCP", Users: map[string]string{"user": "pass"}, }, }, wantErr: false, }, { name: "valid traffic pattern", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Transport: "TCP", Users: map[string]string{"user": "pass"}, TrafficPattern: "GgQIARAK", }, }, wantErr: false, }, { name: "invalid - no port", args: args{ option: &inbound.MieruOption{ Transport: "TCP", Users: map[string]string{"user": "pass"}, }, }, wantErr: true, }, { name: "invalid - transport", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Transport: "INVALID", Users: map[string]string{"user": "pass"}, }, }, wantErr: true, }, { name: "invalid - no transport", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Users: map[string]string{"user": "pass"}, }, }, wantErr: true, }, { name: "invalid - no users", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Transport: "TCP", Users: map[string]string{}, }, }, wantErr: true, }, { name: "invalid - empty username", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Transport: "TCP", Users: map[string]string{"": "pass"}, }, }, wantErr: true, }, { name: "invalid - empty password", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Transport: "TCP", Users: map[string]string{"user": ""}, }, }, wantErr: true, }, { name: "invalid traffic pattern", args: args{ option: &inbound.MieruOption{ BaseOption: inbound.BaseOption{ Port: "8080", }, Transport: "TCP", Users: map[string]string{"user": "pass"}, TrafficPattern: "1212ababXYYX", }, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := inbound.NewMieru(tt.args.option) if (err != nil) != tt.wantErr { t.Errorf("NewMieru() error = %v, wantErr %v", err, tt.wantErr) return } if err == nil { got.Close() } }) } } func TestInboundMieru(t *testing.T) { t.Run("TCP_HANDSHAKE_STANDARD", func(t *testing.T) { testInboundMieruTCP(t, "HANDSHAKE_STANDARD") }) t.Run("TCP_HANDSHAKE_NO_WAIT", func(t *testing.T) { testInboundMieruTCP(t, "HANDSHAKE_NO_WAIT") }) t.Run("UDP_HANDSHAKE_STANDARD", func(t *testing.T) { testInboundMieruUDP(t, "HANDSHAKE_STANDARD") }) t.Run("UDP_HANDSHAKE_NO_WAIT", func(t *testing.T) { testInboundMieruUDP(t, "HANDSHAKE_NO_WAIT") }) } func testInboundMieruTCP(t *testing.T, handshakeMode string) { t.Parallel() l, err := net.Listen("tcp", "127.0.0.1:0") if !assert.NoError(t, err) { return } port := l.Addr().(*net.TCPAddr).Port l.Close() inboundOptions := inbound.MieruOption{ BaseOption: inbound.BaseOption{ NameStr: "mieru_inbound_tcp", Listen: "127.0.0.1", Port: strconv.Itoa(port), }, Transport: "TCP", Users: map[string]string{"test": "password"}, UserHintIsMandatory: true, } in, err := inbound.NewMieru(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions := outbound.MieruOption{ Name: "mieru_outbound_tcp", Server: addrPort.Addr().String(), Port: int(addrPort.Port()), Transport: "TCP", UserName: "test", Password: "password", HandshakeMode: handshakeMode, } outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewMieru(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) } func testInboundMieruUDP(t *testing.T, handshakeMode string) { t.Parallel() l, err := net.ListenPacket("udp", "127.0.0.1:0") if !assert.NoError(t, err) { return } port := l.LocalAddr().(*net.UDPAddr).Port l.Close() inboundOptions := inbound.MieruOption{ BaseOption: inbound.BaseOption{ NameStr: "mieru_inbound_udp", Listen: "127.0.0.1", Port: strconv.Itoa(port), }, Transport: "UDP", Users: map[string]string{"test": "password"}, UserHintIsMandatory: true, } in, err := inbound.NewMieru(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions := outbound.MieruOption{ Name: "mieru_outbound_udp", Server: addrPort.Addr().String(), Port: int(addrPort.Port()), Transport: "UDP", UserName: "test", Password: "password", HandshakeMode: handshakeMode, } outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewMieru(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoSequentialTest(t, out) } ================================================ FILE: core/Clash.Meta/listener/inbound/mixed.go ================================================ package inbound import ( "errors" "fmt" "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/mixed" "github.com/metacubex/mihomo/listener/socks" "github.com/metacubex/mihomo/log" ) type MixedOption struct { BaseOption Users AuthUsers `inbound:"users,omitempty"` UDP bool `inbound:"udp,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o MixedOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Mixed struct { *Base config *MixedOption l []*mixed.Listener lUDP []*socks.UDPListener udp bool } func NewMixed(options *MixedOption) (*Mixed, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &Mixed{ Base: base, config: options, udp: options.UDP, }, nil } // Config implements constant.InboundListener func (m *Mixed) Config() C.InboundConfig { return m.config } // Address implements constant.InboundListener func (m *Mixed) Address() string { var addrList []string for _, l := range m.l { addrList = append(addrList, l.Address()) } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (m *Mixed) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(m.RawAddress(), ",") { l, err := mixed.NewWithConfig( LC.AuthServer{ Enable: true, Listen: addr, AuthStore: m.config.Users.GetAuthStore(), Certificate: m.config.Certificate, PrivateKey: m.config.PrivateKey, ClientAuthType: m.config.ClientAuthType, ClientAuthCert: m.config.ClientAuthCert, EchKey: m.config.EchKey, RealityConfig: m.config.RealityConfig.Build(), }, tunnel, m.Additions()..., ) if err != nil { return err } m.l = append(m.l, l) if m.udp { lUDP, err := socks.NewUDP(addr, tunnel, m.Additions()...) if err != nil { return err } m.lUDP = append(m.lUDP, lUDP) } } log.Infoln("Mixed(http+socks)[%s] proxy listening at: %s", m.Name(), m.Address()) return nil } // Close implements constant.InboundListener func (m *Mixed) Close() error { var errs []error for _, l := range m.l { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) } } for _, l := range m.lUDP { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } var _ C.InboundListener = (*Mixed)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/mux.go ================================================ package inbound import "github.com/metacubex/mihomo/listener/sing" type MuxOption struct { Padding bool `inbound:"padding,omitempty"` Brutal BrutalOptions `inbound:"brutal,omitempty"` } type BrutalOptions struct { Enabled bool `inbound:"enabled,omitempty"` Up string `inbound:"up,omitempty"` Down string `inbound:"down,omitempty"` } func (m MuxOption) Build() sing.MuxOption { return sing.MuxOption{ Padding: m.Padding, Brutal: sing.BrutalOptions{ Enabled: m.Brutal.Enabled, Up: m.Brutal.Up, Down: m.Brutal.Down, }, } } ================================================ FILE: core/Clash.Meta/listener/inbound/mux_test.go ================================================ package inbound_test import ( "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/stretchr/testify/assert" "golang.org/x/exp/slices" ) var singMuxProtocolList = []string{"h2mux", "smux", "yamux"} var singMuxProtocolListLong = []string{"yamux"} // don't test "smux", "h2mux" because it has some confused bugs // notCloseProxyAdapter is a proxy adapter that does not close the underlying outbound.ProxyAdapter. // The outbound.SingMux will close the underlying outbound.ProxyAdapter when it is closed, but we don't want to close it. // The underlying outbound.ProxyAdapter should only be closed by the caller of testSingMux. type notCloseProxyAdapter struct { outbound.ProxyAdapter } func (n *notCloseProxyAdapter) Close() error { return nil } func testSingMux(t *testing.T, tunnel *TestTunnel, out outbound.ProxyAdapter) { t.Run("singmux", func(t *testing.T) { for _, protocol := range singMuxProtocolList { protocol := protocol t.Run(protocol, func(t *testing.T) { singMuxOption := outbound.SingMuxOption{ Enabled: true, Protocol: protocol, } out, err := outbound.NewSingMux(singMuxOption, ¬CloseProxyAdapter{out}) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoSequentialTest(t, out) if slices.Contains(singMuxProtocolListLong, protocol) { tunnel.DoConcurrentTest(t, out) } }) } }) } ================================================ FILE: core/Clash.Meta/listener/inbound/reality.go ================================================ package inbound import "github.com/metacubex/mihomo/listener/reality" type RealityConfig struct { Dest string `inbound:"dest"` PrivateKey string `inbound:"private-key"` ShortID []string `inbound:"short-id"` ServerNames []string `inbound:"server-names"` MaxTimeDifference int `inbound:"max-time-difference,omitempty"` Proxy string `inbound:"proxy,omitempty"` LimitFallbackUpload RealityLimitFallback `inbound:"limit-fallback-upload,omitempty"` LimitFallbackDownload RealityLimitFallback `inbound:"limit-fallback-download,omitempty"` } type RealityLimitFallback struct { AfterBytes uint64 `inbound:"after-bytes,omitempty"` BytesPerSec uint64 `inbound:"bytes-per-sec,omitempty"` BurstBytesPerSec uint64 `inbound:"burst-bytes-per-sec,omitempty"` } func (c RealityConfig) Build() reality.Config { return reality.Config{ Dest: c.Dest, PrivateKey: c.PrivateKey, ShortID: c.ShortID, ServerNames: c.ServerNames, MaxTimeDifference: c.MaxTimeDifference, Proxy: c.Proxy, LimitFallbackUpload: reality.LimitFallback{ AfterBytes: c.LimitFallbackUpload.AfterBytes, BytesPerSec: c.LimitFallbackUpload.BytesPerSec, BurstBytesPerSec: c.LimitFallbackUpload.BurstBytesPerSec, }, LimitFallbackDownload: reality.LimitFallback{ AfterBytes: c.LimitFallbackDownload.AfterBytes, BytesPerSec: c.LimitFallbackDownload.BytesPerSec, BurstBytesPerSec: c.LimitFallbackDownload.BurstBytesPerSec, }, } } ================================================ FILE: core/Clash.Meta/listener/inbound/redir.go ================================================ package inbound import ( "errors" "fmt" "strings" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/redir" "github.com/metacubex/mihomo/log" ) type RedirOption struct { BaseOption } func (o RedirOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Redir struct { *Base config *RedirOption l []*redir.Listener } func NewRedir(options *RedirOption) (*Redir, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &Redir{ Base: base, config: options, }, nil } // Config implements constant.InboundListener func (r *Redir) Config() C.InboundConfig { return r.config } // Address implements constant.InboundListener func (r *Redir) Address() string { var addrList []string for _, l := range r.l { addrList = append(addrList, l.Address()) } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (r *Redir) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(r.RawAddress(), ",") { l, err := redir.New(addr, tunnel, r.Additions()...) if err != nil { return err } r.l = append(r.l, l) } log.Infoln("Redir[%s] proxy listening at: %s", r.Name(), r.Address()) return nil } // Close implements constant.InboundListener func (r *Redir) Close() error { var errs []error for _, l := range r.l { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close redir listener %s err: %w", l.Address(), err)) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } var _ C.InboundListener = (*Redir)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/shadowsocks.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing_shadowsocks" "github.com/metacubex/mihomo/log" ) type ShadowSocksOption struct { BaseOption Password string `inbound:"password"` Cipher string `inbound:"cipher"` UDP bool `inbound:"udp,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` KcpTun KcpTun `inbound:"kcp-tun,omitempty"` } func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type ShadowSocks struct { *Base config *ShadowSocksOption l C.MultiAddrListener ss LC.ShadowsocksServer } func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &ShadowSocks{ Base: base, config: options, ss: LC.ShadowsocksServer{ Enable: true, Listen: base.RawAddress(), Password: options.Password, Cipher: options.Cipher, Udp: options.UDP, MuxOption: options.MuxOption.Build(), ShadowTLS: options.ShadowTLS.Build(), KcpTun: options.KcpTun.Build(), }, }, nil } // Config implements constant.InboundListener func (s *ShadowSocks) Config() C.InboundConfig { return s.config } // Address implements constant.InboundListener func (s *ShadowSocks) Address() string { var addrList []string if s.l != nil { for _, addr := range s.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (s *ShadowSocks) Listen(tunnel C.Tunnel) error { var err error s.l, err = sing_shadowsocks.New(s.ss, tunnel, s.Additions()...) if err != nil { return err } log.Infoln("ShadowSocks[%s] proxy listening at: %s", s.Name(), s.Address()) return nil } // Close implements constant.InboundListener func (s *ShadowSocks) Close() error { return s.l.Close() } var _ C.InboundListener = (*ShadowSocks)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/shadowsocks_test.go ================================================ package inbound_test import ( "crypto/rand" "encoding/base64" "net" "net/netip" "runtime" "strings" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/metacubex/mihomo/transport/kcptun" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" shadowsocks "github.com/metacubex/sing-shadowsocks" "github.com/metacubex/sing-shadowsocks/shadowaead" "github.com/metacubex/sing-shadowsocks/shadowaead_2022" "github.com/metacubex/sing-shadowsocks/shadowstream" "github.com/stretchr/testify/assert" ) var noneList = []string{shadowsocks.MethodNone} var shadowsocksCipherLists = [][]string{noneList, shadowaead.List, shadowaead_2022.List, shadowstream.List} var shadowsocksCipherShortLists = [][]string{noneList, shadowaead.List[:5]} // for test shadowTLS and kcptun var shadowsocksPassword32 string var shadowsocksPassword16 string func init() { passwordBytes := make([]byte, 32) rand.Read(passwordBytes) shadowsocksPassword32 = base64.StdEncoding.EncodeToString(passwordBytes) shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16]) } func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherLists [][]string, enableSingMux bool) { t.Parallel() for _, cipherList := range cipherLists { for i, cipher := range cipherList { enableSingMux := enableSingMux && i == 0 cipher := cipher t.Run(cipher, func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value inboundOptions.Cipher = cipher outboundOptions.Cipher = cipher testInboundShadowSocks0(t, inboundOptions, outboundOptions, enableSingMux) }) } } } func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, enableSingMux bool) { t.Parallel() password := shadowsocksPassword32 if strings.Contains(inboundOptions.Cipher, "-128-") { password = shadowsocksPassword16 } inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "shadowsocks_inbound", Listen: "127.0.0.1", Port: "0", } inboundOptions.Password = password in, err := inbound.NewShadowSocks(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "shadowsocks_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = password outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewShadowSocks(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) if enableSingMux { testSingMux(t, tunnel, out) } } func TestInboundShadowSocks_Basic(t *testing.T) { inboundOptions := inbound.ShadowSocksOption{} outboundOptions := outbound.ShadowSocksOption{} testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherLists, true) } func testInboundShadowSocksShadowTls(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { t.Parallel() t.Run("Conn", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, true) }) t.Run("UConn", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value outboundOptions.ClientFingerprint = "chrome" testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, true) }) } func TestInboundShadowSocks_ShadowTlsv1(t *testing.T) { inboundOptions := inbound.ShadowSocksOption{ ShadowTLS: inbound.ShadowTLS{ Enable: true, Version: 1, Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, }, } outboundOptions := outbound.ShadowSocksOption{ Plugin: shadowtls.Mode, PluginOpts: map[string]any{"host": realityDest, "fingerprint": tlsFingerprint, "version": 1}, } testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) } func TestInboundShadowSocks_ShadowTlsv2(t *testing.T) { inboundOptions := inbound.ShadowSocksOption{ ShadowTLS: inbound.ShadowTLS{ Enable: true, Version: 2, Password: shadowsocksPassword16, Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, }, } outboundOptions := outbound.ShadowSocksOption{ Plugin: shadowtls.Mode, PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 2}, } outboundOptions.PluginOpts["alpn"] = []string{"http/1.1"} // shadowtls v2 work confuse with http/2 server, so we set alpn to http/1.1 to pass the test testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) } func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) { inboundOptions := inbound.ShadowSocksOption{ ShadowTLS: inbound.ShadowTLS{ Enable: true, Version: 3, Users: []inbound.ShadowTLSUser{{Name: "test", Password: shadowsocksPassword16}}, Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, }, } outboundOptions := outbound.ShadowSocksOption{ Plugin: shadowtls.Mode, PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 3}, } testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) } func TestInboundShadowSocks_KcpTun(t *testing.T) { if runtime.GOOS == "windows" && strings.HasPrefix(runtime.Version(), "go1.20") { t.Skip("skip kcptun test on windows go1.20") } inboundOptions := inbound.ShadowSocksOption{ KcpTun: inbound.KcpTun{ Enable: true, Key: shadowsocksPassword16, }, } outboundOptions := outbound.ShadowSocksOption{ Plugin: kcptun.Mode, PluginOpts: map[string]any{"key": shadowsocksPassword16}, } testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false) } ================================================ FILE: core/Clash.Meta/listener/inbound/shadowtls.go ================================================ package inbound import ( "github.com/metacubex/mihomo/common/utils" LC "github.com/metacubex/mihomo/listener/config" ) type ShadowTLS struct { Enable bool `inbound:"enable"` Version int `inbound:"version,omitempty"` Password string `inbound:"password,omitempty"` Users []ShadowTLSUser `inbound:"users,omitempty"` Handshake ShadowTLSHandshakeOptions `inbound:"handshake,omitempty"` HandshakeForServerName map[string]ShadowTLSHandshakeOptions `inbound:"handshake-for-server-name,omitempty"` StrictMode bool `inbound:"strict-mode,omitempty"` WildcardSNI string `inbound:"wildcard-sni,omitempty"` } type ShadowTLSUser struct { Name string `inbound:"name,omitempty"` Password string `inbound:"password,omitempty"` } type ShadowTLSHandshakeOptions struct { Dest string `inbound:"dest"` Proxy string `inbound:"proxy,omitempty"` } func (c ShadowTLS) Build() LC.ShadowTLS { handshakeForServerName := make(map[string]LC.ShadowTLSHandshakeOptions) for k, v := range c.HandshakeForServerName { handshakeForServerName[k] = v.Build() } return LC.ShadowTLS{ Enable: c.Enable, Version: c.Version, Password: c.Password, Users: utils.Map(c.Users, ShadowTLSUser.Build), Handshake: c.Handshake.Build(), HandshakeForServerName: handshakeForServerName, StrictMode: c.StrictMode, WildcardSNI: c.WildcardSNI, } } func (c ShadowTLSUser) Build() LC.ShadowTLSUser { return LC.ShadowTLSUser{ Name: c.Name, Password: c.Password, } } func (c ShadowTLSHandshakeOptions) Build() LC.ShadowTLSHandshakeOptions { return LC.ShadowTLSHandshakeOptions{ Dest: c.Dest, Proxy: c.Proxy, } } ================================================ FILE: core/Clash.Meta/listener/inbound/socks.go ================================================ package inbound import ( "errors" "fmt" "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/socks" "github.com/metacubex/mihomo/log" ) type SocksOption struct { BaseOption Users AuthUsers `inbound:"users,omitempty"` UDP bool `inbound:"udp,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o SocksOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Socks struct { *Base config *SocksOption udp bool stl []*socks.Listener sul []*socks.UDPListener } func NewSocks(options *SocksOption) (*Socks, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &Socks{ Base: base, config: options, udp: options.UDP, }, nil } // Config implements constant.InboundListener func (s *Socks) Config() C.InboundConfig { return s.config } // Close implements constant.InboundListener func (s *Socks) Close() error { var errs []error for _, l := range s.stl { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) } } for _, l := range s.sul { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } // Address implements constant.InboundListener func (s *Socks) Address() string { var addrList []string for _, l := range s.stl { addrList = append(addrList, l.Address()) } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (s *Socks) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(s.RawAddress(), ",") { stl, err := socks.NewWithConfig( LC.AuthServer{ Enable: true, Listen: addr, AuthStore: s.config.Users.GetAuthStore(), Certificate: s.config.Certificate, PrivateKey: s.config.PrivateKey, ClientAuthType: s.config.ClientAuthType, ClientAuthCert: s.config.ClientAuthCert, EchKey: s.config.EchKey, RealityConfig: s.config.RealityConfig.Build(), }, tunnel, s.Additions()..., ) if err != nil { return err } s.stl = append(s.stl, stl) if s.udp { sul, err := socks.NewUDP(addr, tunnel, s.Additions()...) if err != nil { return err } s.sul = append(s.sul, sul) } } log.Infoln("SOCKS[%s] proxy listening at: %s", s.Name(), s.Address()) return nil } var _ C.InboundListener = (*Socks)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/sudoku.go ================================================ package inbound import ( "errors" "fmt" "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sudoku" "github.com/metacubex/mihomo/log" ) type SudokuOption struct { BaseOption Key string `inbound:"key"` AEADMethod string `inbound:"aead-method,omitempty"` PaddingMin *int `inbound:"padding-min,omitempty"` PaddingMax *int `inbound:"padding-max,omitempty"` TableType string `inbound:"table-type,omitempty"` // "prefer_ascii", "prefer_entropy", or directional "up_ascii_down_entropy"/"up_entropy_down_ascii" HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"` EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"` CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv CustomTables []string `inbound:"custom-tables,omitempty"` DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"` HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto" PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints Fallback string `inbound:"fallback,omitempty"` HTTPMaskOptions *SudokuHTTPMaskOptions `inbound:"httpmask,omitempty"` // mihomo private extension (not the part of standard Sudoku protocol) MuxOption MuxOption `inbound:"mux-option,omitempty"` } type SudokuHTTPMaskOptions struct { Disable bool `inbound:"disable,omitempty"` Mode string `inbound:"mode,omitempty"` PathRoot string `inbound:"path-root,omitempty"` } func (o SudokuOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Sudoku struct { *Base config *SudokuOption listeners []*sudoku.Listener serverConf LC.SudokuServer } func NewSudoku(options *SudokuOption) (*Sudoku, error) { if options.Key == "" { return nil, fmt.Errorf("sudoku inbound requires key") } base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } serverConf := LC.SudokuServer{ Enable: true, Listen: base.RawAddress(), Key: options.Key, AEADMethod: options.AEADMethod, PaddingMin: options.PaddingMin, PaddingMax: options.PaddingMax, TableType: options.TableType, HandshakeTimeoutSecond: options.HandshakeTimeoutSecond, EnablePureDownlink: options.EnablePureDownlink, CustomTable: options.CustomTable, CustomTables: options.CustomTables, DisableHTTPMask: options.DisableHTTPMask, HTTPMaskMode: options.HTTPMaskMode, PathRoot: strings.TrimSpace(options.PathRoot), Fallback: strings.TrimSpace(options.Fallback), } if hm := options.HTTPMaskOptions; hm != nil { serverConf.DisableHTTPMask = hm.Disable if hm.Mode != "" { serverConf.HTTPMaskMode = hm.Mode } if pr := strings.TrimSpace(hm.PathRoot); pr != "" { serverConf.PathRoot = pr } } serverConf.MuxOption = options.MuxOption.Build() return &Sudoku{ Base: base, config: options, serverConf: serverConf, }, nil } // Config implements constant.InboundListener func (s *Sudoku) Config() C.InboundConfig { return s.config } // Address implements constant.InboundListener func (s *Sudoku) Address() string { var addrList []string for _, l := range s.listeners { addrList = append(addrList, l.Address()) } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (s *Sudoku) Listen(tunnel C.Tunnel) error { if s.serverConf.Key == "" { return fmt.Errorf("sudoku inbound requires key") } var errs []error for _, addr := range strings.Split(s.RawAddress(), ",") { conf := s.serverConf conf.Listen = addr l, err := sudoku.New(conf, tunnel, s.Additions()...) if err != nil { errs = append(errs, err) continue } s.listeners = append(s.listeners, l) } if len(errs) > 0 { return errors.Join(errs...) } log.Infoln("Sudoku[%s] inbound listening at: %s", s.Name(), s.Address()) return nil } // Close implements constant.InboundListener func (s *Sudoku) Close() error { var errs []error for _, l := range s.listeners { if err := l.Close(); err != nil { errs = append(errs, err) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } var _ C.InboundListener = (*Sudoku)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/sudoku_test.go ================================================ package inbound_test import ( "net/netip" "runtime" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/metacubex/mihomo/transport/sudoku" "github.com/stretchr/testify/assert" ) var sudokuPrivateKey, sudokuPublicKey, _ = sudoku.GenKeyPair() func testInboundSudoku(t *testing.T, inboundOptions inbound.SudokuOption, outboundOptions outbound.SudokuOption) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "sudoku_inbound", Listen: "127.0.0.1", Port: "0", } in, err := inbound.NewSudoku(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "sudoku_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewSudoku(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) testSingMux(t, tunnel, out) } func TestInboundSudoku_Basic(t *testing.T) { key := "test_key" inboundOptions := inbound.SudokuOption{ Key: key, } outboundOptions := outbound.SudokuOption{ Key: key, } testInboundSudoku(t, inboundOptions, outboundOptions) t.Run("ed25519key", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.Key = sudokuPublicKey outboundOptions.Key = sudokuPrivateKey testInboundSudoku(t, inboundOptions, outboundOptions) }) } func TestInboundSudoku_Entropy(t *testing.T) { key := "test_key_entropy" inboundOptions := inbound.SudokuOption{ Key: key, TableType: "prefer_entropy", } outboundOptions := outbound.SudokuOption{ Key: key, TableType: "prefer_entropy", } testInboundSudoku(t, inboundOptions, outboundOptions) t.Run("ed25519key", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.Key = sudokuPublicKey outboundOptions.Key = sudokuPrivateKey testInboundSudoku(t, inboundOptions, outboundOptions) }) } func TestInboundSudoku_Padding(t *testing.T) { key := "test_key_padding" paddingMin := 10 paddingMax := 100 inboundOptions := inbound.SudokuOption{ Key: key, PaddingMin: &paddingMin, PaddingMax: &paddingMax, } outboundOptions := outbound.SudokuOption{ Key: key, PaddingMin: &paddingMin, PaddingMax: &paddingMax, } testInboundSudoku(t, inboundOptions, outboundOptions) t.Run("ed25519key", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.Key = sudokuPublicKey outboundOptions.Key = sudokuPrivateKey testInboundSudoku(t, inboundOptions, outboundOptions) }) } func TestInboundSudoku_PackedDownlink(t *testing.T) { key := "test_key_packed" enablePure := false inboundOptions := inbound.SudokuOption{ Key: key, EnablePureDownlink: &enablePure, } outboundOptions := outbound.SudokuOption{ Key: key, EnablePureDownlink: &enablePure, } testInboundSudoku(t, inboundOptions, outboundOptions) t.Run("ed25519key", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.Key = sudokuPublicKey outboundOptions.Key = sudokuPrivateKey testInboundSudoku(t, inboundOptions, outboundOptions) }) } func TestInboundSudoku_CustomTable(t *testing.T) { key := "test_key_custom" custom := "xpxvvpvv" inboundOptions := inbound.SudokuOption{ Key: key, TableType: "prefer_entropy", CustomTable: custom, } outboundOptions := outbound.SudokuOption{ Key: key, TableType: "prefer_entropy", CustomTable: custom, } testInboundSudoku(t, inboundOptions, outboundOptions) t.Run("ed25519key", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.Key = sudokuPublicKey outboundOptions.Key = sudokuPrivateKey testInboundSudoku(t, inboundOptions, outboundOptions) }) } func TestInboundSudoku_HTTPMaskMode(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR") } key := "test_key_http_mask_mode" for _, mode := range []string{"ws", "stream", "poll", "auto"} { mode := mode t.Run(mode, func(t *testing.T) { inboundOptions := inbound.SudokuOption{ Key: key, HTTPMaskMode: mode, } httpMask := true outboundOptions := outbound.SudokuOption{ Key: key, HTTPMask: &httpMask, HTTPMaskMode: mode, } testInboundSudoku(t, inboundOptions, outboundOptions) }) } } ================================================ FILE: core/Clash.Meta/listener/inbound/tproxy.go ================================================ package inbound import ( "errors" "fmt" "strings" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/tproxy" "github.com/metacubex/mihomo/log" ) type TProxyOption struct { BaseOption UDP bool `inbound:"udp,omitempty"` } func (o TProxyOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type TProxy struct { *Base config *TProxyOption lUDP []*tproxy.UDPListener lTCP []*tproxy.Listener udp bool } func NewTProxy(options *TProxyOption) (*TProxy, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &TProxy{ Base: base, config: options, udp: options.UDP, }, nil } // Config implements constant.InboundListener func (t *TProxy) Config() C.InboundConfig { return t.config } // Address implements constant.InboundListener func (t *TProxy) Address() string { var addrList []string for _, l := range t.lTCP { addrList = append(addrList, l.Address()) } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (t *TProxy) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(t.RawAddress(), ",") { lTCP, err := tproxy.New(addr, tunnel, t.Additions()...) if err != nil { return err } t.lTCP = append(t.lTCP, lTCP) if t.udp { lUDP, err := tproxy.NewUDP(addr, tunnel, t.Additions()...) if err != nil { return err } t.lUDP = append(t.lUDP, lUDP) } } log.Infoln("TProxy[%s] proxy listening at: %s", t.Name(), t.Address()) return nil } // Close implements constant.InboundListener func (t *TProxy) Close() error { var errs []error for _, l := range t.lTCP { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) } } for _, l := range t.lUDP { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } var _ C.InboundListener = (*TProxy)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/trojan.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/trojan" "github.com/metacubex/mihomo/log" ) type TrojanOption struct { BaseOption Users []TrojanUser `inbound:"users"` WsPath string `inbound:"ws-path,omitempty"` GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` SSOption TrojanSSOption `inbound:"ss-option,omitempty"` } type TrojanUser struct { Username string `inbound:"username,omitempty"` Password string `inbound:"password"` } // TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 type TrojanSSOption struct { Enabled bool `inbound:"enabled,omitempty"` Method string `inbound:"method,omitempty"` Password string `inbound:"password,omitempty"` } func (o TrojanOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Trojan struct { *Base config *TrojanOption l C.MultiAddrListener vs LC.TrojanServer } func NewTrojan(options *TrojanOption) (*Trojan, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } users := make([]LC.TrojanUser, len(options.Users)) for i, v := range options.Users { users[i] = LC.TrojanUser{ Username: v.Username, Password: v.Password, } } return &Trojan{ Base: base, config: options, vs: LC.TrojanServer{ Enable: true, Listen: base.RawAddress(), Users: users, WsPath: options.WsPath, GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), TrojanSSOption: LC.TrojanSSOption{ Enabled: options.SSOption.Enabled, Method: options.SSOption.Method, Password: options.SSOption.Password, }, }, }, nil } // Config implements constant.InboundListener func (v *Trojan) Config() C.InboundConfig { return v.config } // Address implements constant.InboundListener func (v *Trojan) Address() string { var addrList []string if v.l != nil { for _, addr := range v.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (v *Trojan) Listen(tunnel C.Tunnel) error { var err error v.l, err = trojan.New(v.vs, tunnel, v.Additions()...) if err != nil { return err } log.Infoln("Trojan[%s] proxy listening at: %s", v.Name(), v.Address()) return nil } // Close implements constant.InboundListener func (v *Trojan) Close() error { return v.l.Close() } var _ C.InboundListener = (*Trojan)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/trojan_test.go ================================================ package inbound_test import ( "net" "net/netip" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/stretchr/testify/assert" ) func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outboundOptions outbound.TrojanOption) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "trojan_inbound", Listen: "127.0.0.1", Port: "0", } inboundOptions.Users = []inbound.TrojanUser{ {Username: "test", Password: userUUID}, } in, err := inbound.NewTrojan(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "trojan_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.Password = userUUID outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTrojan(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) if outboundOptions.Network == "grpc" { // don't test sing-mux over grpc return } testSingMux(t, tunnel, out) } func testInboundTrojanTLS(t *testing.T, inboundOptions inbound.TrojanOption, outboundOptions outbound.TrojanOption) { testInboundTrojan(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundTrojan(t, inboundOptions, outboundOptions) }) t.Run("mTLS", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey testInboundTrojan(t, inboundOptions, outboundOptions) }) t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundTrojan(t, inboundOptions, outboundOptions) }) } func TestInboundTrojan_TLS(t *testing.T) { inboundOptions := inbound.TrojanOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, } outboundOptions := outbound.TrojanOption{ Fingerprint: tlsFingerprint, } testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss1(t *testing.T) { inboundOptions := inbound.TrojanOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", } outboundOptions := outbound.TrojanOption{ Fingerprint: tlsFingerprint, Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", }, } testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss2(t *testing.T) { inboundOptions := inbound.TrojanOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", GrpcServiceName: "GunService", } outboundOptions := outbound.TrojanOption{ Fingerprint: tlsFingerprint, Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", }, } testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Grpc1(t *testing.T) { inboundOptions := inbound.TrojanOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, GrpcServiceName: "GunService", } outboundOptions := outbound.TrojanOption{ Fingerprint: tlsFingerprint, Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Grpc2(t *testing.T) { inboundOptions := inbound.TrojanOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", GrpcServiceName: "GunService", } outboundOptions := outbound.TrojanOption{ Fingerprint: tlsFingerprint, Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Reality(t *testing.T) { inboundOptions := inbound.TrojanOption{ RealityConfig: inbound.RealityConfig{ Dest: net.JoinHostPort(realityDest, "443"), PrivateKey: realityPrivateKey, ShortID: []string{realityShortid}, ServerNames: []string{realityDest}, }, } outboundOptions := outbound.TrojanOption{ SNI: realityDest, RealityOpts: outbound.RealityOptions{ PublicKey: realityPublickey, ShortID: realityShortid, }, ClientFingerprint: "chrome", } testInboundTrojan(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Reality_Grpc(t *testing.T) { inboundOptions := inbound.TrojanOption{ RealityConfig: inbound.RealityConfig{ Dest: net.JoinHostPort(realityDest, "443"), PrivateKey: realityPrivateKey, ShortID: []string{realityShortid}, ServerNames: []string{realityDest}, }, GrpcServiceName: "GunService", } outboundOptions := outbound.TrojanOption{ SNI: realityDest, RealityOpts: outbound.RealityOptions{ PublicKey: realityPublickey, ShortID: realityShortid, }, ClientFingerprint: "chrome", Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundTrojan(t, inboundOptions, outboundOptions) } func TestInboundTrojan_TLS_TrojanSS(t *testing.T) { inboundOptions := inbound.TrojanOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, SSOption: inbound.TrojanSSOption{ Enabled: true, Method: "", Password: "password", }, } outboundOptions := outbound.TrojanOption{ Fingerprint: tlsFingerprint, SSOpts: outbound.TrojanSSOption{ Enabled: true, Method: "", Password: "password", }, } testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { inboundOptions := inbound.TrojanOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, SSOption: inbound.TrojanSSOption{ Enabled: true, Method: "", Password: "password", }, WsPath: "/ws", } outboundOptions := outbound.TrojanOption{ Fingerprint: tlsFingerprint, SSOpts: outbound.TrojanSSOption{ Enabled: true, Method: "", Password: "password", }, Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", }, } testInboundTrojanTLS(t, inboundOptions, outboundOptions) } ================================================ FILE: core/Clash.Meta/listener/inbound/trusttunnel.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/trusttunnel" "github.com/metacubex/mihomo/log" ) type TrustTunnelOption struct { BaseOption Users AuthUsers `inbound:"users,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` Network []string `inbound:"network,omitempty"` CongestionController string `inbound:"congestion-controller,omitempty"` CWND int `inbound:"cwnd,omitempty"` BBRProfile string `inbound:"bbr-profile,omitempty"` } func (o TrustTunnelOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type TrustTunnel struct { *Base config *TrustTunnelOption l C.MultiAddrListener vs LC.TrustTunnelServer } func NewTrustTunnel(options *TrustTunnelOption) (*TrustTunnel, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } users := make(map[string]string) for _, user := range options.Users { users[user.Username] = user.Password } return &TrustTunnel{ Base: base, config: options, vs: LC.TrustTunnelServer{ Enable: true, Listen: base.RawAddress(), Users: users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, Network: options.Network, CongestionController: options.CongestionController, CWND: options.CWND, BBRProfile: options.BBRProfile, }, }, nil } // Config implements constant.InboundListener func (v *TrustTunnel) Config() C.InboundConfig { return v.config } // Address implements constant.InboundListener func (v *TrustTunnel) Address() string { var addrList []string if v.l != nil { for _, addr := range v.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (v *TrustTunnel) Listen(tunnel C.Tunnel) error { var err error v.l, err = trusttunnel.New(v.vs, tunnel, v.Additions()...) if err != nil { return err } log.Infoln("TrustTunnel[%s] proxy listening at: %s", v.Name(), v.Address()) return nil } // Close implements constant.InboundListener func (v *TrustTunnel) Close() error { return v.l.Close() } var _ C.InboundListener = (*TrustTunnel)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/trusttunnel_test.go ================================================ package inbound_test import ( "net/netip" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/stretchr/testify/assert" ) func testInboundTrustTunnel(t *testing.T, inboundOptions inbound.TrustTunnelOption, outboundOptions outbound.TrustTunnelOption) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "trusttunnel_inbound", Listen: "127.0.0.1", Port: "0", } inboundOptions.Users = []inbound.AuthUser{{Username: "test", Password: userUUID}} in, err := inbound.NewTrustTunnel(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "trusttunnel_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.UserName = "test" outboundOptions.Password = userUUID outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTrustTunnel(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) } func testInboundTrustTunnelTLS(t *testing.T, quic bool) { inboundOptions := inbound.TrustTunnelOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, } outboundOptions := outbound.TrustTunnelOption{ Fingerprint: tlsFingerprint, HealthCheck: true, } if quic { inboundOptions.Network = []string{"udp"} inboundOptions.CongestionController = "bbr" outboundOptions.Quic = true } testInboundTrustTunnel(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundTrustTunnel(t, inboundOptions, outboundOptions) }) t.Run("mTLS", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey testInboundTrustTunnel(t, inboundOptions, outboundOptions) }) t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundTrustTunnel(t, inboundOptions, outboundOptions) }) } func TestInboundTrustTunnel_H2(t *testing.T) { testInboundTrustTunnelTLS(t, true) } func TestInboundTrustTunnel_QUIC(t *testing.T) { testInboundTrustTunnelTLS(t, true) } ================================================ FILE: core/Clash.Meta/listener/inbound/tuic.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/tuic" "github.com/metacubex/mihomo/log" ) type TuicOption struct { BaseOption Token []string `inbound:"token,omitempty"` Users map[string]string `inbound:"users,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` CongestionController string `inbound:"congestion-controller,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` AuthenticationTimeout int `inbound:"authentication-timeout,omitempty"` ALPN []string `inbound:"alpn,omitempty"` MaxUdpRelayPacketSize int `inbound:"max-udp-relay-packet-size,omitempty"` CWND int `inbound:"cwnd,omitempty"` BBRProfile string `inbound:"bbr-profile,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` } func (o TuicOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Tuic struct { *Base config *TuicOption l *tuic.Listener ts LC.TuicServer } func NewTuic(options *TuicOption) (*Tuic, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &Tuic{ Base: base, config: options, ts: LC.TuicServer{ Enable: true, Listen: base.RawAddress(), Token: options.Token, Users: options.Users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, CongestionController: options.CongestionController, MaxIdleTime: options.MaxIdleTime, AuthenticationTimeout: options.AuthenticationTimeout, ALPN: options.ALPN, MaxUdpRelayPacketSize: options.MaxUdpRelayPacketSize, CWND: options.CWND, BBRProfile: options.BBRProfile, MuxOption: options.MuxOption.Build(), }, }, nil } // Config implements constant.InboundListener func (t *Tuic) Config() C.InboundConfig { return t.config } // Address implements constant.InboundListener func (t *Tuic) Address() string { var addrList []string if t.l != nil { for _, addr := range t.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (t *Tuic) Listen(tunnel C.Tunnel) error { var err error t.l, err = tuic.New(t.ts, tunnel, t.Additions()...) if err != nil { return err } log.Infoln("Tuic[%s] proxy listening at: %s", t.Name(), t.Address()) return nil } // Close implements constant.InboundListener func (t *Tuic) Close() error { return t.l.Close() } var _ C.InboundListener = (*Tuic)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/tuic_test.go ================================================ package inbound_test import ( "net/netip" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/stretchr/testify/assert" ) var tuicCCs = []string{"cubic", "new_reno", "bbr"} func testInboundTuic(t *testing.T, inboundOptions inbound.TuicOption, outboundOptions outbound.TuicOption) { t.Parallel() inboundOptions.Users = map[string]string{userUUID: userUUID} inboundOptions.Token = []string{userUUID} for _, tuicCC := range tuicCCs { tuicCC := tuicCC t.Run(tuicCC, func(t *testing.T) { t.Parallel() t.Run("v4", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value outboundOptions.Token = userUUID outboundOptions.CongestionController = tuicCC inboundOptions.CongestionController = tuicCC testInboundTuic0(t, inboundOptions, outboundOptions) }) t.Run("v5", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value outboundOptions.UUID = userUUID outboundOptions.Password = userUUID outboundOptions.CongestionController = tuicCC inboundOptions.CongestionController = tuicCC testInboundTuic0(t, inboundOptions, outboundOptions) }) }) } } func testInboundTuic0(t *testing.T, inboundOptions inbound.TuicOption, outboundOptions outbound.TuicOption) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "tuic_inbound", Listen: "127.0.0.1", Port: "0", } in, err := inbound.NewTuic(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "tuic_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewTuic(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) } func TestInboundTuic_TLS(t *testing.T) { inboundOptions := inbound.TuicOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, AuthenticationTimeout: 5000, } outboundOptions := outbound.TuicOption{ Fingerprint: tlsFingerprint, } testInboundTuic(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundTuic(t, inboundOptions, outboundOptions) }) t.Run("mTLS", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey testInboundTuic(t, inboundOptions, outboundOptions) }) t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundTuic(t, inboundOptions, outboundOptions) }) } ================================================ FILE: core/Clash.Meta/listener/inbound/tun.go ================================================ package inbound import ( "encoding" "net/netip" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing_tun" "github.com/metacubex/mihomo/log" ) type TunOption struct { BaseOption Device string `inbound:"device,omitempty"` Stack C.TUNStack `inbound:"stack,omitempty"` DNSHijack []string `inbound:"dns-hijack,omitempty"` AutoRoute bool `inbound:"auto-route,omitempty"` AutoDetectInterface bool `inbound:"auto-detect-interface,omitempty"` MTU uint32 `inbound:"mtu,omitempty"` GSO bool `inbound:"gso,omitempty"` GSOMaxSize uint32 `inbound:"gso-max-size,omitempty"` Inet4Address []netip.Prefix `inbound:"inet4-address,omitempty"` Inet6Address []netip.Prefix `inbound:"inet6-address,omitempty"` IPRoute2TableIndex int `inbound:"iproute2-table-index,omitempty"` IPRoute2RuleIndex int `inbound:"iproute2-rule-index,omitempty"` AutoRedirect bool `inbound:"auto-redirect,omitempty"` AutoRedirectInputMark uint32 `inbound:"auto-redirect-input-mark,omitempty"` AutoRedirectOutputMark uint32 `inbound:"auto-redirect-output-mark,omitempty"` AutoRedirectIPRoute2FallbackRuleIndex int `inbound:"auto-redirect-iproute2-fallback-rule-index,omitempty"` LoopbackAddress []netip.Addr `inbound:"loopback-address,omitempty"` StrictRoute bool `inbound:"strict-route,omitempty"` RouteAddress []netip.Prefix `inbound:"route-address,omitempty"` RouteAddressSet []string `inbound:"route-address-set,omitempty"` RouteExcludeAddress []netip.Prefix `inbound:"route-exclude-address,omitempty"` RouteExcludeAddressSet []string `inbound:"route-exclude-address-set,omitempty"` IncludeInterface []string `inbound:"include-interface,omitempty"` ExcludeInterface []string `inbound:"exclude-interface,omitempty"` IncludeUID []uint32 `inbound:"include-uid,omitempty"` IncludeUIDRange []string `inbound:"include-uid-range,omitempty"` ExcludeUID []uint32 `inbound:"exclude-uid,omitempty"` ExcludeUIDRange []string `inbound:"exclude-uid-range,omitempty"` ExcludeSrcPort []uint16 `inbound:"exclude-src-port,omitempty"` ExcludeSrcPortRange []string `inbound:"exclude-src-port-range,omitempty"` ExcludeDstPort []uint16 `inbound:"exclude-dst-port,omitempty"` ExcludeDstPortRange []string `inbound:"exclude-dst-port-range,omitempty"` IncludeAndroidUser []int `inbound:"include-android-user,omitempty"` IncludePackage []string `inbound:"include-package,omitempty"` ExcludePackage []string `inbound:"exclude-package,omitempty"` IncludeMACAddress []string `inbound:"include-mac-address,omitempty"` ExcludeMACAddress []string `inbound:"exclude-mac-address,omitempty"` EndpointIndependentNat bool `inbound:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `inbound:"udp-timeout,omitempty"` DisableICMPForwarding bool `inbound:"disable-icmp-forwarding,omitempty"` FileDescriptor int `inbound:"file-descriptor,omitempty"` Inet4RouteAddress []netip.Prefix `inbound:"inet4-route-address,omitempty"` Inet6RouteAddress []netip.Prefix `inbound:"inet6-route-address,omitempty"` Inet4RouteExcludeAddress []netip.Prefix `inbound:"inet4-route-exclude-address,omitempty"` Inet6RouteExcludeAddress []netip.Prefix `inbound:"inet6-route-exclude-address,omitempty"` // darwin special config RecvMsgX bool `inbound:"recvmsgx,omitempty"` SendMsgX bool `inbound:"sendmsgx,omitempty"` } var _ encoding.TextUnmarshaler = (*netip.Addr)(nil) // ensure netip.Addr can decode direct by structure package var _ encoding.TextUnmarshaler = (*netip.Prefix)(nil) // ensure netip.Prefix can decode direct by structure package var _ encoding.TextUnmarshaler = (*C.TUNStack)(nil) // ensure C.TUNStack can decode direct by structure package func (o TunOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Tun struct { *Base config *TunOption l *sing_tun.Listener tun LC.Tun } func NewTun(options *TunOption) (*Tun, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &Tun{ Base: base, config: options, tun: LC.Tun{ Enable: true, Device: options.Device, Stack: options.Stack, DNSHijack: options.DNSHijack, AutoRoute: options.AutoRoute, AutoDetectInterface: options.AutoDetectInterface, MTU: options.MTU, GSO: options.GSO, GSOMaxSize: options.GSOMaxSize, Inet4Address: options.Inet4Address, Inet6Address: options.Inet6Address, IPRoute2TableIndex: options.IPRoute2TableIndex, IPRoute2RuleIndex: options.IPRoute2RuleIndex, AutoRedirect: options.AutoRedirect, AutoRedirectInputMark: options.AutoRedirectInputMark, AutoRedirectOutputMark: options.AutoRedirectOutputMark, AutoRedirectIPRoute2FallbackRuleIndex: options.AutoRedirectIPRoute2FallbackRuleIndex, LoopbackAddress: options.LoopbackAddress, StrictRoute: options.StrictRoute, RouteAddress: options.RouteAddress, RouteAddressSet: options.RouteAddressSet, RouteExcludeAddress: options.RouteExcludeAddress, RouteExcludeAddressSet: options.RouteExcludeAddressSet, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, IncludeUID: options.IncludeUID, IncludeUIDRange: options.IncludeUIDRange, ExcludeUID: options.ExcludeUID, ExcludeUIDRange: options.ExcludeUIDRange, ExcludeSrcPort: options.ExcludeSrcPort, ExcludeSrcPortRange: options.ExcludeSrcPortRange, ExcludeDstPort: options.ExcludeDstPort, ExcludeDstPortRange: options.ExcludeDstPortRange, IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, IncludeMACAddress: options.IncludeMACAddress, ExcludeMACAddress: options.ExcludeMACAddress, EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: options.UDPTimeout, DisableICMPForwarding: options.DisableICMPForwarding, FileDescriptor: options.FileDescriptor, Inet4RouteAddress: options.Inet4RouteAddress, Inet6RouteAddress: options.Inet6RouteAddress, Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, RecvMsgX: options.RecvMsgX, SendMsgX: options.SendMsgX, }, }, nil } // Config implements constant.InboundListener func (t *Tun) Config() C.InboundConfig { return t.config } // Address implements constant.InboundListener func (t *Tun) Address() string { return t.l.Address() } // Listen implements constant.InboundListener func (t *Tun) Listen(tunnel C.Tunnel) error { var err error t.l, err = sing_tun.New(t.tun, tunnel, t.Additions()...) if err != nil { return err } log.Infoln("Tun[%s] proxy listening at: %s", t.Name(), t.Address()) return nil } // Close implements constant.InboundListener func (t *Tun) Close() error { return t.l.Close() } var _ C.InboundListener = (*Tun)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/tunnel.go ================================================ package inbound import ( "errors" "fmt" "strings" C "github.com/metacubex/mihomo/constant" LT "github.com/metacubex/mihomo/listener/tunnel" "github.com/metacubex/mihomo/log" ) type TunnelOption struct { BaseOption Network []string `inbound:"network"` Target string `inbound:"target"` } func (o TunnelOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Tunnel struct { *Base config *TunnelOption ttl []*LT.Listener tul []*LT.PacketConn } func NewTunnel(options *TunnelOption) (*Tunnel, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } return &Tunnel{ Base: base, config: options, }, nil } // Config implements constant.InboundListener func (t *Tunnel) Config() C.InboundConfig { return t.config } // Close implements constant.InboundListener func (t *Tunnel) Close() error { var errs []error for _, l := range t.ttl { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) } } for _, l := range t.tul { err := l.Close() if err != nil { errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } // Address implements constant.InboundListener func (t *Tunnel) Address() string { var addrList []string for _, l := range t.ttl { addrList = append(addrList, "tcp://"+l.Address()) } for _, l := range t.tul { addrList = append(addrList, "udp://"+l.Address()) } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (t *Tunnel) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(t.RawAddress(), ",") { for _, network := range t.config.Network { switch network { case "tcp": ttl, err := LT.New(addr, t.config.Target, t.config.SpecialProxy, tunnel, t.Additions()...) if err != nil { return err } t.ttl = append(t.ttl, ttl) case "udp": tul, err := LT.NewUDP(addr, t.config.Target, t.config.SpecialProxy, tunnel, t.Additions()...) if err != nil { return err } t.tul = append(t.tul, tul) default: log.Warnln("unknown network type: %s, passed", network) continue } } } log.Infoln("Tunnel[%s](%s)proxy listening at: %s", t.Name(), t.config.Target, t.Address()) return nil } var _ C.InboundListener = (*Tunnel)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/vless.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing_vless" "github.com/metacubex/mihomo/log" ) type VlessOption struct { BaseOption Users []VlessUser `inbound:"users"` Decryption string `inbound:"decryption,omitempty"` WsPath string `inbound:"ws-path,omitempty"` XHTTPConfig XHTTPConfig `inbound:"xhttp-config,omitempty"` GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` } type VlessUser struct { Username string `inbound:"username,omitempty"` UUID string `inbound:"uuid"` Flow string `inbound:"flow,omitempty"` } type XHTTPConfig struct { Path string `inbound:"path,omitempty"` Host string `inbound:"host,omitempty"` Mode string `inbound:"mode,omitempty"` XPaddingBytes string `inbound:"x-padding-bytes,omitempty"` XPaddingObfsMode bool `inbound:"x-padding-obfs-mode,omitempty"` XPaddingKey string `inbound:"x-padding-key,omitempty"` XPaddingHeader string `inbound:"x-padding-header,omitempty"` XPaddingPlacement string `inbound:"x-padding-placement,omitempty"` XPaddingMethod string `inbound:"x-padding-method,omitempty"` UplinkHTTPMethod string `inbound:"uplink-http-method,omitempty"` SessionPlacement string `inbound:"session-placement,omitempty"` SessionKey string `inbound:"session-key,omitempty"` SeqPlacement string `inbound:"seq-placement,omitempty"` SeqKey string `inbound:"seq-key,omitempty"` UplinkDataPlacement string `inbound:"uplink-data-placement,omitempty"` UplinkDataKey string `inbound:"uplink-data-key,omitempty"` UplinkChunkSize string `inbound:"uplink-chunk-size,omitempty"` NoSSEHeader bool `inbound:"no-sse-header,omitempty"` ScStreamUpServerSecs string `inbound:"sc-stream-up-server-secs,omitempty"` ScMaxBufferedPosts string `inbound:"sc-max-buffered-posts,omitempty"` ScMaxEachPostBytes string `inbound:"sc-max-each-post-bytes,omitempty"` } func (o XHTTPConfig) Build() LC.XHTTPConfig { return LC.XHTTPConfig{ Path: o.Path, Host: o.Host, Mode: o.Mode, NoSSEHeader: o.NoSSEHeader, XPaddingBytes: o.XPaddingBytes, XPaddingObfsMode: o.XPaddingObfsMode, XPaddingKey: o.XPaddingKey, XPaddingHeader: o.XPaddingHeader, XPaddingPlacement: o.XPaddingPlacement, UplinkHTTPMethod: o.UplinkHTTPMethod, SessionPlacement: o.SessionPlacement, SessionKey: o.SessionKey, SeqPlacement: o.SeqPlacement, SeqKey: o.SeqKey, UplinkDataPlacement: o.UplinkDataPlacement, UplinkDataKey: o.UplinkDataKey, UplinkChunkSize: o.UplinkChunkSize, ScStreamUpServerSecs: o.ScStreamUpServerSecs, ScMaxBufferedPosts: o.ScMaxBufferedPosts, ScMaxEachPostBytes: o.ScMaxEachPostBytes, } } func (o VlessOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Vless struct { *Base config *VlessOption l C.MultiAddrListener vs LC.VlessServer } func NewVless(options *VlessOption) (*Vless, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } users := make([]LC.VlessUser, len(options.Users)) for i, v := range options.Users { users[i] = LC.VlessUser{ Username: v.Username, UUID: v.UUID, Flow: v.Flow, } } return &Vless{ Base: base, config: options, vs: LC.VlessServer{ Enable: true, Listen: base.RawAddress(), Users: users, Decryption: options.Decryption, WsPath: options.WsPath, XHTTPConfig: options.XHTTPConfig.Build(), GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), }, }, nil } // Config implements constant.InboundListener func (v *Vless) Config() C.InboundConfig { return v.config } // Address implements constant.InboundListener func (v *Vless) Address() string { var addrList []string if v.l != nil { for _, addr := range v.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (v *Vless) Listen(tunnel C.Tunnel) error { var err error v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...) if err != nil { return err } log.Infoln("Vless[%s] proxy listening at: %s", v.Name(), v.Address()) return nil } // Close implements constant.InboundListener func (v *Vless) Close() error { return v.l.Close() } var _ C.InboundListener = (*Vless)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/vless_test.go ================================================ package inbound_test import ( "net" "net/netip" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/metacubex/mihomo/transport/vless/encryption" "github.com/stretchr/testify/assert" ) func testInboundVless(t *testing.T, inboundOptions inbound.VlessOption, outboundOptions outbound.VlessOption) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "vless_inbound", Listen: "127.0.0.1", Port: "0", } inboundOptions.Users = []inbound.VlessUser{ {Username: "test", UUID: userUUID, Flow: "xtls-rprx-vision"}, } in, err := inbound.NewVless(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "vless_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.UUID = userUUID outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewVless(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) if outboundOptions.Network == "grpc" { // don't test sing-mux over grpc return } testSingMux(t, tunnel, out) } func testInboundVlessTLS(t *testing.T, inboundOptions inbound.VlessOption, outboundOptions outbound.VlessOption, testVision bool) { testInboundVless(t, inboundOptions, outboundOptions) if testVision { t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) } t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundVless(t, inboundOptions, outboundOptions) if testVision { t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) } }) t.Run("mTLS", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey testInboundVless(t, inboundOptions, outboundOptions) if testVision { t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) } }) t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundVless(t, inboundOptions, outboundOptions) if testVision { t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) } }) } func TestInboundVless_TLS(t *testing.T) { inboundOptions := inbound.VlessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, } outboundOptions := outbound.VlessOption{ TLS: true, Fingerprint: tlsFingerprint, } testInboundVlessTLS(t, inboundOptions, outboundOptions, true) } func TestInboundVless_Encryption(t *testing.T) { seedBase64, clientBase64, _, err := encryption.GenMLKEM768("") if err != nil { t.Fatal(err) return } privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") if err != nil { t.Fatal(err) return } paddings := []struct { name string data string }{ {"unconfigured-padding", ""}, {"default-padding", "100-111-1111.75-0-111.50-0-3333."}, {"old-padding", "100-100-1000."}, // Xray-core v25.8.29 {"custom-padding", "100-1234-7890.33-0-1111.66-0-6666.55-111-777."}, } var modes = []string{ "native", "xorpub", "random", } for i := range modes { mode := modes[i] t.Run(mode, func(t *testing.T) { t.Parallel() for i := range paddings { padding := paddings[i].data t.Run(paddings[i].name, func(t *testing.T) { t.Parallel() inboundOptions := inbound.VlessOption{ Decryption: "mlkem768x25519plus." + mode + ".600s." + padding + privateKeyBase64 + "." + seedBase64, } outboundOptions := outbound.VlessOption{ Encryption: "mlkem768x25519plus." + mode + ".0rtt." + padding + passwordBase64 + "." + clientBase64, } t.Run("raw", func(t *testing.T) { testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) }) t.Run("ws", func(t *testing.T) { inboundOptions := inboundOptions inboundOptions.WsPath = "/ws" outboundOptions := outboundOptions outboundOptions.Network = "ws" outboundOptions.WSOpts = outbound.WSOptions{Path: "/ws"} testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) }) t.Run("grpc", func(t *testing.T) { inboundOptions := inboundOptions inboundOptions.GrpcServiceName = "GunService" outboundOptions := outboundOptions outboundOptions.Network = "grpc" outboundOptions.GrpcOpts = outbound.GrpcOptions{GrpcServiceName: "GunService"} testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) }) }) } }) } } func TestInboundVless_Wss1(t *testing.T) { inboundOptions := inbound.VlessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", } outboundOptions := outbound.VlessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "ws", WSOpts: outbound.WSOptions{Path: "/ws"}, } testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Wss2(t *testing.T) { inboundOptions := inbound.VlessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", GrpcServiceName: "GunService", } outboundOptions := outbound.VlessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "ws", WSOpts: outbound.WSOptions{Path: "/ws"}, } testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Grpc1(t *testing.T) { inboundOptions := inbound.VlessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, GrpcServiceName: "GunService", } outboundOptions := outbound.VlessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Grpc2(t *testing.T) { inboundOptions := inbound.VlessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", GrpcServiceName: "GunService", } outboundOptions := outbound.VlessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Reality(t *testing.T) { inboundOptions := inbound.VlessOption{ RealityConfig: inbound.RealityConfig{ Dest: net.JoinHostPort(realityDest, "443"), PrivateKey: realityPrivateKey, ShortID: []string{realityShortid}, ServerNames: []string{realityDest}, }, } outboundOptions := outbound.VlessOption{ TLS: true, ServerName: realityDest, RealityOpts: outbound.RealityOptions{ PublicKey: realityPublickey, ShortID: realityShortid, }, ClientFingerprint: "chrome", } testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) t.Run("X25519MLKEM768", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.RealityOpts.SupportX25519MLKEM768 = true testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) }) } func TestInboundVless_Reality_Grpc(t *testing.T) { inboundOptions := inbound.VlessOption{ RealityConfig: inbound.RealityConfig{ Dest: net.JoinHostPort(realityDest, "443"), PrivateKey: realityPrivateKey, ShortID: []string{realityShortid}, ServerNames: []string{realityDest}, }, GrpcServiceName: "GunService", } outboundOptions := outbound.VlessOption{ TLS: true, ServerName: realityDest, RealityOpts: outbound.RealityOptions{ PublicKey: realityPublickey, ShortID: realityShortid, }, ClientFingerprint: "chrome", Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVless(t, inboundOptions, outboundOptions) t.Run("X25519MLKEM768", func(t *testing.T) { outboundOptions := outboundOptions outboundOptions.RealityOpts.SupportX25519MLKEM768 = true testInboundVless(t, inboundOptions, outboundOptions) }) } func TestInboundVless_XHTTP(t *testing.T) { testCases := []struct { mode string }{ {mode: "auto"}, {mode: "stream-one"}, {mode: "stream-up"}, {mode: "packet-up"}, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.mode, func(t *testing.T) { getConfig := func() (inbound.VlessOption, outbound.VlessOption) { inboundOptions := inbound.VlessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, XHTTPConfig: inbound.XHTTPConfig{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } outboundOptions := outbound.VlessOption{ TLS: true, Fingerprint: tlsFingerprint, ServerName: "example.org", ClientFingerprint: "chrome", Network: "xhttp", XHTTPOpts: outbound.XHTTPOptions{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } return inboundOptions, outboundOptions } testInboundVless_XHTTP(t, getConfig, testCase.mode) }) } } func testInboundVless_XHTTP(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { t.Run("nosplit", func(t *testing.T) { t.Run("single", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() testInboundVlessTLS(t, inboundOptions, outboundOptions, false) }) t.Run("reuse", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) }) }) t.Run("split", func(t *testing.T) { if mode == "stream-one" { // stream-one not supported download settings return } t.Run("single", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} testInboundVlessTLS(t, inboundOptions, outboundOptions, false) }) t.Run("reuse", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) }) }) } func TestInboundVless_XHTTP_Reality(t *testing.T) { testCases := []struct { mode string }{ {mode: "auto"}, {mode: "stream-one"}, {mode: "stream-up"}, {mode: "packet-up"}, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.mode, func(t *testing.T) { getConfig := func() (inbound.VlessOption, outbound.VlessOption) { inboundOptions := inbound.VlessOption{ RealityConfig: inbound.RealityConfig{ Dest: net.JoinHostPort(realityDest, "443"), PrivateKey: realityPrivateKey, ShortID: []string{realityShortid}, ServerNames: []string{realityDest}, }, XHTTPConfig: inbound.XHTTPConfig{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } outboundOptions := outbound.VlessOption{ TLS: true, ServerName: realityDest, RealityOpts: outbound.RealityOptions{ PublicKey: realityPublickey, ShortID: realityShortid, }, ClientFingerprint: "chrome", Network: "xhttp", XHTTPOpts: outbound.XHTTPOptions{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } return inboundOptions, outboundOptions } testInboundVless_XHTTP_Reality(t, getConfig, testCase.mode) }) } } func testInboundVless_XHTTP_Reality(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { t.Run("nosplit", func(t *testing.T) { t.Run("single", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() testInboundVless(t, inboundOptions, outboundOptions) }) t.Run("reuse", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) }) }) t.Run("split", func(t *testing.T) { if mode == "stream-one" { // stream-one not supported download settings return } t.Run("single", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} testInboundVless(t, inboundOptions, outboundOptions) }) t.Run("reuse", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) }) }) } func TestInboundVless_XHTTP_Encryption(t *testing.T) { privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") if err != nil { t.Fatal(err) return } testCases := []struct { mode string }{ {mode: "auto"}, {mode: "stream-one"}, {mode: "stream-up"}, {mode: "packet-up"}, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.mode, func(t *testing.T) { getConfig := func() (inbound.VlessOption, outbound.VlessOption) { inboundOptions := inbound.VlessOption{ Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, XHTTPConfig: inbound.XHTTPConfig{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } outboundOptions := outbound.VlessOption{ Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, Network: "xhttp", XHTTPOpts: outbound.XHTTPOptions{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } return inboundOptions, outboundOptions } testInboundVless_XHTTP_Encryption(t, getConfig, testCase.mode) }) } } func testInboundVless_XHTTP_Encryption(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { t.Run("nosplit", func(t *testing.T) { t.Run("single", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() testInboundVless(t, inboundOptions, outboundOptions) }) t.Run("reuse", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) }) }) t.Run("split", func(t *testing.T) { if mode == "stream-one" { // stream-one not supported download settings return } t.Run("single", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} testInboundVless(t, inboundOptions, outboundOptions) }) t.Run("reuse", func(t *testing.T) { inboundOptions, outboundOptions := getConfig() outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) }) }) } func TestInboundVless_XHTTP_H1(t *testing.T) { testCases := []struct { mode string }{ {mode: "auto"}, {mode: "stream-one"}, {mode: "stream-up"}, {mode: "packet-up"}, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.mode, func(t *testing.T) { getConfig := func() (inbound.VlessOption, outbound.VlessOption) { inboundOptions := inbound.VlessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, XHTTPConfig: inbound.XHTTPConfig{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } outboundOptions := outbound.VlessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "xhttp", ALPN: []string{"http/1.1"}, XHTTPOpts: outbound.XHTTPOptions{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } return inboundOptions, outboundOptions } testInboundVless_XHTTP(t, getConfig, testCase.mode) }) } } func TestInboundVless_XHTTP_H1_Encryption(t *testing.T) { privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") if err != nil { t.Fatal(err) return } testCases := []struct { mode string }{ {mode: "auto"}, {mode: "stream-one"}, {mode: "stream-up"}, {mode: "packet-up"}, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.mode, func(t *testing.T) { getConfig := func() (inbound.VlessOption, outbound.VlessOption) { inboundOptions := inbound.VlessOption{ Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, XHTTPConfig: inbound.XHTTPConfig{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } outboundOptions := outbound.VlessOption{ Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, Network: "xhttp", ALPN: []string{"http/1.1"}, XHTTPOpts: outbound.XHTTPOptions{ Path: "/vless-xhttp", Host: "example.com", Mode: testCase.mode, }, } return inboundOptions, outboundOptions } testInboundVless_XHTTP_Encryption(t, getConfig, testCase.mode) }) } } func withXHTTPReuse(out outbound.VlessOption) outbound.VlessOption { out.XHTTPOpts.ReuseSettings = &outbound.XHTTPReuseSettings{ MaxConnections: "0", MaxConcurrency: "16-32", CMaxReuseTimes: "0", HMaxRequestTimes: "600-900", HMaxReusableSecs: "1800-3000", } if out.XHTTPOpts.DownloadSettings != nil { out.XHTTPOpts.DownloadSettings.ReuseSettings = &outbound.XHTTPReuseSettings{ MaxConnections: "0", MaxConcurrency: "16-32", CMaxReuseTimes: "0", HMaxRequestTimes: "600-900", HMaxReusableSecs: "1800-3000", } } return out } ================================================ FILE: core/Clash.Meta/listener/inbound/vmess.go ================================================ package inbound import ( "strings" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing_vmess" "github.com/metacubex/mihomo/log" ) type VmessOption struct { BaseOption Users []VmessUser `inbound:"users"` WsPath string `inbound:"ws-path,omitempty"` GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` ClientAuthType string `inbound:"client-auth-type,omitempty"` ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` } type VmessUser struct { Username string `inbound:"username,omitempty"` UUID string `inbound:"uuid"` AlterID int `inbound:"alterId,omitempty"` } func (o VmessOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } type Vmess struct { *Base config *VmessOption l C.MultiAddrListener vs LC.VmessServer } func NewVmess(options *VmessOption) (*Vmess, error) { base, err := NewBase(&options.BaseOption) if err != nil { return nil, err } users := make([]LC.VmessUser, len(options.Users)) for i, v := range options.Users { users[i] = LC.VmessUser{ Username: v.Username, UUID: v.UUID, AlterID: v.AlterID, } } return &Vmess{ Base: base, config: options, vs: LC.VmessServer{ Enable: true, Listen: base.RawAddress(), Users: users, WsPath: options.WsPath, GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, ClientAuthType: options.ClientAuthType, ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), }, }, nil } // Config implements constant.InboundListener func (v *Vmess) Config() C.InboundConfig { return v.config } // Address implements constant.InboundListener func (v *Vmess) Address() string { var addrList []string if v.l != nil { for _, addr := range v.l.AddrList() { addrList = append(addrList, addr.String()) } } return strings.Join(addrList, ",") } // Listen implements constant.InboundListener func (v *Vmess) Listen(tunnel C.Tunnel) error { var err error v.l, err = sing_vmess.New(v.vs, tunnel, v.Additions()...) if err != nil { return err } log.Infoln("Vmess[%s] proxy listening at: %s", v.Name(), v.Address()) return nil } // Close implements constant.InboundListener func (v *Vmess) Close() error { return v.l.Close() } var _ C.InboundListener = (*Vmess)(nil) ================================================ FILE: core/Clash.Meta/listener/inbound/vmess_test.go ================================================ package inbound_test import ( "net" "net/netip" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" "github.com/stretchr/testify/assert" ) func testInboundVMess(t *testing.T, inboundOptions inbound.VmessOption, outboundOptions outbound.VmessOption) { t.Parallel() inboundOptions.BaseOption = inbound.BaseOption{ NameStr: "vmess_inbound", Listen: "127.0.0.1", Port: "0", } inboundOptions.Users = []inbound.VmessUser{ {Username: "test", UUID: userUUID, AlterID: 0}, } in, err := inbound.NewVmess(&inboundOptions) if !assert.NoError(t, err) { return } tunnel := NewHttpTestTunnel() defer tunnel.Close() err = in.Listen(tunnel) if !assert.NoError(t, err) { return } defer in.Close() addrPort, err := netip.ParseAddrPort(in.Address()) if !assert.NoError(t, err) { return } outboundOptions.Name = "vmess_outbound" outboundOptions.Server = addrPort.Addr().String() outboundOptions.Port = int(addrPort.Port()) outboundOptions.UUID = userUUID outboundOptions.AlterID = 0 outboundOptions.Cipher = "auto" outboundOptions.DialerForAPI = tunnel.NewDialer() out, err := outbound.NewVmess(outboundOptions) if !assert.NoError(t, err) { return } defer out.Close() tunnel.DoTest(t, out) if outboundOptions.Network == "grpc" { // don't test sing-mux over grpc return } testSingMux(t, tunnel, out) } func TestInboundVMess_Basic(t *testing.T) { inboundOptions := inbound.VmessOption{} outboundOptions := outbound.VmessOption{} testInboundVMess(t, inboundOptions, outboundOptions) } func testInboundVMessTLS(t *testing.T, inboundOptions inbound.VmessOption, outboundOptions outbound.VmessOption) { testInboundVMess(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundVMess(t, inboundOptions, outboundOptions) }) t.Run("mTLS", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey testInboundVMess(t, inboundOptions, outboundOptions) }) t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions inboundOptions.ClientAuthCert = tlsAuthCertificate outboundOptions.Certificate = tlsAuthCertificate outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundVMess(t, inboundOptions, outboundOptions) }) } func TestInboundVMess_TLS(t *testing.T) { inboundOptions := inbound.VmessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, } outboundOptions := outbound.VmessOption{ TLS: true, Fingerprint: tlsFingerprint, } testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Ws(t *testing.T) { inboundOptions := inbound.VmessOption{ WsPath: "/ws", } outboundOptions := outbound.VmessOption{ Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", }, } testInboundVMess(t, inboundOptions, outboundOptions) } func TestInboundVMess_Ws_ed1(t *testing.T) { inboundOptions := inbound.VmessOption{ WsPath: "/ws", } outboundOptions := outbound.VmessOption{ Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws?ed=2048", }, } testInboundVMess(t, inboundOptions, outboundOptions) } func TestInboundVMess_Ws_ed2(t *testing.T) { inboundOptions := inbound.VmessOption{ WsPath: "/ws", } outboundOptions := outbound.VmessOption{ Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", MaxEarlyData: 2048, EarlyDataHeaderName: "Sec-WebSocket-Protocol", }, } testInboundVMess(t, inboundOptions, outboundOptions) } func TestInboundVMess_Ws_Upgrade1(t *testing.T) { inboundOptions := inbound.VmessOption{ WsPath: "/ws", } outboundOptions := outbound.VmessOption{ Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", V2rayHttpUpgrade: true, }, } testInboundVMess(t, inboundOptions, outboundOptions) } func TestInboundVMess_Ws_Upgrade2(t *testing.T) { inboundOptions := inbound.VmessOption{ WsPath: "/ws", } outboundOptions := outbound.VmessOption{ Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", V2rayHttpUpgrade: true, V2rayHttpUpgradeFastOpen: true, }, } testInboundVMess(t, inboundOptions, outboundOptions) } func TestInboundVMess_Wss1(t *testing.T) { inboundOptions := inbound.VmessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", } outboundOptions := outbound.VmessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", }, } testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Wss2(t *testing.T) { inboundOptions := inbound.VmessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", GrpcServiceName: "GunService", } outboundOptions := outbound.VmessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "ws", WSOpts: outbound.WSOptions{ Path: "/ws", }, } testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Grpc1(t *testing.T) { inboundOptions := inbound.VmessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, GrpcServiceName: "GunService", } outboundOptions := outbound.VmessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Grpc2(t *testing.T) { inboundOptions := inbound.VmessOption{ Certificate: tlsCertificate, PrivateKey: tlsPrivateKey, WsPath: "/ws", GrpcServiceName: "GunService", } outboundOptions := outbound.VmessOption{ TLS: true, Fingerprint: tlsFingerprint, Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Reality(t *testing.T) { inboundOptions := inbound.VmessOption{ RealityConfig: inbound.RealityConfig{ Dest: net.JoinHostPort(realityDest, "443"), PrivateKey: realityPrivateKey, ShortID: []string{realityShortid}, ServerNames: []string{realityDest}, }, } outboundOptions := outbound.VmessOption{ TLS: true, ServerName: realityDest, RealityOpts: outbound.RealityOptions{ PublicKey: realityPublickey, ShortID: realityShortid, }, ClientFingerprint: "chrome", } testInboundVMess(t, inboundOptions, outboundOptions) } func TestInboundVMess_Reality_Grpc(t *testing.T) { inboundOptions := inbound.VmessOption{ RealityConfig: inbound.RealityConfig{ Dest: net.JoinHostPort(realityDest, "443"), PrivateKey: realityPrivateKey, ShortID: []string{realityShortid}, ServerNames: []string{realityDest}, }, GrpcServiceName: "GunService", } outboundOptions := outbound.VmessOption{ TLS: true, ServerName: realityDest, RealityOpts: outbound.RealityOptions{ PublicKey: realityPublickey, ShortID: realityShortid, }, ClientFingerprint: "chrome", Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVMess(t, inboundOptions, outboundOptions) } ================================================ FILE: core/Clash.Meta/listener/inner/tcp.go ================================================ package inner import ( "errors" "net" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" ) var tunnel C.Tunnel func New(t C.Tunnel) { tunnel = t } func GetTunnel() C.Tunnel { return tunnel } func HandleTcp(tunnel C.Tunnel, address string, proxy string) (conn net.Conn, err error) { if tunnel == nil { return nil, errors.New("tunnel uninitialized") } // executor Parsed conn1, conn2 := N.Pipe() metadata := &C.Metadata{} metadata.NetWork = C.TCP metadata.Type = C.INNER metadata.DNSMode = C.DNSNormal metadata.Process = C.MihomoName if proxy != "" { metadata.SpecialProxy = proxy } if err = metadata.SetRemoteAddress(address); err != nil { return nil, err } go tunnel.HandleTCPConn(conn2, metadata) return conn1, nil } ================================================ FILE: core/Clash.Meta/listener/listener.go ================================================ package listener import ( "fmt" "net" "strconv" "strings" "sync" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/mixed" "github.com/metacubex/mihomo/listener/redir" embedSS "github.com/metacubex/mihomo/listener/shadowsocks" "github.com/metacubex/mihomo/listener/sing_shadowsocks" "github.com/metacubex/mihomo/listener/sing_tun" "github.com/metacubex/mihomo/listener/sing_vmess" "github.com/metacubex/mihomo/listener/socks" "github.com/metacubex/mihomo/listener/tproxy" "github.com/metacubex/mihomo/listener/tuic" LT "github.com/metacubex/mihomo/listener/tunnel" "github.com/metacubex/mihomo/log" "github.com/samber/lo" ) var ( allowLan = false bindAddress = "*" socksListener *socks.Listener socksUDPListener *socks.UDPListener httpListener *http.Listener redirListener *redir.Listener redirUDPListener *tproxy.UDPListener tproxyListener *tproxy.Listener tproxyUDPListener *tproxy.UDPListener mixedListener *mixed.Listener mixedUDPLister *socks.UDPListener tunnelTCPListeners = map[string]*LT.Listener{} tunnelUDPListeners = map[string]*LT.PacketConn{} inboundListeners = map[string]C.InboundListener{} tunLister *sing_tun.Listener shadowSocksListener C.MultiAddrListener vmessListener *sing_vmess.Listener tuicListener *tuic.Listener // lock for recreate function socksMux sync.Mutex httpMux sync.Mutex redirMux sync.Mutex tproxyMux sync.Mutex mixedMux sync.Mutex tunnelMux sync.Mutex inboundMux sync.Mutex tunMux sync.Mutex ssMux sync.Mutex vmessMux sync.Mutex tuicMux sync.Mutex LastTunConf LC.Tun LastTuicConf LC.TuicServer ) type Ports 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"` ShadowSocksConfig string `json:"ss-config"` VmessConfig string `json:"vmess-config"` } func GetTunConf() LC.Tun { if tunLister == nil { return LastTunConf } return tunLister.Config() } func GetTuicConf() LC.TuicServer { if tuicListener == nil { return LC.TuicServer{Enable: false} } return tuicListener.Config() } func AllowLan() bool { return allowLan } func BindAddress() string { return bindAddress } func SetAllowLan(al bool) { allowLan = al } func SetBindAddress(host string) { bindAddress = host } func ReCreateHTTP(port int, tunnel C.Tunnel) { httpMux.Lock() defer httpMux.Unlock() var err error defer func() { if err != nil { log.Errorln("Start HTTP server error: %s", err.Error()) } }() addr := genAddr(bindAddress, port, allowLan) if httpListener != nil { if httpListener.RawAddress() == addr { return } httpListener.Close() httpListener = nil } if portIsZero(addr) { return } httpListener, err = http.New(addr, tunnel) if err != nil { log.Errorln("Start HTTP server error: %s", err.Error()) return } log.Infoln("HTTP proxy listening at: %s", httpListener.Address()) } func ReCreateSocks(port int, tunnel C.Tunnel) { socksMux.Lock() defer socksMux.Unlock() var err error defer func() { if err != nil { log.Errorln("Start SOCKS server error: %s", err.Error()) } }() addr := genAddr(bindAddress, port, allowLan) shouldTCPIgnore := false shouldUDPIgnore := false if socksListener != nil { if socksListener.RawAddress() != addr { socksListener.Close() socksListener = nil } else { shouldTCPIgnore = true } } if socksUDPListener != nil { if socksUDPListener.RawAddress() != addr { socksUDPListener.Close() socksUDPListener = nil } else { shouldUDPIgnore = true } } if shouldTCPIgnore && shouldUDPIgnore { return } if portIsZero(addr) { return } tcpListener, err := socks.New(addr, tunnel) if err != nil { return } udpListener, err := socks.NewUDP(addr, tunnel) if err != nil { tcpListener.Close() return } socksListener = tcpListener socksUDPListener = udpListener log.Infoln("SOCKS proxy listening at: %s", socksListener.Address()) } func ReCreateRedir(port int, tunnel C.Tunnel) { redirMux.Lock() defer redirMux.Unlock() var err error defer func() { if err != nil { log.Errorln("Start Redir server error: %s", err.Error()) } }() addr := genAddr(bindAddress, port, allowLan) if redirListener != nil { if redirListener.RawAddress() == addr { return } redirListener.Close() redirListener = nil } if redirUDPListener != nil { if redirUDPListener.RawAddress() == addr { return } redirUDPListener.Close() redirUDPListener = nil } if portIsZero(addr) { return } redirListener, err = redir.New(addr, tunnel) if err != nil { return } redirUDPListener, err = tproxy.NewUDP(addr, tunnel) if err != nil { log.Warnln("Failed to start Redir UDP Listener: %s", err) } log.Infoln("Redirect proxy listening at: %s", redirListener.Address()) } func ReCreateShadowSocks(shadowSocksConfig string, tunnel C.Tunnel) { ssMux.Lock() defer ssMux.Unlock() var err error defer func() { if err != nil { log.Errorln("Start ShadowSocks server error: %s", err.Error()) } }() var ssConfig LC.ShadowsocksServer if addr, cipher, password, err := embedSS.ParseSSURL(shadowSocksConfig); err == nil { ssConfig = LC.ShadowsocksServer{ Enable: len(shadowSocksConfig) > 0, Listen: addr, Password: password, Cipher: cipher, Udp: true, } } shouldIgnore := false if shadowSocksListener != nil { if shadowSocksListener.Config() != ssConfig.String() { shadowSocksListener.Close() shadowSocksListener = nil } else { shouldIgnore = true } } if shouldIgnore { return } if !ssConfig.Enable { return } listener, err := sing_shadowsocks.New(ssConfig, tunnel) if err != nil { return } shadowSocksListener = listener for _, addr := range shadowSocksListener.AddrList() { log.Infoln("ShadowSocks proxy listening at: %s", addr.String()) } return } func ReCreateVmess(vmessConfig string, tunnel C.Tunnel) { vmessMux.Lock() defer vmessMux.Unlock() var err error defer func() { if err != nil { log.Errorln("Start Vmess server error: %s", err.Error()) } }() var vsConfig LC.VmessServer if addr, username, password, err := sing_vmess.ParseVmessURL(vmessConfig); err == nil { vsConfig = LC.VmessServer{ Enable: len(vmessConfig) > 0, Listen: addr, Users: []LC.VmessUser{{Username: username, UUID: password, AlterID: 1}}, } } shouldIgnore := false if vmessListener != nil { if vmessListener.Config() != vsConfig.String() { vmessListener.Close() vmessListener = nil } else { shouldIgnore = true } } if shouldIgnore { return } if !vsConfig.Enable { return } listener, err := sing_vmess.New(vsConfig, tunnel) if err != nil { return } vmessListener = listener for _, addr := range vmessListener.AddrList() { log.Infoln("Vmess proxy listening at: %s", addr.String()) } return } func ReCreateTuic(config LC.TuicServer, tunnel C.Tunnel) { tuicMux.Lock() defer func() { LastTuicConf = config tuicMux.Unlock() }() shouldIgnore := false var err error defer func() { if err != nil { log.Errorln("Start Tuic server error: %s", err.Error()) } }() if tuicListener != nil { if tuicListener.Config().String() != config.String() { tuicListener.Close() tuicListener = nil } else { shouldIgnore = true } } if shouldIgnore { return } if !config.Enable { return } listener, err := tuic.New(config, tunnel) if err != nil { return } tuicListener = listener for _, addr := range tuicListener.AddrList() { log.Infoln("Tuic proxy listening at: %s", addr.String()) } return } func ReCreateTProxy(port int, tunnel C.Tunnel) { tproxyMux.Lock() defer tproxyMux.Unlock() var err error defer func() { if err != nil { log.Errorln("Start TProxy server error: %s", err.Error()) } }() addr := genAddr(bindAddress, port, allowLan) if tproxyListener != nil { if tproxyListener.RawAddress() == addr { return } tproxyListener.Close() tproxyListener = nil } if tproxyUDPListener != nil { if tproxyUDPListener.RawAddress() == addr { return } tproxyUDPListener.Close() tproxyUDPListener = nil } if portIsZero(addr) { return } tproxyListener, err = tproxy.New(addr, tunnel) if err != nil { return } tproxyUDPListener, err = tproxy.NewUDP(addr, tunnel) if err != nil { log.Warnln("Failed to start TProxy UDP Listener: %s", err) } log.Infoln("TProxy server listening at: %s", tproxyListener.Address()) } func ReCreateMixed(port int, tunnel C.Tunnel) { mixedMux.Lock() defer mixedMux.Unlock() var err error defer func() { if err != nil { log.Errorln("Start Mixed(http+socks) server error: %s", err.Error()) } }() addr := genAddr(bindAddress, port, allowLan) shouldTCPIgnore := false shouldUDPIgnore := false if mixedListener != nil { if mixedListener.RawAddress() != addr { mixedListener.Close() mixedListener = nil } else { shouldTCPIgnore = true } } if mixedUDPLister != nil { if mixedUDPLister.RawAddress() != addr { mixedUDPLister.Close() mixedUDPLister = nil } else { shouldUDPIgnore = true } } if shouldTCPIgnore && shouldUDPIgnore { return } if portIsZero(addr) { return } mixedListener, err = mixed.New(addr, tunnel) if err != nil { return } mixedUDPLister, err = socks.NewUDP(addr, tunnel) if err != nil { mixedListener.Close() return } log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address()) } func ReCreateTun(tunConf LC.Tun, tunnel C.Tunnel) { tunConf.Sort() tunMux.Lock() defer func() { LastTunConf = tunConf tunMux.Unlock() }() var err error defer func() { if err != nil { log.Errorln("Start TUN listening error: %s", err.Error()) tunConf.Enable = false } }() if tunConf.Equal(LastTunConf) { if tunLister != nil { // some default value in dialer maybe changed when config reload, reset at here tunLister.OnReload() } return } closeTunListener() if !tunConf.Enable { return } lister, err := sing_tun.New(tunConf, tunnel) if err != nil { return } tunLister = lister log.Infoln("[TUN] Tun adapter listening at: %s", tunLister.Address()) } func PatchTunnel(tunnels []LC.Tunnel, tunnel C.Tunnel) { tunnelMux.Lock() defer tunnelMux.Unlock() type addrProxy struct { network string addr string target string proxy string } tcpOld := lo.Map( lo.Keys(tunnelTCPListeners), func(key string, _ int) addrProxy { parts := strings.Split(key, "/") return addrProxy{ network: "tcp", addr: parts[0], target: parts[1], proxy: parts[2], } }, ) udpOld := lo.Map( lo.Keys(tunnelUDPListeners), func(key string, _ int) addrProxy { parts := strings.Split(key, "/") return addrProxy{ network: "udp", addr: parts[0], target: parts[1], proxy: parts[2], } }, ) oldElm := lo.Union(tcpOld, udpOld) newElm := lo.FlatMap( tunnels, func(tunnel LC.Tunnel, _ int) []addrProxy { return lo.Map( tunnel.Network, func(network string, _ int) addrProxy { return addrProxy{ network: network, addr: tunnel.Address, target: tunnel.Target, proxy: tunnel.Proxy, } }, ) }, ) needClose, needCreate := lo.Difference(oldElm, newElm) for _, elm := range needClose { key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) if elm.network == "tcp" { tunnelTCPListeners[key].Close() delete(tunnelTCPListeners, key) } else { tunnelUDPListeners[key].Close() delete(tunnelUDPListeners, key) } } for _, elm := range needCreate { key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) if elm.network == "tcp" { l, err := LT.New(elm.addr, elm.target, elm.proxy, tunnel) if err != nil { log.Errorln("Start tunnel %s error: %s", elm.target, err.Error()) continue } tunnelTCPListeners[key] = l log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address()) } else { l, err := LT.NewUDP(elm.addr, elm.target, elm.proxy, tunnel) if err != nil { log.Errorln("Start tunnel %s error: %s", elm.target, err.Error()) continue } tunnelUDPListeners[key] = l log.Infoln("Tunnel(udp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelUDPListeners[key].Address()) } } } func PatchInboundListeners(newListenerMap map[string]C.InboundListener, tunnel C.Tunnel, dropOld bool) { inboundMux.Lock() defer inboundMux.Unlock() for name, newListener := range newListenerMap { if oldListener, ok := inboundListeners[name]; ok { if !oldListener.Config().Equal(newListener.Config()) { _ = oldListener.Close() } else { continue } } if err := newListener.Listen(tunnel); err != nil { log.Errorln("Listener %s listen err: %s", name, err.Error()) continue } inboundListeners[name] = newListener } if dropOld { for name, oldListener := range inboundListeners { if _, ok := newListenerMap[name]; !ok { _ = oldListener.Close() delete(inboundListeners, name) } } } } // GetPorts return the ports of proxy servers func GetPorts() *Ports { ports := &Ports{} if httpListener != nil { _, portStr, _ := net.SplitHostPort(httpListener.Address()) port, _ := strconv.Atoi(portStr) ports.Port = port } if socksListener != nil { _, portStr, _ := net.SplitHostPort(socksListener.Address()) port, _ := strconv.Atoi(portStr) ports.SocksPort = port } if redirListener != nil { _, portStr, _ := net.SplitHostPort(redirListener.Address()) port, _ := strconv.Atoi(portStr) ports.RedirPort = port } if tproxyListener != nil { _, portStr, _ := net.SplitHostPort(tproxyListener.Address()) port, _ := strconv.Atoi(portStr) ports.TProxyPort = port } if mixedListener != nil { _, portStr, _ := net.SplitHostPort(mixedListener.Address()) port, _ := strconv.Atoi(portStr) ports.MixedPort = port } if shadowSocksListener != nil { ports.ShadowSocksConfig = shadowSocksListener.Config() } if vmessListener != nil { ports.VmessConfig = vmessListener.Config() } return ports } func portIsZero(addr string) bool { _, port, err := net.SplitHostPort(addr) if port == "0" || port == "" || err != nil { return true } return false } func genAddr(host string, port int, allowLan bool) string { if allowLan { if host == "*" { return fmt.Sprintf(":%d", port) } return fmt.Sprintf("%s:%d", host, port) } return fmt.Sprintf("127.0.0.1:%d", port) } func closeTunListener() { if tunLister != nil { tunLister.Close() tunLister = nil } } func Cleanup() { closeTunListener() } ================================================ FILE: core/Clash.Meta/listener/mieru/server.go ================================================ package mieru import ( "errors" "io" "net" "net/netip" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" mierucommon "github.com/enfein/mieru/v3/apis/common" mieruconstant "github.com/enfein/mieru/v3/apis/constant" mierumodel "github.com/enfein/mieru/v3/apis/model" ) func Handle(conn net.Conn, tunnel C.Tunnel, request *mierumodel.Request, additions ...inbound.Addition) { // Return a fake response to the client. resp := &mierumodel.Response{ Reply: mieruconstant.Socks5ReplySuccess, BindAddr: mierumodel.AddrSpec{ IP: net.IPv4zero, Port: 0, }, } if err := resp.WriteToSocks5(conn); err != nil { conn.Close() return } // Handle the connection with tunnel. switch request.Command { case mieruconstant.Socks5ConnectCmd: // TCP metadata := &C.Metadata{ NetWork: C.TCP, Type: C.MIERU, DstPort: uint16(request.DstAddr.Port), } if request.DstAddr.FQDN != "" { metadata.Host = request.DstAddr.FQDN } else if request.DstAddr.IP != nil { metadata.DstIP, _ = netip.AddrFromSlice(request.DstAddr.IP) metadata.DstIP = metadata.DstIP.Unmap() } inbound.ApplyAdditions( metadata, inbound.WithInName(conn.(mierucommon.UserContext).UserName()), inbound.WithSrcAddr(conn.RemoteAddr()), inbound.WithInAddr(conn.LocalAddr()), ) inbound.ApplyAdditions(metadata, additions...) tunnel.HandleTCPConn(conn, metadata) case mieruconstant.Socks5UDPAssociateCmd: // UDP pc := mierucommon.NewPacketOverStreamTunnel(conn) ep := N.NewEnhancePacketConn(pc) for { data, put, addr, err := ep.WaitReadFrom() if err != nil { if put != nil { // Unresolved UDP packet, return buffer to the pool. put() } // mieru returns EOF or ErrUnexpectedEOF when a session is closed. if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.ErrClosedPipe) { break } continue } target, payload, err := socks5.DecodeUDPPacket(data) if err != nil { return } packet := &packet{ pc: ep, addr: addr, payload: payload, put: put, } tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.MIERU, additions...)) } } } type packet struct { pc net.PacketConn addr net.Addr // source (i.e. remote) IP & Port of the packet payload []byte put func() } var _ C.UDPPacket = (*packet)(nil) var _ C.UDPPacketInAddr = (*packet)(nil) func (c *packet) Data() []byte { return c.payload } func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) if err != nil { return } return c.pc.WriteTo(packet, c.addr) } func (c *packet) Drop() { if c.put != nil { c.put() c.put = nil } c.payload = nil } func (c *packet) LocalAddr() net.Addr { return c.addr } func (c *packet) InAddr() net.Addr { return c.pc.LocalAddr() } ================================================ FILE: core/Clash.Meta/listener/mixed/mixed.go ================================================ package mixed import ( "errors" "net" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/socks" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/socks4" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/tls" ) type Listener struct { listener net.Listener addr string closed bool } // RawAddress implements C.Listener func (l *Listener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *Listener) Address() string { return l.listener.Addr().String() } // Close implements C.Listener func (l *Listener) Close() error { l.closed = true return l.listener.Close() } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: authStore.Default}, tunnel, additions...) } func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { isDefault := false if len(additions) == 0 { isDefault = true additions = []inbound.Addition{ inbound.WithInName("DEFAULT-MIXED"), inbound.WithSpecialRules(""), } } l, err := inbound.Listen("tcp", config.Listen) if err != nil { return nil, err } tlsConfig := &tls.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { return nil, errors.New("client-auth is unavailable in reality") } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err } } if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } ml := &Listener{ listener: l, addr: config.Listen, } go func() { for { c, err := ml.listener.Accept() if err != nil { if ml.closed { break } continue } store := config.AuthStore if isDefault || store == authStore.Default { // only apply on default listener if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { _ = c.Close() continue } if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) { store = authStore.Nil } } go handleConn(c, tunnel, store, additions...) } }() return ml, nil } func handleConn(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { bufConn := N.NewBufferedConn(conn) head, err := bufConn.Peek(1) if err != nil { conn.Close() return } switch head[0] { case socks4.Version: socks.HandleSocks4(bufConn, tunnel, store, additions...) case socks5.Version: socks.HandleSocks5(bufConn, tunnel, store, additions...) default: http.HandleConn(bufConn, tunnel, store, additions...) } } ================================================ FILE: core/Clash.Meta/listener/parse.go ================================================ package listener import ( "fmt" "github.com/metacubex/mihomo/common/structure" C "github.com/metacubex/mihomo/constant" IN "github.com/metacubex/mihomo/listener/inbound" ) func ParseListener(mapping map[string]any) (C.InboundListener, error) { decoder := structure.NewDecoder(structure.Option{TagName: "inbound", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) proxyType, existType := mapping["type"].(string) if !existType { return nil, fmt.Errorf("missing type") } var ( listener C.InboundListener err error ) switch proxyType { case "socks": socksOption := &IN.SocksOption{UDP: true} err = decoder.Decode(mapping, socksOption) if err != nil { return nil, err } listener, err = IN.NewSocks(socksOption) case "http": httpOption := &IN.HTTPOption{} err = decoder.Decode(mapping, httpOption) if err != nil { return nil, err } listener, err = IN.NewHTTP(httpOption) case "tproxy": tproxyOption := &IN.TProxyOption{UDP: true} err = decoder.Decode(mapping, tproxyOption) if err != nil { return nil, err } listener, err = IN.NewTProxy(tproxyOption) case "redir": redirOption := &IN.RedirOption{} err = decoder.Decode(mapping, redirOption) if err != nil { return nil, err } listener, err = IN.NewRedir(redirOption) case "mixed": mixedOption := &IN.MixedOption{UDP: true} err = decoder.Decode(mapping, mixedOption) if err != nil { return nil, err } listener, err = IN.NewMixed(mixedOption) case "tunnel": tunnelOption := &IN.TunnelOption{} err = decoder.Decode(mapping, tunnelOption) if err != nil { return nil, err } listener, err = IN.NewTunnel(tunnelOption) case "tun": tunOption := &IN.TunOption{ Stack: C.TunGvisor, DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query } err = decoder.Decode(mapping, tunOption) if err != nil { return nil, err } listener, err = IN.NewTun(tunOption) case "shadowsocks": shadowsocksOption := &IN.ShadowSocksOption{UDP: true} err = decoder.Decode(mapping, shadowsocksOption) if err != nil { return nil, err } listener, err = IN.NewShadowSocks(shadowsocksOption) case "vmess": vmessOption := &IN.VmessOption{} err = decoder.Decode(mapping, vmessOption) if err != nil { return nil, err } listener, err = IN.NewVmess(vmessOption) case "vless": vlessOption := &IN.VlessOption{} err = decoder.Decode(mapping, vlessOption) if err != nil { return nil, err } listener, err = IN.NewVless(vlessOption) case "trojan": trojanOption := &IN.TrojanOption{} err = decoder.Decode(mapping, trojanOption) if err != nil { return nil, err } listener, err = IN.NewTrojan(trojanOption) case "hysteria2": hysteria2Option := &IN.Hysteria2Option{} err = decoder.Decode(mapping, hysteria2Option) if err != nil { return nil, err } listener, err = IN.NewHysteria2(hysteria2Option) case "tuic": tuicOption := &IN.TuicOption{ MaxIdleTime: 15000, AuthenticationTimeout: 1000, ALPN: []string{"h3"}, MaxUdpRelayPacketSize: 1500, CongestionController: "bbr", } err = decoder.Decode(mapping, tuicOption) if err != nil { return nil, err } listener, err = IN.NewTuic(tuicOption) case "anytls": anytlsOption := &IN.AnyTLSOption{} err = decoder.Decode(mapping, anytlsOption) if err != nil { return nil, err } listener, err = IN.NewAnyTLS(anytlsOption) case "mieru": mieruOption := &IN.MieruOption{} err = decoder.Decode(mapping, mieruOption) if err != nil { return nil, err } listener, err = IN.NewMieru(mieruOption) case "sudoku": sudokuOption := &IN.SudokuOption{} err = decoder.Decode(mapping, sudokuOption) if err != nil { return nil, err } listener, err = IN.NewSudoku(sudokuOption) case "trusttunnel": trusttunnelOption := &IN.TrustTunnelOption{} err = decoder.Decode(mapping, trusttunnelOption) if err != nil { return nil, err } listener, err = IN.NewTrustTunnel(trusttunnelOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } return listener, err } ================================================ FILE: core/Clash.Meta/listener/patch.go ================================================ package listener func StopListener() { if socksListener != nil { _ = socksListener.Close() socksListener = nil } if socksUDPListener != nil { _ = socksUDPListener.Close() socksUDPListener = nil } if httpListener != nil { _ = httpListener.Close() httpListener = nil } if redirListener != nil { _ = redirListener.Close() redirListener = nil } if redirUDPListener != nil { _ = redirUDPListener.Close() redirUDPListener = nil } if tproxyListener != nil { _ = tproxyListener.Close() tproxyListener = nil } if tproxyUDPListener != nil { _ = tproxyUDPListener.Close() tproxyUDPListener = nil } if mixedListener != nil { _ = mixedListener.Close() mixedListener = nil } if mixedUDPLister != nil { _ = mixedUDPLister.Close() mixedUDPLister = nil } if tunLister != nil { _ = tunLister.Close() tunLister = nil } if shadowSocksListener != nil { _ = shadowSocksListener.Close() shadowSocksListener = nil } if shadowSocksListener != nil { _ = shadowSocksListener.Close() shadowSocksListener = nil } if vmessListener != nil { _ = vmessListener.Close() vmessListener = nil } if tuicListener != nil { _ = tuicListener.Close() tuicListener = nil } } ================================================ FILE: core/Clash.Meta/listener/reality/reality.go ================================================ package reality import ( "context" "encoding/base64" "encoding/hex" "errors" "fmt" "net" "runtime/debug" "time" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/inner" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" utls "github.com/metacubex/utls" ) type Conn = utls.Conn type LimitFallback = utls.RealityLimitFallback type Config struct { Dest string PrivateKey string ShortID []string ServerNames []string MaxTimeDifference int Proxy string LimitFallbackUpload LimitFallback LimitFallbackDownload LimitFallback } func (c Config) Build(tunnel C.Tunnel) (*Builder, error) { realityConfig := &utls.RealityConfig{} realityConfig.SessionTicketsDisabled = true realityConfig.Type = "tcp" realityConfig.Dest = c.Dest realityConfig.Time = ntp.Now realityConfig.ServerNames = make(map[string]bool) realityConfig.Log = log.Debugln for _, it := range c.ServerNames { realityConfig.ServerNames[it] = true } privateKey, err := base64.RawURLEncoding.DecodeString(c.PrivateKey) if err != nil { return nil, fmt.Errorf("decode private key: %w", err) } if len(privateKey) != 32 { return nil, errors.New("invalid private key") } realityConfig.PrivateKey = privateKey realityConfig.MaxTimeDiff = time.Duration(c.MaxTimeDifference) * time.Microsecond realityConfig.ShortIds = make(map[[8]byte]bool) for i, shortIDString := range c.ShortID { var shortID [8]byte decodedLen := hex.DecodedLen(len(shortIDString)) if decodedLen > 8 { return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString) } decodedLen, err = hex.Decode(shortID[:], []byte(shortIDString)) if err != nil { return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err) } if decodedLen > 8 { return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString) } realityConfig.ShortIds[shortID] = true } realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { return inner.HandleTcp(tunnel, address, c.Proxy) } realityConfig.LimitFallbackUpload = c.LimitFallbackUpload realityConfig.LimitFallbackDownload = c.LimitFallbackDownload return &Builder{realityConfig}, nil } type Builder struct { realityConfig *utls.RealityConfig } func (b Builder) NewListener(l net.Listener) net.Listener { return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) { c, err := utls.RealityServer(ctx, conn, b.realityConfig) if err != nil { return nil, err } // Due to low implementation quality, the reality server intercepted half-close and caused memory leaks. // We fixed it by calling Close() directly. return realityConnWrapper{c}, nil }, func(a any) { stack := debug.Stack() log.Errorln("reality server panic: %s\n%s", a, stack) }) } type realityConnWrapper struct { *utls.Conn } func (c realityConnWrapper) Upstream() any { return c.Conn } func (c realityConnWrapper) CloseWrite() error { return c.Close() } func (c realityConnWrapper) ReaderReplaceable() bool { return true } func (c realityConnWrapper) WriterReplaceable() bool { return true } ================================================ FILE: core/Clash.Meta/listener/redir/tcp.go ================================================ package redir import ( "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/keepalive" C "github.com/metacubex/mihomo/constant" ) type Listener struct { listener net.Listener addr string closed bool } // RawAddress implements C.Listener func (l *Listener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *Listener) Address() string { return l.listener.Addr().String() } // Close implements C.Listener func (l *Listener) Close() error { l.closed = true return l.listener.Close() } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-REDIR"), inbound.WithSpecialRules(""), } } l, err := net.Listen("tcp", addr) if err != nil { return nil, err } rl := &Listener{ listener: l, addr: addr, } go func() { for { c, err := l.Accept() if err != nil { if rl.closed { break } continue } go handleRedir(c, tunnel, additions...) } }() return rl, nil } func handleRedir(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { target, err := parserPacket(conn) if err != nil { conn.Close() return } keepalive.TCPKeepAlive(conn) tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.REDIR, additions...)) } ================================================ FILE: core/Clash.Meta/listener/redir/tcp_darwin.go ================================================ package redir import ( "net" "syscall" "unsafe" "github.com/metacubex/mihomo/transport/socks5" ) func parserPacket(c net.Conn) (socks5.Addr, error) { const ( PfInout = 0 PfIn = 1 PfOut = 2 IOCOut = 0x40000000 IOCIn = 0x80000000 IOCInOut = IOCIn | IOCOut IOCPARMMask = 0x1FFF LEN = 4*16 + 4*4 + 4*1 // #define _IOC(inout,group,num,len) (inout | ((len & IOCPARMMask) << 16) | ((group) << 8) | (num)) // #define _IOWR(g,n,t) _IOC(IOCInOut, (g), (n), sizeof(t)) // #define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook) DIOCNATLOOK = IOCInOut | ((LEN & IOCPARMMask) << 16) | ('D' << 8) | 23 ) fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY) if err != nil { return nil, err } defer syscall.Close(fd) nl := struct { // struct pfioc_natlook 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: PfOut, } saddr := c.RemoteAddr().(*net.TCPAddr) daddr := c.LocalAddr().(*net.TCPAddr) copy(nl.saddr[:], saddr.IP) copy(nl.daddr[:], daddr.IP) nl.sxport[0], nl.sxport[1] = byte(saddr.Port>>8), byte(saddr.Port) nl.dxport[0], nl.dxport[1] = byte(daddr.Port>>8), byte(daddr.Port) if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 { return nil, errno } addr := make([]byte, 1+net.IPv4len+2) addr[0] = socks5.AtypIPv4 copy(addr[1:1+net.IPv4len], nl.rdaddr[:4]) copy(addr[1+net.IPv4len:], nl.rdxport[:2]) return addr, nil } ================================================ FILE: core/Clash.Meta/listener/redir/tcp_freebsd.go ================================================ package redir import ( "encoding/binary" "errors" "net" "net/netip" "syscall" "unsafe" "github.com/metacubex/mihomo/transport/socks5" "golang.org/x/sys/unix" ) const ( SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h ) func parserPacket(conn net.Conn) (socks5.Addr, error) { c, ok := conn.(*net.TCPConn) if !ok { return nil, errors.New("only work with TCP connection") } rc, err := c.SyscallConn() if err != nil { return nil, err } var addr netip.AddrPort rc.Control(func(fd uintptr) { if ip4 := c.LocalAddr().(*net.TCPAddr).IP.To4(); ip4 != nil { addr, err = getorigdst(fd) } else { addr, err = getorigdst6(fd) } }) return socks5.AddrFromStdAddrPort(addr), err } // Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c func getorigdst(fd uintptr) (netip.AddrPort, error) { addr := unix.RawSockaddrInet4{} size := uint32(unsafe.Sizeof(addr)) _, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0) if err != 0 { return netip.AddrPort{}, err } port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) return netip.AddrPortFrom(netip.AddrFrom4(addr.Addr), port), nil } func getorigdst6(fd uintptr) (netip.AddrPort, error) { addr := unix.RawSockaddrInet6{} size := uint32(unsafe.Sizeof(addr)) _, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0) if err != 0 { return netip.AddrPort{}, err } port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) return netip.AddrPortFrom(netip.AddrFrom16(addr.Addr), port), nil } ================================================ FILE: core/Clash.Meta/listener/redir/tcp_linux.go ================================================ package redir import ( "encoding/binary" "errors" "net" "net/netip" "syscall" "unsafe" "github.com/metacubex/mihomo/transport/socks5" "golang.org/x/sys/unix" ) const ( SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h ) func parserPacket(conn net.Conn) (socks5.Addr, error) { c, ok := conn.(*net.TCPConn) if !ok { return nil, errors.New("only work with TCP connection") } rc, err := c.SyscallConn() if err != nil { return nil, err } var addr netip.AddrPort rc.Control(func(fd uintptr) { if ip4 := c.LocalAddr().(*net.TCPAddr).IP.To4(); ip4 != nil { addr, err = getorigdst(fd) } else { addr, err = getorigdst6(fd) } }) return socks5.AddrFromStdAddrPort(addr), err } // Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c func getorigdst(fd uintptr) (netip.AddrPort, error) { addr := unix.RawSockaddrInet4{} size := uint32(unsafe.Sizeof(addr)) if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0); err != nil { return netip.AddrPort{}, err } port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) return netip.AddrPortFrom(netip.AddrFrom4(addr.Addr), port), nil } func getorigdst6(fd uintptr) (netip.AddrPort, error) { addr := unix.RawSockaddrInet6{} size := uint32(unsafe.Sizeof(addr)) if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0); err != nil { return netip.AddrPort{}, err } port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) return netip.AddrPortFrom(netip.AddrFrom16(addr.Addr), port), nil } ================================================ FILE: core/Clash.Meta/listener/redir/tcp_linux_386.go ================================================ package redir import ( "syscall" "unsafe" ) const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183 func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error { var a [6]uintptr a[0], a[1], a[2], a[3], a[4], a[5] = a0, a1, a2, a3, a4, a5 if _, _, errno := syscall.Syscall6(syscall.SYS_SOCKETCALL, call, uintptr(unsafe.Pointer(&a)), 0, 0, 0, 0); errno != 0 { return errno } return nil } ================================================ FILE: core/Clash.Meta/listener/redir/tcp_linux_other.go ================================================ //go:build linux && !386 package redir import "syscall" const GETSOCKOPT = syscall.SYS_GETSOCKOPT func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error { if _, _, errno := syscall.Syscall6(call, a0, a1, a2, a3, a4, a5); errno != 0 { return errno } return nil } ================================================ FILE: core/Clash.Meta/listener/redir/tcp_other.go ================================================ //go:build !darwin && !linux && !freebsd package redir import ( "errors" "net" "github.com/metacubex/mihomo/transport/socks5" ) func parserPacket(conn net.Conn) (socks5.Addr, error) { return nil, errors.New("system not support yet") } ================================================ FILE: core/Clash.Meta/listener/shadowsocks/tcp.go ================================================ package shadowsocks import ( "net" "strings" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/socks5" ) type Listener struct { closed bool config LC.ShadowsocksServer listeners []net.Listener udpListeners []*UDPListener pickCipher core.Cipher handler *sing.ListenerHandler } var _listener *Listener func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { pickCipher, err := core.PickCipher(config.Cipher, nil, config.Password) if err != nil { return nil, err } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.SHADOWSOCKS, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } sl := &Listener{false, config, nil, nil, pickCipher, h} _listener = sl for _, addr := range strings.Split(config.Listen, ",") { addr := addr if config.Udp { //UDP ul, err := NewUDP(addr, pickCipher, tunnel, additions...) if err != nil { return nil, err } sl.udpListeners = append(sl.udpListeners, ul) } //TCP l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } sl.listeners = append(sl.listeners, l) go func() { for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } go sl.HandleConn(c, tunnel, additions...) } }() } return sl, nil } func (l *Listener) Close() error { var retErr error for _, lis := range l.listeners { err := lis.Close() if err != nil { retErr = err } } for _, lis := range l.udpListeners { err := lis.Close() if err != nil { retErr = err } } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.listeners { addrList = append(addrList, lis.Addr()) } for _, lis := range l.udpListeners { addrList = append(addrList, lis.LocalAddr()) } return } func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { conn = l.pickCipher.StreamConn(conn) conn = N.NewDeadlineConn(conn) // embed ss can't handle readDeadline correctly target, err := socks5.ReadAddr0(conn) if err != nil { _ = conn.Close() return } l.handler.HandleSocket(target, conn, additions...) //tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.SHADOWSOCKS, additions...)) } func HandleShadowSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { if _listener != nil && _listener.pickCipher != nil { go _listener.HandleConn(conn, tunnel, additions...) return true } return false } ================================================ FILE: core/Clash.Meta/listener/shadowsocks/udp.go ================================================ package shadowsocks import ( "net" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/sockopt" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/socks5" ) type UDPListener struct { packetConn net.PacketConn closed bool } func NewUDP(addr string, pickCipher core.Cipher, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { l, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } if err := sockopt.UDPReuseaddr(l); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } sl := &UDPListener{l, false} conn := pickCipher.PacketConn(N.NewEnhancePacketConn(l)) go func() { for { data, put, remoteAddr, err := conn.WaitReadFrom() if err != nil { if put != nil { put() } if sl.closed { break } continue } handleSocksUDP(conn, tunnel, data, put, remoteAddr, additions...) } }() return sl, nil } func (l *UDPListener) Close() error { l.closed = true return l.packetConn.Close() } func (l *UDPListener) LocalAddr() net.Addr { return l.packetConn.LocalAddr() } func handleSocksUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, put func(), addr net.Addr, additions ...inbound.Addition) { tgtAddr := socks5.SplitAddr(buf) if tgtAddr == nil { // Unresolved UDP packet, return buffer to the pool if put != nil { put() } return } target := tgtAddr payload := buf[len(tgtAddr):] packet := &packet{ pc: pc, rAddr: addr, payload: payload, put: put, } tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SHADOWSOCKS, additions...)) } ================================================ FILE: core/Clash.Meta/listener/shadowsocks/utils.go ================================================ package shadowsocks import ( "bytes" "errors" "net" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mhurl" ) type packet struct { pc net.PacketConn rAddr net.Addr payload []byte put func() } func (c *packet) Data() []byte { return c.payload } // WriteBack wirtes UDP packet with source(ip, port) = `addr` func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { if addr == nil { err = errors.New("address is invalid") return } packet := bytes.Join([][]byte{socks5.ParseAddrToSocksAddr(addr), b}, []byte{}) return c.pc.WriteTo(packet, c.rAddr) } // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { return c.rAddr } func (c *packet) Drop() { if c.put != nil { c.put() c.put = nil } c.payload = nil } func (c *packet) InAddr() net.Addr { return c.pc.LocalAddr() } func ParseSSURL(s string) (addr, cipher, password string, err error) { u, err := mhurl.Parse(s) // we need multiple hosts url supports if err != nil { return } addr = u.Host if u.User != nil { cipher = u.User.Username() password, _ = u.User.Password() } return } ================================================ FILE: core/Clash.Meta/listener/shadowsocks/utils_test.go ================================================ package shadowsocks import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func TestParseSSURL(t *testing.T) { for _, test := range []struct{ method, passwd, hosts string }{ {method: "aes-256-gcm", passwd: "password", hosts: ":1000,:2000,:3000"}, {method: "aes-256-gcm", passwd: "password", hosts: "127.0.0.1:1000,127.0.0.1:2000,127.0.0.1:3000"}, {method: "aes-256-gcm", passwd: "password", hosts: "[::1]:1000,[::1]:2000,[::1]:3000"}, } { addr, cipher, password, err := ParseSSURL(fmt.Sprintf("ss://%s:%s@%s", test.method, test.passwd, test.hosts)) require.NoError(t, err) require.Equal(t, test.hosts, addr) require.Equal(t, test.method, cipher) require.Equal(t, test.passwd, password) } } ================================================ FILE: core/Clash.Meta/listener/sing/context.go ================================================ package sing import ( "context" "golang.org/x/exp/slices" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/sing/common/auth" ) type contextKey string var ctxKeyAdditions = contextKey("Additions") func WithAdditions(ctx context.Context, additions ...inbound.Addition) context.Context { return context.WithValue(ctx, ctxKeyAdditions, additions) } func getAdditions(ctx context.Context) (additions []inbound.Addition) { if v := ctx.Value(ctxKeyAdditions); v != nil { if a, ok := v.([]inbound.Addition); ok { additions = a } } if user, ok := auth.UserFromContext[string](ctx); ok { additions = slices.Clone(additions) additions = append(additions, inbound.WithInUser(user)) } return } var ctxKeyInAddr = contextKey("InAddr") func WithInAddr(ctx context.Context, inAddr net.Addr) context.Context { return context.WithValue(ctx, ctxKeyInAddr, inAddr) } func getInAddr(ctx context.Context) net.Addr { if v := ctx.Value(ctxKeyInAddr); v != nil { if a, ok := v.(net.Addr); ok { return a } } return nil } ================================================ FILE: core/Clash.Meta/listener/sing/dialer.go ================================================ package sing import ( "context" "fmt" "net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/inner" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) type Dialer struct { t C.Tunnel proxy string } func (d Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if network != "tcp" && network != "tcp4" && network != "tcp6" { return nil, fmt.Errorf("unsupported network %s", network) } return inner.HandleTcp(d.t, destination.String(), d.proxy) } func (d Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, fmt.Errorf("unsupported ListenPacket") } var _ N.Dialer = (*Dialer)(nil) func NewDialer(t C.Tunnel, proxy string) (d *Dialer) { return &Dialer{t, proxy} } ================================================ FILE: core/Clash.Meta/listener/sing/sing.go ================================================ package sing import ( "context" "errors" "net" "net/netip" "sync" "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" mux "github.com/metacubex/sing-mux" vmess "github.com/metacubex/sing-vmess" "github.com/metacubex/sing-vmess/packetaddr" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" "github.com/metacubex/sing/common/bufio/deadline" E "github.com/metacubex/sing/common/exceptions" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/network" "github.com/metacubex/sing/common/uot" ) const UDPTimeout = 5 * time.Minute type ListenerConfig struct { Tunnel C.Tunnel Type C.Type Additions []inbound.Addition UDPTimeout time.Duration MuxOption MuxOption } type MuxOption struct { Padding bool `yaml:"padding" json:"padding,omitempty"` Brutal BrutalOptions `yaml:"brutal" json:"brutal,omitempty"` } type BrutalOptions struct { Enabled bool `yaml:"enabled" json:"enabled"` Up string `yaml:"up" json:"up,omitempty"` Down string `yaml:"down" json:"down,omitempty"` } type ListenerHandler struct { ListenerConfig muxService *mux.Service } func UpstreamMetadata(metadata M.Metadata) M.Metadata { return M.Metadata{ Source: metadata.Source, Destination: metadata.Destination, } } func ConvertMetadata(metadata *C.Metadata) M.Metadata { return M.Metadata{ Protocol: metadata.Type.String(), Source: M.SocksaddrFrom(metadata.SrcIP, metadata.SrcPort), Destination: M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort), } } func NewListenerHandler(lc ListenerConfig) (h *ListenerHandler, err error) { h = &ListenerHandler{ListenerConfig: lc} h.muxService, err = mux.NewService(mux.ServiceOptions{ NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { return ctx }, Logger: log.SingInfoToDebugLogger, // convert sing-mux info log to debug Handler: h, Padding: lc.MuxOption.Padding, Brutal: mux.BrutalOptions{ Enabled: lc.MuxOption.Brutal.Enabled, SendBPS: outbound.StringToBps(lc.MuxOption.Brutal.Up), ReceiveBPS: outbound.StringToBps(lc.MuxOption.Brutal.Down), }, }) return } func (h *ListenerHandler) IsSpecialFqdn(fqdn string) bool { switch fqdn { case mux.Destination.Fqdn, vmess.MuxDestination.Fqdn, uot.MagicAddress, uot.LegacyMagicAddress: return true default: return false } } func (h *ListenerHandler) ParseSpecialFqdn(ctx context.Context, conn net.Conn, metadata M.Metadata) error { switch metadata.Destination.Fqdn { case mux.Destination.Fqdn: return h.muxService.NewConnection(ctx, conn, UpstreamMetadata(metadata)) case vmess.MuxDestination.Fqdn: return vmess.HandleMuxConnection(ctx, conn, metadata, h) case uot.MagicAddress: request, err := uot.ReadRequest(conn) if err != nil { return E.Cause(err, "read UoT request") } metadata.Destination = request.Destination return h.NewPacketConnection(ctx, uot.NewConn(conn, *request), metadata) case uot.LegacyMagicAddress: metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} return h.NewPacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) } return errors.New("not special fqdn") } func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { if h.IsSpecialFqdn(metadata.Destination.Fqdn) { return h.ParseSpecialFqdn(ctx, conn, metadata) } if deadline.NeedAdditionalReadDeadline(conn) { conn = N.NewDeadlineConn(conn) // conn from sing should check NeedAdditionalReadDeadline } cMetadata := &C.Metadata{ NetWork: C.TCP, Type: h.Type, } if metadata.Source.IsIP() && metadata.Source.Fqdn == "" { cMetadata.RawSrcAddr = metadata.Source.Unwrap().TCPAddr() } if metadata.Destination.IsIP() && metadata.Destination.Fqdn == "" { cMetadata.RawDstAddr = metadata.Destination.Unwrap().TCPAddr() } inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(metadata.Destination), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr())) inbound.ApplyAdditions(cMetadata, h.Additions...) inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) h.Tunnel.HandleTCPConn(conn, cMetadata) // this goroutine must exit after conn unused return nil } func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), M.Socksaddr{}) } connID := utils.NewUUIDV4().String() // make a new SNAT key defer func() { _ = conn.Close() }() mutex := sync.Mutex{} writer := bufio.NewNetPacketWriter(conn) // a new interface to set nil in defer defer func() { mutex.Lock() // this goroutine must exit after all conn.WritePacket() is not running defer mutex.Unlock() writer = nil }() rwOptions := network.ReadWaitOptions{} readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) if isReadWaiter { readWaiter.InitializeReadWaiter(rwOptions) } for { var ( buff *buf.Buffer dest M.Socksaddr err error ) if isReadWaiter { buff, dest, err = readWaiter.WaitReadPacket() } else { buff = rwOptions.NewPacketBuffer() dest, err = conn.ReadPacket(buff) if buff != nil { rwOptions.PostReturn(buff) } } if err != nil { buff.Release() if ShouldIgnorePacketError(err) { break } return err } cPacket := &packet{ writer: &writer, mutex: &mutex, rAddr: metadata.Source.UDPAddr(), lAddr: conn.LocalAddr(), buff: buff, } cPacket.rAddr = N.NewCustomAddr(h.Type.String(), connID, cPacket.rAddr) // for tunnel's handleUDPConn if lAddr := getInAddr(ctx); lAddr != nil { cPacket.lAddr = lAddr } h.handlePacket(ctx, cPacket, metadata.Source, dest) } return nil } type localAddr interface { LocalAddr() net.Addr } func (h *ListenerHandler) NewPacket(ctx context.Context, key netip.AddrPort, buffer *buf.Buffer, metadata M.Metadata, init func(natConn network.PacketConn) network.PacketWriter) { writer := bufio.NewNetPacketWriter(init(nil)) mutex := sync.Mutex{} cPacket := &packet{ writer: &writer, mutex: &mutex, rAddr: metadata.Source.UDPAddr(), // TODO: using key argument to make a SNAT key buff: buffer, } if conn, ok := common.Cast[localAddr](writer); ok { // tun does not have real inAddr cPacket.lAddr = conn.LocalAddr() } h.handlePacket(ctx, cPacket, metadata.Source, metadata.Destination) } func (h *ListenerHandler) handlePacket(ctx context.Context, cPacket *packet, source M.Socksaddr, destination M.Socksaddr) { cMetadata := &C.Metadata{ NetWork: C.UDP, Type: h.Type, } if source.IsIP() && source.Fqdn == "" { cMetadata.RawSrcAddr = source.Unwrap().UDPAddr() } if destination.IsIP() && destination.Fqdn == "" { cMetadata.RawDstAddr = destination.Unwrap().UDPAddr() } inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(destination), inbound.WithSrcAddr(source), inbound.WithInAddr(cPacket.InAddr())) inbound.ApplyAdditions(cMetadata, h.Additions...) inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) h.Tunnel.HandleUDPPacket(cPacket, cMetadata) } func (h *ListenerHandler) NewError(ctx context.Context, err error) { log.Warnln("%s listener get error: %+v", h.Type.String(), err) } func (h *ListenerHandler) TypeMutation(typ C.Type) *ListenerHandler { handler := *h handler.Type = typ return &handler } func ShouldIgnorePacketError(err error) bool { // ignore simple error if E.IsTimeout(err) || E.IsClosed(err) || E.IsCanceled(err) { return true } return false } type packet struct { writer *network.NetPacketWriter mutex *sync.Mutex rAddr net.Addr lAddr net.Addr buff *buf.Buffer } func (c *packet) Data() []byte { return c.buff.Bytes() } // WriteBack wirtes UDP packet with source(ip, port) = `addr` func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { if addr == nil { err = errors.New("address is invalid") return } c.mutex.Lock() defer c.mutex.Unlock() conn := *c.writer if conn == nil { err = errors.New("writeBack to closed connection") return } return conn.WriteTo(b, addr) } // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { return c.rAddr } func (c *packet) Drop() { c.buff.Release() } func (c *packet) InAddr() net.Addr { return c.lAddr } ================================================ FILE: core/Clash.Meta/listener/sing/util.go ================================================ package sing import ( "context" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/transport/socks5" ) // HandleSocket like inbound.NewSocket combine with Tunnel.HandleTCPConn but also handel specialFqdn func (h *ListenerHandler) HandleSocket(target socks5.Addr, conn net.Conn, _additions ...inbound.Addition) { conn, metadata := inbound.NewSocket(target, conn, h.Type, h.Additions...) if h.IsSpecialFqdn(metadata.Host) { err := h.ParseSpecialFqdn( WithAdditions(context.Background(), _additions...), conn, ConvertMetadata(metadata), ) if err != nil { _ = conn.Close() } } else { inbound.ApplyAdditions(metadata, _additions...) h.Tunnel.HandleTCPConn(conn, metadata) } } ================================================ FILE: core/Clash.Meta/listener/sing_hysteria2/server.go ================================================ package sing_hysteria2 import ( "context" "errors" "fmt" "net" "net/url" "strings" "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/common/sockopt" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/inner" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/http" "github.com/metacubex/http/httputil" "github.com/metacubex/quic-go" "github.com/metacubex/sing-quic/hysteria2" E "github.com/metacubex/sing/common/exceptions" "github.com/metacubex/tls" ) type Listener struct { closed bool config LC.Hysteria2Server udpListeners []net.PacketConn services []*hysteria2.Service[string] } func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { var sl *Listener var err error if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-HYSTERIA2"), inbound.WithSpecialRules(""), } } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.HYSTERIA2, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } sl = &Listener{false, config, nil, nil} tlsConfig := &tls.Config{ Time: ntp.Now, MinVersion: tls.VersionTLS13, } certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } if len(config.ALPN) > 0 { tlsConfig.NextProtos = config.ALPN } else { tlsConfig.NextProtos = []string{"h3"} } var salamanderPassword string if len(config.Obfs) > 0 { if config.ObfsPassword == "" { return nil, errors.New("missing obfs password") } switch config.Obfs { case hysteria2.ObfsTypeSalamander: salamanderPassword = config.ObfsPassword default: return nil, fmt.Errorf("unknown obfs type: %s", config.Obfs) } } var masqueradeHandler http.Handler if config.Masquerade != "" { masqueradeURL, err := url.Parse(config.Masquerade) if err != nil { return nil, E.Cause(err, "parse masquerade URL") } switch masqueradeURL.Scheme { case "file": masqueradeHandler = http.FileServer(http.Dir(masqueradeURL.Path)) case "http", "https": masqueradeHandler = &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(masqueradeURL) r.Out.Host = r.In.Host }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { w.WriteHeader(http.StatusBadGateway) }, Transport: &http.Transport{ // fellow hysteria2's code skip verify TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, // from http.DefaultTransport ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { return inner.HandleTcp(tunnel, address, "") }, }, } default: return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) } } if config.UdpMTU == 0 { // "1200" from quic-go's MaxDatagramSize // "-3" from quic-go's DatagramFrame.MaxDataLen config.UdpMTU = 1200 - 3 } quicConfig := &quic.Config{ InitialStreamReceiveWindow: config.InitialStreamReceiveWindow, MaxStreamReceiveWindow: config.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: config.InitialConnectionReceiveWindow, MaxConnectionReceiveWindow: config.MaxConnectionReceiveWindow, } service, err := hysteria2.NewService[string](hysteria2.ServiceOptions{ Context: context.Background(), Logger: log.SingLogger, SendBPS: outbound.StringToBps(config.Up), ReceiveBPS: outbound.StringToBps(config.Down), SalamanderPassword: salamanderPassword, TLSConfig: tlsConfig, QUICConfig: quicConfig, IgnoreClientBandwidth: config.IgnoreClientBandwidth, UDPTimeout: sing.UDPTimeout, Handler: h, MasqueradeHandler: masqueradeHandler, UdpMTU: config.UdpMTU, SetBBRCongestion: func(quicConn *quic.Conn) { common.SetCongestionController(quicConn, "bbr", config.CWND, config.BBRProfile) }, }) if err != nil { return nil, err } userNameList := make([]string, 0, len(config.Users)) userPasswordList := make([]string, 0, len(config.Users)) for name, password := range config.Users { userNameList = append(userNameList, name) userPasswordList = append(userPasswordList, password) } service.UpdateUsers(userNameList, userPasswordList) for _, addr := range strings.Split(config.Listen, ",") { addr := addr _service := *service service := &_service // make a copy ul, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } if err := sockopt.UDPReuseaddr(ul); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } sl.udpListeners = append(sl.udpListeners, ul) sl.services = append(sl.services, service) go func() { _ = service.Start(ul) }() } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, service := range l.services { err := service.Close() if err != nil { retErr = err } } for _, lis := range l.udpListeners { err := lis.Close() if err != nil { retErr = err } } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.udpListeners { addrList = append(addrList, lis.LocalAddr()) } return } ================================================ FILE: core/Clash.Meta/listener/sing_shadowsocks/server.go ================================================ package sing_shadowsocks import ( "context" "fmt" "net" "strings" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" embedSS "github.com/metacubex/mihomo/listener/shadowsocks" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/kcptun" shadowsocks "github.com/metacubex/sing-shadowsocks" "github.com/metacubex/sing-shadowsocks/shadowaead" "github.com/metacubex/sing-shadowsocks/shadowaead_2022" shadowtls "github.com/metacubex/sing-shadowtls" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/network" ) type Listener struct { closed bool config LC.ShadowsocksServer listeners []net.Listener udpListeners []net.PacketConn service shadowsocks.Service shadowTLS *shadowtls.Service } var _listener *Listener // shadowTLSService is a wrapper for shadowsocks.Service to support shadowTLS. type shadowTLSService struct { shadowsocks.Service shadowTLS *shadowtls.Service } func (s *shadowTLSService) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { if s.shadowTLS != nil { return s.shadowTLS.NewConnection(ctx, conn, metadata) } return s.Service.NewConnection(ctx, conn, metadata) } func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (C.MultiAddrListener, error) { var sl *Listener var err error if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-SHADOWSOCKS"), inbound.WithSpecialRules(""), } defer func() { _listener = sl }() } udpTimeout := int64(sing.UDPTimeout.Seconds()) h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.SHADOWSOCKS, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } sl = &Listener{} sl.config = config switch { case config.Cipher == shadowsocks.MethodNone: sl.service = shadowsocks.NewNoneService(udpTimeout, h) case common.Contains(shadowaead.List, config.Cipher): sl.service, err = shadowaead.NewService(config.Cipher, nil, config.Password, udpTimeout, h) case common.Contains(shadowaead_2022.List, config.Cipher): sl.service, err = shadowaead_2022.NewServiceWithPassword(config.Cipher, config.Password, udpTimeout, h, ntp.Now) default: err = fmt.Errorf("shadowsocks: unsupported method: %s", config.Cipher) return embedSS.New(config, tunnel, additions...) } if err != nil { return nil, err } if config.ShadowTLS.Enable { buildHandshake := func(handshake LC.ShadowTLSHandshakeOptions) (handshakeConfig shadowtls.HandshakeConfig) { handshakeConfig.Server = M.ParseSocksaddr(handshake.Dest) handshakeConfig.Dialer = sing.NewDialer(tunnel, handshake.Proxy) return } var handshakeForServerName map[string]shadowtls.HandshakeConfig if config.ShadowTLS.Version > 1 { handshakeForServerName = make(map[string]shadowtls.HandshakeConfig) for serverName, serverOptions := range config.ShadowTLS.HandshakeForServerName { handshakeForServerName[serverName] = buildHandshake(serverOptions) } } var wildcardSNI shadowtls.WildcardSNI switch config.ShadowTLS.WildcardSNI { case "authed": wildcardSNI = shadowtls.WildcardSNIAuthed case "all": wildcardSNI = shadowtls.WildcardSNIAll default: wildcardSNI = shadowtls.WildcardSNIOff } var shadowTLS *shadowtls.Service shadowTLS, err = shadowtls.NewService(shadowtls.ServiceConfig{ Version: config.ShadowTLS.Version, Password: config.ShadowTLS.Password, Users: common.Map(config.ShadowTLS.Users, func(it LC.ShadowTLSUser) shadowtls.User { return shadowtls.User{Name: it.Name, Password: it.Password} }), Handshake: buildHandshake(config.ShadowTLS.Handshake), HandshakeForServerName: handshakeForServerName, StrictMode: config.ShadowTLS.StrictMode, WildcardSNI: wildcardSNI, Handler: sl.service, Logger: log.SingLogger, }) if err != nil { return nil, err } sl.service = &shadowTLSService{ Service: sl.service, shadowTLS: shadowTLS, } } var kcptunServer *kcptun.Server if config.KcpTun.Enable { kcptunServer = kcptun.NewServer(config.KcpTun.Config) config.Udp = true } for _, addr := range strings.Split(config.Listen, ",") { addr := addr if config.Udp { //UDP ul, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } if err := sockopt.UDPReuseaddr(ul); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } sl.udpListeners = append(sl.udpListeners, ul) if kcptunServer != nil { go kcptunServer.Serve(ul, func(c net.Conn) { sl.HandleConn(c, tunnel) }) continue // skip tcp listener } go func() { conn := bufio.NewPacketConn(ul) rwOptions := network.NewReadWaitOptions(conn, sl.service) readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) if isReadWaiter { readWaiter.InitializeReadWaiter(rwOptions) } for { var ( buff *buf.Buffer dest M.Socksaddr err error ) buff = nil // clear last loop status, avoid repeat release if isReadWaiter { buff, dest, err = readWaiter.WaitReadPacket() } else { buff = rwOptions.NewPacketBuffer() dest, err = conn.ReadPacket(buff) if buff != nil { rwOptions.PostReturn(buff) } } if err != nil { buff.Release() if sl.closed { break } continue } ctx := context.TODO() ctx = sing.WithInAddr(ctx, ul.LocalAddr()) _ = sl.service.NewPacket(ctx, conn, buff, M.Metadata{ Protocol: "shadowsocks", Source: dest, }) } }() } //TCP l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } sl.listeners = append(sl.listeners, l) go func() { for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } go sl.HandleConn(c, tunnel) } }() } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, lis := range l.listeners { err := lis.Close() if err != nil { retErr = err } } for _, lis := range l.udpListeners { err := lis.Close() if err != nil { retErr = err } } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.listeners { addrList = append(addrList, lis.Addr()) } for _, lis := range l.udpListeners { addrList = append(addrList, lis.LocalAddr()) } return } func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { ctx := sing.WithAdditions(context.TODO(), additions...) err := l.service.NewConnection(ctx, conn, M.Metadata{ Protocol: "shadowsocks", Source: M.SocksaddrFromNet(conn.RemoteAddr()), }) if err != nil { _ = conn.Close() return } } func HandleShadowSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { if _listener != nil && _listener.service != nil { go _listener.HandleConn(conn, tunnel, additions...) return true } return embedSS.HandleShadowSocks(conn, tunnel, additions...) } ================================================ FILE: core/Clash.Meta/listener/sing_tun/dns.go ================================================ package sing_tun import ( "context" "net" "net/netip" "sync" "time" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/network" ) func (h *ListenerHandler) ShouldHijackDns(targetAddr netip.AddrPort) bool { for _, addrPort := range h.DnsAddrPorts { if addrPort == targetAddr || (addrPort.Addr().IsUnspecified() && targetAddr.Port() == 53) { return true } } return false } func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { if h.ShouldHijackDns(metadata.Destination.AddrPort()) { log.Debugln("[DNS] hijack tcp:%s", metadata.Destination.String()) return resolver.RelayDnsConn(ctx, conn, resolver.DefaultDnsReadTimeout) } return h.ListenerHandler.NewConnection(ctx, conn, metadata) } func (h *ListenerHandler) NewPacket(ctx context.Context, key netip.AddrPort, buffer *buf.Buffer, metadata M.Metadata, init func(natConn network.PacketConn) network.PacketWriter) { if h.ShouldHijackDns(metadata.Destination.AddrPort()) { log.Debugln("[DNS] hijack udp:%s from %s", metadata.Destination.String(), metadata.Source.String()) writer := init(nil) rwOptions := network.ReadWaitOptions{ FrontHeadroom: network.CalculateFrontHeadroom(writer), RearHeadroom: network.CalculateRearHeadroom(writer), MTU: resolver.SafeDnsPacketSize, } go relayDnsPacket(ctx, buffer, rwOptions, metadata.Destination, nil, &writer) return } h.ListenerHandler.NewPacket(ctx, key, buffer, metadata, init) } func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { if h.ShouldHijackDns(metadata.Destination.AddrPort()) { log.Debugln("[DNS] hijack udp:%s from %s", metadata.Destination.String(), metadata.Source.String()) defer func() { _ = conn.Close() }() mutex := sync.Mutex{} var writer network.PacketWriter = conn // a new interface to set nil in defer defer func() { mutex.Lock() // this goroutine must exit after all conn.WritePacket() is not running defer mutex.Unlock() writer = nil }() rwOptions := network.ReadWaitOptions{ FrontHeadroom: network.CalculateFrontHeadroom(conn), RearHeadroom: network.CalculateRearHeadroom(conn), MTU: resolver.SafeDnsPacketSize, } readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) if isReadWaiter { readWaiter.InitializeReadWaiter(rwOptions) } for { var ( readBuff *buf.Buffer dest M.Socksaddr err error ) _ = conn.SetReadDeadline(time.Now().Add(resolver.DefaultDnsReadTimeout)) readBuff = nil // clear last loop status, avoid repeat release if isReadWaiter { readBuff, dest, err = readWaiter.WaitReadPacket() } else { readBuff = rwOptions.NewPacketBuffer() dest, err = conn.ReadPacket(readBuff) if readBuff != nil { rwOptions.PostReturn(readBuff) } } if err != nil { if readBuff != nil { readBuff.Release() } if sing.ShouldIgnorePacketError(err) { break } return err } go relayDnsPacket(ctx, readBuff, rwOptions, dest, &mutex, &writer) } return nil } return h.ListenerHandler.NewPacketConnection(ctx, conn, metadata) } func relayDnsPacket(ctx context.Context, readBuff *buf.Buffer, rwOptions network.ReadWaitOptions, dest M.Socksaddr, mutex *sync.Mutex, writer *network.PacketWriter) { ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDnsRelayTimeout) defer cancel() inData := readBuff.Bytes() writeBuff := readBuff writeBuff.Resize(writeBuff.Start(), 0) if len(writeBuff.FreeBytes()) < resolver.SafeDnsPacketSize { // only create a new buffer when space don't enough writeBuff = rwOptions.NewPacketBuffer() } msg, err := resolver.RelayDnsPacket(ctx, inData, writeBuff.FreeBytes()) if writeBuff != readBuff { readBuff.Release() } if err != nil { writeBuff.Release() return } writeBuff.Truncate(len(msg)) if mutex != nil { mutex.Lock() defer mutex.Unlock() } conn := *writer if conn == nil { writeBuff.Release() return } err = conn.WritePacket(writeBuff, dest) // WritePacket will release writeBuff if err != nil { writeBuff.Release() return } } func (h *ListenerHandler) TypeMutation(typ C.Type) *ListenerHandler { handle := *h handle.ListenerHandler = h.ListenerHandler.TypeMutation(typ) return &handle } ================================================ FILE: core/Clash.Meta/listener/sing_tun/iface.go ================================================ package sing_tun import ( "net" "net/netip" "github.com/metacubex/mihomo/component/iface" "github.com/metacubex/sing/common/control" ) type defaultInterfaceFinder struct{} var DefaultInterfaceFinder control.InterfaceFinder = (*defaultInterfaceFinder)(nil) func (f *defaultInterfaceFinder) Update() error { iface.FlushCache() _, err := iface.Interfaces() return err } func (f *defaultInterfaceFinder) Interfaces() []control.Interface { ifaces, err := iface.Interfaces() if err != nil { return nil } interfaces := make([]control.Interface, 0, len(ifaces)) for _, _interface := range ifaces { interfaces = append(interfaces, control.Interface(*_interface)) } return interfaces } func (f *defaultInterfaceFinder) ByName(name string) (*control.Interface, error) { netInterface, err := iface.ResolveInterface(name) if err == nil { return (*control.Interface)(netInterface), nil } if _, err := net.InterfaceByName(name); err == nil { err = f.Update() if err != nil { return nil, err } return f.ByName(name) } return nil, err } func (f *defaultInterfaceFinder) ByIndex(index int) (*control.Interface, error) { ifaces, err := iface.Interfaces() if err != nil { return nil, err } for _, netInterface := range ifaces { if netInterface.Index == index { return (*control.Interface)(netInterface), nil } } _, err = net.InterfaceByIndex(index) if err == nil { err = f.Update() if err != nil { return nil, err } return f.ByIndex(index) } return nil, iface.ErrIfaceNotFound } func (f *defaultInterfaceFinder) ByAddr(addr netip.Addr) (*control.Interface, error) { netInterface, err := iface.ResolveInterfaceByAddr(addr) if err != nil { return nil, err } return (*control.Interface)(netInterface), nil } ================================================ FILE: core/Clash.Meta/listener/sing_tun/prepare.go ================================================ package sing_tun import ( "context" "net/netip" "time" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/log" tun "github.com/metacubex/sing-tun" "github.com/metacubex/sing-tun/ping" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) func (h *ListenerHandler) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { switch network { case N.NetworkICMP: // our fork only send those type to PrepareConnection now if h.DisableICMPForwarding || h.skipPingForwardingByAddr(destination.Addr) { // skip if ICMP handling is disabled or other condition log.Infoln("[ICMP] %s %s --> %s using fake ping echo", network, source, destination) return nil, nil } log.Infoln("[ICMP] %s %s --> %s using DIRECT", network, source, destination) directRouteDestination, err := ping.ConnectDestination(context.TODO(), log.SingLogger, dialer.ICMPControl(destination.Addr), destination.Addr, routeContext, timeout) if err != nil { log.Warnln("[ICMP] failed to connect to %s", destination) return nil, err } log.Debugln("[ICMP] success connect to %s", destination) return directRouteDestination, nil } return nil, nil } func (h *ListenerHandler) skipPingForwardingByAddr(addr netip.Addr) bool { for _, prefix := range h.Inet4Address { // skip in interface ipv4 range if prefix.Contains(addr) { return true } } for _, prefix := range h.Inet6Address { // skip in interface ipv6 range if prefix.Contains(addr) { return true } } if resolver.IsFakeIP(addr) { // skip in fakeIp pool return true } return false } ================================================ FILE: core/Clash.Meta/listener/sing_tun/redirect_linux.go ================================================ package sing_tun const supportRedirect = true ================================================ FILE: core/Clash.Meta/listener/sing_tun/redirect_stub.go ================================================ //go:build !linux package sing_tun const supportRedirect = false ================================================ FILE: core/Clash.Meta/listener/sing_tun/server.go ================================================ package sing_tun import ( "context" "fmt" "io" "net" "net/netip" "os" "runtime" "strconv" "strings" "sync" "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/iface" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "golang.org/x/exp/constraints" tun "github.com/metacubex/sing-tun" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/control" E "github.com/metacubex/sing/common/exceptions" F "github.com/metacubex/sing/common/format" "github.com/metacubex/sing/common/ranges" "go4.org/netipx" "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) var InterfaceName = "Meta" var EnforceBindInterface = false var ( tunLogMu sync.Mutex tunLogLastTime time.Time tunLogCount int ) func shouldLogTun() bool { tunLogMu.Lock() defer tunLogMu.Unlock() now := time.Now() if now.Sub(tunLogLastTime) >= time.Second { tunLogLastTime = now tunLogCount = 0 } if tunLogCount >= 10 { return false } tunLogCount++ return true } type Listener struct { closed bool options LC.Tun handler *ListenerHandler tunName string addrStr string tunIf tun.Tun tunStack tun.Stack networkUpdateMonitor tun.NetworkUpdateMonitor defaultInterfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager autoRedirect tun.AutoRedirect autoRedirectOutputMark int32 cDialerInterfaceFinder dialer.InterfaceFinder ruleUpdateCallbackCloser io.Closer ruleUpdateMutex sync.Mutex routeAddressMap map[string]*netipx.IPSet routeExcludeAddressMap map[string]*netipx.IPSet routeAddressSet []*netipx.IPSet routeExcludeAddressSet []*netipx.IPSet dnsServerIp []string } type ListenerHandler struct { *sing.ListenerHandler DnsAddrPorts []netip.AddrPort Inet4Address []netip.Prefix Inet6Address []netip.Prefix DisableICMPForwarding bool } var emptyAddressSet = []*netipx.IPSet{{}} func CalculateInterfaceName(name string) (tunName string) { if runtime.GOOS == "darwin" { tunName = "utun" } else if name != "" { tunName = name return } else { tunName = "tun" } interfaces, err := net.Interfaces() if err != nil { return } tunIndex := 0 indexArr := make([]int, 0, len(interfaces)) for _, netInterface := range interfaces { if strings.HasPrefix(netInterface.Name, tunName) { index, parseErr := strconv.ParseInt(netInterface.Name[len(tunName):], 10, 16) if parseErr == nil { indexArr = append(indexArr, int(index)) } } } slices.Sort(indexArr) indexArr = slices.Compact(indexArr) for _, index := range indexArr { if index == tunIndex { tunIndex += 1 } else { // indexArr already sorted and distinct, so this tunIndex nobody used break } } tunName = F.ToString(tunName, tunIndex) return } func checkTunName(tunName string) (ok bool) { defer func() { if !ok { log.Warnln("[TUN] Unsupported tunName(%s) in %s, force regenerate by ourselves.", tunName, runtime.GOOS) } }() if runtime.GOOS == "darwin" { if len(tunName) <= 4 { return false } if tunName[:4] != "utun" { return false } if _, parseErr := strconv.ParseInt(tunName[4:], 10, 16); parseErr != nil { return false } } return true } func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Listener, err error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TUN"), inbound.WithSpecialRules(""), } } ctx := context.TODO() rpTunnel := tunnel.(P.Tunnel) if options.GSOMaxSize == 0 { options.GSOMaxSize = 65536 } if !supportRedirect { options.AutoRedirect = false } tunName := options.Device if options.FileDescriptor == 0 && (tunName == "" || !checkTunName(tunName)) { tunName = CalculateInterfaceName(InterfaceName) options.Device = tunName } forwarderBindInterface := false if options.FileDescriptor > 0 { if tunnelName, err := getTunnelName(int32(options.FileDescriptor)); err == nil { tunName = tunnelName // sing-tun must have the truth tun interface name even it from a fd //forwarderBindInterface = true log.Debugln("[TUN] use tun name %s for fd %d", tunnelName, options.FileDescriptor) } else { log.Warnln("[TUN] get tun name failed for fd %d, fallback to use tun interface name %s", options.FileDescriptor, tunName) } } routeAddress := options.RouteAddress if len(options.Inet4RouteAddress) > 0 { routeAddress = append(routeAddress, options.Inet4RouteAddress...) } if len(options.Inet6RouteAddress) > 0 { routeAddress = append(routeAddress, options.Inet6RouteAddress...) } 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 if len(options.Inet4RouteExcludeAddress) > 0 { routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...) } if len(options.Inet6RouteExcludeAddress) > 0 { routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...) } 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() }) tunMTU := options.MTU if tunMTU == 0 { tunMTU = 9000 } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Second * time.Duration(options.UDPTimeout) } else { udpTimeout = sing.UDPTimeout } tableIndex := options.IPRoute2TableIndex if tableIndex == 0 { tableIndex = tun.DefaultIPRoute2TableIndex } ruleIndex := options.IPRoute2RuleIndex if ruleIndex == 0 { ruleIndex = tun.DefaultIPRoute2RuleIndex } autoRedirectFallbackRuleIndex := options.AutoRedirectIPRoute2FallbackRuleIndex if autoRedirectFallbackRuleIndex == 0 { autoRedirectFallbackRuleIndex = tun.DefaultIPRoute2AutoRedirectFallbackRuleIndex } inputMark := options.AutoRedirectInputMark if inputMark == 0 { inputMark = tun.DefaultAutoRedirectInputMark } outputMark := options.AutoRedirectOutputMark if outputMark == 0 { outputMark = tun.DefaultAutoRedirectOutputMark } includeUID := uidToRange(options.IncludeUID) if len(options.IncludeUIDRange) > 0 { var err error 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 { var err error excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) if err != nil { return nil, E.Cause(err, "parse exclude_uid_range") } } excludeSrcPort := uidToRange(options.ExcludeSrcPort) if len(options.ExcludeSrcPortRange) > 0 { var err error excludeSrcPort, err = parseRange(excludeSrcPort, options.ExcludeSrcPortRange) if err != nil { return nil, E.Cause(err, "parse exclude_src_port_range") } } excludeDstPort := uidToRange(options.ExcludeDstPort) if len(options.ExcludeDstPortRange) > 0 { var err error excludeDstPort, err = parseRange(excludeDstPort, options.ExcludeDstPortRange) if err != nil { return nil, E.Cause(err, "parse exclude_dst_port_range") } } var includeMACAddress []net.HardwareAddr for _, mac := range options.IncludeMACAddress { addr, err := net.ParseMAC(mac) if err != nil { return nil, E.Cause(err, "parse include_mac_address") } includeMACAddress = append(includeMACAddress, addr) } var excludeMACAddress []net.HardwareAddr for _, mac := range options.ExcludeMACAddress { addr, err := net.ParseMAC(mac) if err != nil { return nil, E.Cause(err, "parse exclude_mac_address") } excludeMACAddress = append(excludeMACAddress, addr) } var dnsAdds []netip.AddrPort for _, d := range options.DNSHijack { if _, after, ok := strings.Cut(d, "://"); ok { d = after } d = strings.Replace(d, "any", "0.0.0.0", 1) addrPort, err := netip.ParseAddrPort(d) if err != nil { return nil, fmt.Errorf("parse dns-hijack url error: %w", err) } dnsAdds = append(dnsAdds, addrPort) } var dnsServerIp []string for _, a := range options.Inet4Address { addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) dnsServerIp = append(dnsServerIp, a.Addr().Next().String()) dnsAdds = append(dnsAdds, addrPort) } for _, a := range options.Inet6Address { addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) dnsServerIp = append(dnsServerIp, a.Addr().Next().String()) dnsAdds = append(dnsAdds, addrPort) } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.TUN, Additions: additions, }) if err != nil { return nil, err } handler := &ListenerHandler{ ListenerHandler: h, DnsAddrPorts: dnsAdds, Inet4Address: options.Inet4Address, Inet6Address: options.Inet6Address, DisableICMPForwarding: options.DisableICMPForwarding, } l = &Listener{ closed: false, options: options, handler: handler, tunName: tunName, } defer func() { if err != nil { l.Close() l = nil } }() interfaceFinder := DefaultInterfaceFinder var networkUpdateMonitor tun.NetworkUpdateMonitor var defaultInterfaceMonitor tun.DefaultInterfaceMonitor if options.AutoRoute || options.AutoDetectInterface { // don't start NetworkUpdateMonitor because netlink banned by google on Android14+ networkUpdateMonitor, err = tun.NewNetworkUpdateMonitor(log.SingLogger) if err != nil { err = E.Cause(err, "create NetworkUpdateMonitor") return } l.networkUpdateMonitor = networkUpdateMonitor err = networkUpdateMonitor.Start() if err != nil { err = E.Cause(err, "start NetworkUpdateMonitor") return } overrideAndroidVPN := true if disable, _ := strconv.ParseBool(os.Getenv("DISABLE_OVERRIDE_ANDROID_VPN")); disable { overrideAndroidVPN = false } defaultInterfaceMonitor, err = tun.NewDefaultInterfaceMonitor(networkUpdateMonitor, log.SingLogger, tun.DefaultInterfaceMonitorOptions{InterfaceFinder: interfaceFinder, OverrideAndroidVPN: overrideAndroidVPN}) if err != nil { err = E.Cause(err, "create DefaultInterfaceMonitor") return } l.defaultInterfaceMonitor = defaultInterfaceMonitor defaultInterfaceMonitor.RegisterCallback(func(defaultInterface *control.Interface, event int) { if defaultInterface != nil { log.Warnln("[TUN] default interface changed by monitor, => %s", defaultInterface.Name) } else { log.Errorln("[TUN] default interface lost by monitor") } iface.FlushCache() resolver.ResetConnection() // reset resolver's connection after default interface changed }) err = defaultInterfaceMonitor.Start() if err != nil { err = E.Cause(err, "start DefaultInterfaceMonitor") return } if options.AutoDetectInterface { l.cDialerInterfaceFinder = &cDialerInterfaceFinder{ tunName: tunName, defaultInterfaceMonitor: defaultInterfaceMonitor, } if !dialer.DefaultInterfaceFinder.CompareAndSwap(nil, l.cDialerInterfaceFinder) { err = E.New("not allowed two tun listener using auto-detect-interface") return } } } tunOptions := tun.Options{ Name: tunName, MTU: tunMTU, GSO: options.GSO, Inet4Address: options.Inet4Address, Inet6Address: options.Inet6Address, AutoRoute: options.AutoRoute, IPRoute2TableIndex: tableIndex, IPRoute2RuleIndex: ruleIndex, IPRoute2AutoRedirectFallbackRuleIndex: autoRedirectFallbackRuleIndex, AutoRedirectInputMark: inputMark, AutoRedirectOutputMark: outputMark, Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4), Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6), StrictRoute: options.StrictRoute, Inet4RouteAddress: inet4RouteAddress, Inet6RouteAddress: inet6RouteAddress, Inet4RouteExcludeAddress: inet4RouteExcludeAddress, Inet6RouteExcludeAddress: inet6RouteExcludeAddress, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, IncludeUID: includeUID, ExcludeUID: excludeUID, ExcludeSrcPort: excludeSrcPort, ExcludeDstPort: excludeDstPort, IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, IncludeMACAddress: includeMACAddress, ExcludeMACAddress: excludeMACAddress, FileDescriptor: options.FileDescriptor, InterfaceMonitor: defaultInterfaceMonitor, EXP_RecvMsgX: options.RecvMsgX, EXP_SendMsgX: options.SendMsgX, } if options.AutoRedirect { l.routeAddressMap = make(map[string]*netipx.IPSet) l.routeExcludeAddressMap = make(map[string]*netipx.IPSet) if !options.AutoRoute { return nil, E.New("`auto-route` is required by `auto-redirect`") } disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) l.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ TunOptions: &tunOptions, Context: ctx, Handler: handler.TypeMutation(C.REDIR), Logger: log.SingLogger, NetworkMonitor: l.networkUpdateMonitor, InterfaceFinder: interfaceFinder, TableName: "mihomo", DisableNFTables: dErr == nil && disableNFTables, RouteAddressSet: &l.routeAddressSet, RouteExcludeAddressSet: &l.routeExcludeAddressSet, }) if err != nil { err = E.Cause(err, "initialize auto redirect") return } var markMode bool for _, routeAddressSet := range options.RouteAddressSet { rp, loaded := rpTunnel.RuleProviders()[routeAddressSet] if !loaded { err = E.New("parse route-address-set: rule-set not found: ", routeAddressSet) return } l.updateRule(rp, false, false) markMode = true } for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { rp, loaded := rpTunnel.RuleProviders()[routeExcludeAddressSet] if !loaded { err = E.New("parse route-exclude_address-set: rule-set not found: ", routeExcludeAddressSet) return } l.updateRule(rp, true, false) markMode = true } if markMode { tunOptions.AutoRedirectMarkMode = true } } tunIf, err := tunNew(tunOptions) if err != nil { err = E.Cause(err, "configure tun interface") return } l.dnsServerIp = dnsServerIp // after tun.New sing-tun has set DNS to TUN interface resolver.AddSystemDnsBlacklist(dnsServerIp...) stackOptions := tun.StackOptions{ Context: ctx, Tun: tunIf, TunOptions: tunOptions, EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: udpTimeout, Handler: handler, Logger: log.SingLogger, ForwarderBindInterface: forwarderBindInterface, InterfaceFinder: interfaceFinder, EnforceBindInterface: EnforceBindInterface, } l.tunIf = tunIf tunStack, err := tun.NewStack(strings.ToLower(options.Stack.String()), stackOptions) if err != nil { return } err = tunStack.Start() if err != nil { return } l.tunStack = tunStack if l.autoRedirect != nil { if len(l.options.RouteAddressSet) > 0 && len(l.routeAddressSet) == 0 { l.routeAddressSet = emptyAddressSet // without this we can't call UpdateRouteAddressSet after Start } if len(l.options.RouteExcludeAddressSet) > 0 && len(l.routeExcludeAddressSet) == 0 { l.routeExcludeAddressSet = emptyAddressSet // without this we can't call UpdateRouteAddressSet after Start } err = l.autoRedirect.Start() if err != nil { err = E.Cause(err, "auto redirect") return } if tunOptions.AutoRedirectMarkMode { l.autoRedirectOutputMark = int32(outputMark) if !dialer.DefaultRoutingMark.CompareAndSwap(0, l.autoRedirectOutputMark) { err = E.New("not allowed setting global routing-mark when working with autoRedirectMarkMode") return } l.autoRedirect.UpdateRouteAddressSet() l.ruleUpdateCallbackCloser = rpTunnel.RuleUpdateCallback().Register(l.ruleUpdateCallback) } } if !l.options.AutoDetectInterface { resolver.ResetConnection() } if options.FileDescriptor != 0 { tunName = fmt.Sprintf("%s(fd=%d)", tunName, options.FileDescriptor) } l.addrStr = fmt.Sprintf("%s(%s,%s), mtu: %d, auto route: %v, auto redir: %v, ip stack: %s", tunName, tunOptions.Inet4Address, tunOptions.Inet6Address, tunMTU, options.AutoRoute, options.AutoRedirect, options.Stack) return } func (l *Listener) ruleUpdateCallback(ruleProvider P.RuleProvider) { name := ruleProvider.Name() if slices.Contains(l.options.RouteAddressSet, name) { l.updateRule(ruleProvider, false, true) return } if slices.Contains(l.options.RouteExcludeAddressSet, name) { l.updateRule(ruleProvider, true, true) return } } type toIpCidr interface { ToIpCidr() *netipx.IPSet } func (l *Listener) updateRule(ruleProvider P.RuleProvider, exclude bool, update bool) { l.ruleUpdateMutex.Lock() defer l.ruleUpdateMutex.Unlock() name := ruleProvider.Name() switch rp := ruleProvider.Strategy().(type) { case toIpCidr: if !exclude { ipCidr := rp.ToIpCidr() if ipCidr != nil { l.routeAddressMap[name] = ipCidr } else { delete(l.routeAddressMap, name) } l.routeAddressSet = maps.Values(l.routeAddressMap) } else { ipCidr := rp.ToIpCidr() if ipCidr != nil { l.routeExcludeAddressMap[name] = ipCidr } else { delete(l.routeExcludeAddressMap, name) } l.routeExcludeAddressSet = maps.Values(l.routeExcludeAddressMap) } default: return } if update && l.autoRedirect != nil { l.autoRedirect.UpdateRouteAddressSet() } } func (l *Listener) OnReload() { if l.autoRedirectOutputMark != 0 { dialer.DefaultRoutingMark.CompareAndSwap(0, l.autoRedirectOutputMark) } if l.cDialerInterfaceFinder != nil { dialer.DefaultInterfaceFinder.CompareAndSwap(nil, l.cDialerInterfaceFinder) } } type cDialerInterfaceFinder struct { tunName string defaultInterfaceMonitor tun.DefaultInterfaceMonitor } func (d *cDialerInterfaceFinder) DefaultInterfaceName(destination netip.Addr) string { if netInterface, _ := DefaultInterfaceFinder.ByAddr(destination); netInterface != nil { return netInterface.Name } if netInterface := d.defaultInterfaceMonitor.DefaultInterface(); netInterface != nil { return netInterface.Name } return "" } func (d *cDialerInterfaceFinder) FindInterfaceName(destination netip.Addr) string { for _, dest := range []netip.Addr{destination, netip.IPv4Unspecified(), netip.IPv6Unspecified()} { autoDetectInterfaceName := d.DefaultInterfaceName(dest) if autoDetectInterfaceName == d.tunName { if shouldLogTun() { log.Warnln("[TUN] Auto detect interface for %s get same name with tun", destination.String()) } } else if autoDetectInterfaceName == "" || autoDetectInterfaceName == "" { if shouldLogTun() { log.Warnln("[TUN] Auto detect interface for %s get empty name.", destination.String()) } } else { log.Debugln("[TUN] Auto detect interface for %s --> %s", destination, autoDetectInterfaceName) return autoDetectInterfaceName } } if shouldLogTun() { log.Warnln("[TUN] Auto detect interface for %s failed, return '' to avoid lookback", destination) } return "" } func uidToRange[T constraints.Integer](uidList []T) []ranges.Range[T] { return common.Map(uidList, func(uid T) ranges.Range[T] { return ranges.NewSingle(uid) }) } func parseRange[T constraints.Integer](uidRanges []ranges.Range[T], rangeList []string) ([]ranges.Range[T], 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(T(start), T(end))) } return uidRanges, nil } func (l *Listener) Close() error { l.closed = true resolver.RemoveSystemDnsBlacklist(l.dnsServerIp...) if l.autoRedirectOutputMark != 0 { dialer.DefaultRoutingMark.CompareAndSwap(l.autoRedirectOutputMark, 0) } if l.cDialerInterfaceFinder != nil { dialer.DefaultInterfaceFinder.CompareAndSwap(l.cDialerInterfaceFinder, nil) } return common.Close( l.ruleUpdateCallbackCloser, l.tunStack, l.tunIf, l.autoRedirect, l.defaultInterfaceMonitor, l.networkUpdateMonitor, l.packageManager, ) } func (l *Listener) Config() LC.Tun { return l.options } func (l *Listener) Address() string { return l.addrStr } ================================================ FILE: core/Clash.Meta/listener/sing_tun/server_notwindows.go ================================================ //go:build !windows package sing_tun import ( tun "github.com/metacubex/sing-tun" ) func tunNew(options tun.Options) (tun.Tun, error) { return tun.New(options) } ================================================ FILE: core/Clash.Meta/listener/sing_tun/server_windows.go ================================================ package sing_tun import ( "time" "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" tun "github.com/metacubex/sing-tun" ) func tunNew(options tun.Options) (tunIf tun.Tun, err error) { maxRetry := 3 for i := 0; i < maxRetry; i++ { timeBegin := time.Now() tunIf, err = tun.New(options) if err == nil { return } timeEnd := time.Now() if timeEnd.Sub(timeBegin) < 1*time.Second { // retrying for "Cannot create a file when that file already exists." return } log.Warnln("Start Tun interface timeout: %s [retrying %d/%d]", err, i+1, maxRetry) } return } func init() { tun.TunnelType = InterfaceName if features.WindowsMajorVersion < 10 { // to resolve "bind: The requested address is not valid in its context" EnforceBindInterface = true } } ================================================ FILE: core/Clash.Meta/listener/sing_tun/tun_name_darwin.go ================================================ package sing_tun 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: core/Clash.Meta/listener/sing_tun/tun_name_linux.go ================================================ package sing_tun import ( "fmt" "golang.org/x/sys/unix" "syscall" "unsafe" ) 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: core/Clash.Meta/listener/sing_tun/tun_name_other.go ================================================ //go:build !(darwin || linux) package sing_tun import "os" func getTunnelName(fd int32) (string, error) { return "", os.ErrInvalid } ================================================ FILE: core/Clash.Meta/listener/sing_vless/server.go ================================================ package sing_vless import ( "context" "errors" "net" "strings" "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/vless/encryption" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/xhttp" "github.com/metacubex/http" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/metadata" "github.com/metacubex/tls" "golang.org/x/exp/slices" ) type Listener struct { closed bool config LC.VlessServer listeners []net.Listener service *Service[string] decryption *encryption.ServerInstance } func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-VLESS"), inbound.WithSpecialRules(""), } } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.VLESS, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } service := NewService[string](h) service.UpdateUsers( common.Map(config.Users, func(it LC.VlessUser) string { return it.Username }), common.Map(config.Users, func(it LC.VlessUser) string { return it.UUID }), common.Map(config.Users, func(it LC.VlessUser) string { return it.Flow })) sl = &Listener{config: config, service: service} sl.decryption, err = encryption.NewServer(config.Decryption) if err != nil { return nil, err } if sl.decryption != nil { defer func() { // decryption must be closed to avoid the goroutine leak if err != nil { _ = sl.decryption.Close() sl.decryption = nil } }() } httpServer := http.Server{ IdleTimeout: 30 * time.Second, Protocols: new(http.Protocols), } tlsConfig := &tls.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { return nil, errors.New("client-auth is unavailable in reality") } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err } } if config.WsPath != "" { httpMux := http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) if err != nil { http.Error(w, err.Error(), 500) return } sl.HandleConn(conn, tunnel, additions...) }) httpServer.Handler = httpMux httpServer.Protocols.SetHTTP1(true) tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } if config.GrpcServiceName != "" { httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, ConnHandler: func(conn net.Conn) { sl.HandleConn(conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, }) httpServer.Protocols.SetHTTP2(true) // SetUnencryptedHTTP2 to ensure we can work in plain http2 and some tls conn is not *tls.Conn (like *reality.Conn) httpServer.Protocols.SetUnencryptedHTTP2(true) tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 } if config.XHTTPConfig.Mode != "" { switch config.XHTTPConfig.Mode { case "auto", "stream-up", "stream-one", "packet-up": default: return nil, errors.New("unsupported xhttp mode") } } if config.XHTTPConfig.Path != "" || config.XHTTPConfig.Host != "" || config.XHTTPConfig.Mode != "" { httpServer.Handler, err = xhttp.NewServerHandler(xhttp.ServerOption{ Config: xhttp.Config{ Host: config.XHTTPConfig.Host, Path: config.XHTTPConfig.Path, Mode: config.XHTTPConfig.Mode, XPaddingBytes: config.XHTTPConfig.XPaddingBytes, XPaddingObfsMode: config.XHTTPConfig.XPaddingObfsMode, XPaddingKey: config.XHTTPConfig.XPaddingKey, XPaddingHeader: config.XHTTPConfig.XPaddingHeader, XPaddingPlacement: config.XHTTPConfig.XPaddingPlacement, XPaddingMethod: config.XHTTPConfig.XPaddingMethod, UplinkHTTPMethod: config.XHTTPConfig.UplinkHTTPMethod, SessionPlacement: config.XHTTPConfig.SessionPlacement, SessionKey: config.XHTTPConfig.SessionKey, SeqPlacement: config.XHTTPConfig.SeqPlacement, SeqKey: config.XHTTPConfig.SeqKey, UplinkDataPlacement: config.XHTTPConfig.UplinkDataPlacement, UplinkDataKey: config.XHTTPConfig.UplinkDataKey, UplinkChunkSize: config.XHTTPConfig.UplinkChunkSize, NoSSEHeader: config.XHTTPConfig.NoSSEHeader, ScStreamUpServerSecs: config.XHTTPConfig.ScStreamUpServerSecs, ScMaxBufferedPosts: config.XHTTPConfig.ScMaxBufferedPosts, ScMaxEachPostBytes: config.XHTTPConfig.ScMaxEachPostBytes, }, ConnHandler: func(conn net.Conn) { sl.HandleConn(conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, }) if err != nil { return nil, err } httpServer.Protocols.SetHTTP1(true) httpServer.Protocols.SetHTTP2(true) // SetUnencryptedHTTP2 to ensure we can work in plain http2 and some tls conn is not *tls.Conn (like *reality.Conn) httpServer.Protocols.SetUnencryptedHTTP2(true) if !slices.Contains(tlsConfig.NextProtos, "http/1.1") { tlsConfig.NextProtos = append([]string{"http/1.1"}, tlsConfig.NextProtos...) } if !slices.Contains(tlsConfig.NextProtos, "h2") { tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) } } for _, addr := range strings.Split(config.Listen, ",") { addr := addr //TCP l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else if sl.decryption == nil { return nil, errors.New("disallow using Vless without any certificates/reality/decryption config") } sl.listeners = append(sl.listeners, l) go func() { if httpServer.Handler != nil { _ = httpServer.Serve(l) return } for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } go sl.HandleConn(c, tunnel) } }() } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, lis := range l.listeners { err := lis.Close() if err != nil { retErr = err } } if l.decryption != nil { _ = l.decryption.Close() } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.listeners { addrList = append(addrList, lis.Addr()) } return } func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { ctx := sing.WithAdditions(context.TODO(), additions...) if l.decryption != nil { var err error conn, err = l.decryption.Handshake(conn, nil) if err != nil { return } } err := l.service.NewConnection(ctx, conn, metadata.Metadata{ Protocol: "vless", Source: metadata.SocksaddrFromNet(conn.RemoteAddr()), }) if err != nil { _ = conn.Close() return } } ================================================ FILE: core/Clash.Meta/listener/sing_vless/service.go ================================================ package sing_vless // copy and modify from https://github.com/SagerNet/sing-vmess/tree/3c1cf255413250b09a57e4ecdf1def1fa505e3cc/vless import ( "context" "encoding/binary" "io" "net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/transport/vless" "github.com/metacubex/mihomo/transport/vless/vision" "github.com/gofrs/uuid/v5" "github.com/metacubex/sing-vmess" "github.com/metacubex/sing/common/auth" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" E "github.com/metacubex/sing/common/exceptions" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" "google.golang.org/protobuf/proto" ) type Service[T comparable] struct { userMap map[[16]byte]T userFlow map[T]string handler Handler } type Handler interface { N.TCPConnectionHandler N.UDPConnectionHandler E.Handler } func NewService[T comparable](handler Handler) *Service[T] { return &Service[T]{ handler: handler, } } func (s *Service[T]) UpdateUsers(userList []T, userUUIDList []string, userFlowList []string) { userMap := make(map[[16]byte]T) userFlowMap := make(map[T]string) for i, userName := range userList { userID := utils.UUIDMap(userUUIDList[i]) userMap[userID] = userName userFlowMap[userName] = userFlowList[i] } s.userMap = userMap s.userFlow = userFlowMap } var _ N.TCPConnectionHandler = (*Service[int])(nil) func (s *Service[T]) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { var version uint8 err := binary.Read(conn, binary.BigEndian, &version) if err != nil { return err } if version != vless.Version { return E.New("unknown version: ", version) } var requestUUID [16]byte _, err = io.ReadFull(conn, requestUUID[:]) if err != nil { return err } var addonsLen uint8 err = binary.Read(conn, binary.BigEndian, &addonsLen) if err != nil { return err } var addons vless.Addons if addonsLen > 0 { addonsBytes := make([]byte, addonsLen) _, err = io.ReadFull(conn, addonsBytes) if err != nil { return err } err = proto.Unmarshal(addonsBytes, &addons) if err != nil { return err } } var command byte err = binary.Read(conn, binary.BigEndian, &command) if err != nil { return err } var destination M.Socksaddr if command != vless.CommandMux { destination, err = vmess.AddressSerializer.ReadAddrPort(conn) if err != nil { return err } } user, loaded := s.userMap[requestUUID] if !loaded { return E.New("unknown UUID: ", uuid.FromBytesOrNil(requestUUID[:])) } ctx = auth.ContextWithUser(ctx, user) metadata.Destination = destination userFlow := s.userFlow[user] requestFlow := addons.Flow if requestFlow != userFlow && requestFlow != "" { return E.New("flow mismatch: expected ", flowName(userFlow), ", but got ", flowName(requestFlow)) } responseConn := &serverConn{ExtendedConn: bufio.NewExtendedConn(conn)} switch requestFlow { case vless.XRV: conn, err = vision.NewConn(responseConn, conn, requestUUID) if err != nil { return E.Cause(err, "initialize vision") } case "": conn = responseConn default: return E.New("unknown flow: ", requestFlow) } switch command { case vless.CommandTCP: return s.handler.NewConnection(ctx, conn, metadata) case vless.CommandUDP: if requestFlow == vless.XRV { return E.New(vless.XRV, " flow does not support UDP") } return s.handler.NewPacketConnection(ctx, &serverPacketConn{ExtendedConn: bufio.NewExtendedConn(conn), destination: destination}, metadata) case vless.CommandMux: return vmess.HandleMuxConnection(ctx, conn, metadata, s.handler) default: return E.New("unknown command: ", command) } } func flowName(value string) string { if value == "" { return "none" } return value } type serverConn struct { N.ExtendedConn responseWritten bool } func (c *serverConn) Write(b []byte) (n int, err error) { if !c.responseWritten { buffer := buf.NewSize(2 + len(b)) buffer.WriteByte(vless.Version) buffer.WriteByte(0) buffer.Write(b) _, err = c.ExtendedConn.Write(buffer.Bytes()) buffer.Release() if err == nil { n = len(b) } c.responseWritten = true return } return c.ExtendedConn.Write(b) } func (c *serverConn) WriteBuffer(buffer *buf.Buffer) error { if !c.responseWritten { header := buffer.ExtendHeader(2) header[0] = vless.Version header[1] = 0 c.responseWritten = true } return c.ExtendedConn.WriteBuffer(buffer) } func (c *serverConn) FrontHeadroom() int { if c.responseWritten { return 0 } return 2 } func (c *serverConn) ReaderReplaceable() bool { return true } func (c *serverConn) WriterReplaceable() bool { return c.responseWritten } func (c *serverConn) NeedAdditionalReadDeadline() bool { return true } func (c *serverConn) Upstream() any { return c.ExtendedConn } type serverPacketConn struct { N.ExtendedConn destination M.Socksaddr readWaitOptions N.ReadWaitOptions } func (c *serverPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { c.readWaitOptions = options return false } func (c *serverPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { var packetLen uint16 err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen) if err != nil { return } buffer = c.readWaitOptions.NewPacketBuffer() _, err = buffer.ReadFullFrom(c.ExtendedConn, int(packetLen)) if err != nil { buffer.Release() return } c.readWaitOptions.PostReturn(buffer) destination = c.destination return } func (c *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { var packetLen uint16 err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen) if err != nil { return } if len(p) < int(packetLen) { err = io.ErrShortBuffer return } n, err = io.ReadFull(c.ExtendedConn, p[:packetLen]) if err != nil { return } if c.destination.IsFqdn() { addr = c.destination } else { addr = c.destination.UDPAddr() } return } func (c *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(p))) if err != nil { return } return c.ExtendedConn.Write(p) } func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { var packetLen uint16 err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen) if err != nil { return } _, err = buffer.ReadFullFrom(c.ExtendedConn, int(packetLen)) if err != nil { return } destination = c.destination return } func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { packetLen := buffer.Len() binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(packetLen)) return c.ExtendedConn.WriteBuffer(buffer) } func (c *serverPacketConn) FrontHeadroom() int { return 2 } func (c *serverPacketConn) NeedAdditionalReadDeadline() bool { return true } func (c *serverPacketConn) Upstream() any { return c.ExtendedConn } ================================================ FILE: core/Clash.Meta/listener/sing_vmess/server.go ================================================ package sing_vmess import ( "context" "errors" "net" "strings" "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/gun" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" "github.com/metacubex/mhurl" vmess "github.com/metacubex/sing-vmess" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/metadata" "github.com/metacubex/tls" ) type Listener struct { closed bool config LC.VmessServer listeners []net.Listener service *vmess.Service[string] } var _listener *Listener func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-VMESS"), inbound.WithSpecialRules(""), } defer func() { _listener = sl }() } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.VMESS, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } service := vmess.NewService[string](h, vmess.ServiceWithDisableHeaderProtection(), vmess.ServiceWithTimeFunc(ntp.Now)) err = service.UpdateUsers( common.Map(config.Users, func(it LC.VmessUser) string { return it.Username }), common.Map(config.Users, func(it LC.VmessUser) string { return it.UUID }), common.Map(config.Users, func(it LC.VmessUser) int { return it.AlterID })) if err != nil { return nil, err } err = service.Start() if err != nil { return nil, err } sl = &Listener{false, config, nil, service} httpServer := http.Server{ IdleTimeout: 30 * time.Second, Protocols: new(http.Protocols), } tlsConfig := &tls.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { return nil, errors.New("client-auth is unavailable in reality") } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err } } if config.WsPath != "" { httpMux := http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) if err != nil { http.Error(w, err.Error(), 500) return } sl.HandleConn(conn, tunnel, additions...) }) httpServer.Handler = httpMux httpServer.Protocols.SetHTTP1(true) tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } if config.GrpcServiceName != "" { httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, ConnHandler: func(conn net.Conn) { sl.HandleConn(conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, }) httpServer.Protocols.SetHTTP2(true) // SetUnencryptedHTTP2 to ensure we can work in plain http2 and some tls conn is not *tls.Conn (like *reality.Conn) httpServer.Protocols.SetUnencryptedHTTP2(true) tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 } for _, addr := range strings.Split(config.Listen, ",") { addr := addr //TCP l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } sl.listeners = append(sl.listeners, l) go func() { if httpServer.Handler != nil { _ = httpServer.Serve(l) return } for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } go sl.HandleConn(c, tunnel) } }() } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, lis := range l.listeners { err := lis.Close() if err != nil { retErr = err } } err := l.service.Close() if err != nil { retErr = err } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.listeners { addrList = append(addrList, lis.Addr()) } return } func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { ctx := sing.WithAdditions(context.TODO(), additions...) err := l.service.NewConnection(ctx, conn, metadata.Metadata{ Protocol: "vmess", Source: metadata.SocksaddrFromNet(conn.RemoteAddr()), }) if err != nil { _ = conn.Close() return } } func HandleVmess(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { if _listener != nil && _listener.service != nil { go _listener.HandleConn(conn, tunnel, additions...) return true } return false } func ParseVmessURL(s string) (addr, username, password string, err error) { u, err := mhurl.Parse(s) // we need multiple hosts url supports if err != nil { return } addr = u.Host if u.User != nil { username = u.User.Username() password, _ = u.User.Password() } return } ================================================ FILE: core/Clash.Meta/listener/sing_vmess/server_test.go ================================================ package sing_vmess import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func TestParseVmessURL(t *testing.T) { for _, test := range []struct{ username, passwd, hosts string }{ {username: "username", passwd: "password", hosts: ":1000,:2000,:3000"}, {username: "username", passwd: "password", hosts: "127.0.0.1:1000,127.0.0.1:2000,127.0.0.1:3000"}, {username: "username", passwd: "password", hosts: "[::1]:1000,[::1]:2000,[::1]:3000"}, } { addr, username, password, err := ParseVmessURL(fmt.Sprintf("vmess://%s:%s@%s", test.username, test.passwd, test.hosts)) require.NoError(t, err) require.Equal(t, test.hosts, addr) require.Equal(t, test.username, username) require.Equal(t, test.passwd, password) } } ================================================ FILE: core/Clash.Meta/listener/socks/tcp.go ================================================ package socks import ( "errors" "io" "net" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/socks4" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/tls" ) type Listener struct { listener net.Listener addr string closed bool } // RawAddress implements C.Listener func (l *Listener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *Listener) Address() string { return l.listener.Addr().String() } // Close implements C.Listener func (l *Listener) Close() error { l.closed = true return l.listener.Close() } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: authStore.Default}, tunnel, additions...) } func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { isDefault := false if len(additions) == 0 { isDefault = true additions = []inbound.Addition{ inbound.WithInName("DEFAULT-SOCKS"), inbound.WithSpecialRules(""), } } l, err := inbound.Listen("tcp", config.Listen) if err != nil { return nil, err } tlsConfig := &tls.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { return nil, errors.New("client-auth is unavailable in reality") } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err } } if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } sl := &Listener{ listener: l, addr: config.Listen, } go func() { for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } store := config.AuthStore if isDefault || store == authStore.Default { // only apply on default listener if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { _ = c.Close() continue } if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) { store = authStore.Nil } } go handleSocks(c, tunnel, store, additions...) } }() return sl, nil } func handleSocks(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { bufConn := N.NewBufferedConn(conn) head, err := bufConn.Peek(1) if err != nil { conn.Close() return } switch head[0] { case socks4.Version: HandleSocks4(bufConn, tunnel, store, additions...) case socks5.Version: HandleSocks5(bufConn, tunnel, store, additions...) default: conn.Close() } } func HandleSocks4(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { authenticator := store.Authenticator() addr, _, user, err := socks4.ServerHandshake(conn, authenticator) if err != nil { conn.Close() return } additions = append(additions, inbound.WithInUser(user)) tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...)) } func HandleSocks5(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { authenticator := store.Authenticator() target, command, user, err := socks5.ServerHandshake(conn, authenticator) if err != nil { conn.Close() return } if command == socks5.CmdUDPAssociate { defer conn.Close() io.Copy(io.Discard, conn) return } additions = append(additions, inbound.WithInUser(user)) tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.SOCKS5, additions...)) } ================================================ FILE: core/Clash.Meta/listener/socks/udp.go ================================================ package socks import ( "net" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/sockopt" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/socks5" ) type UDPListener struct { packetConn net.PacketConn addr string closed bool } // RawAddress implements C.Listener func (l *UDPListener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *UDPListener) Address() string { return l.packetConn.LocalAddr().String() } // Close implements C.Listener func (l *UDPListener) Close() error { l.closed = true return l.packetConn.Close() } func NewUDP(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-SOCKS"), inbound.WithSpecialRules(""), } } l, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } if err := sockopt.UDPReuseaddr(l); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } sl := &UDPListener{ packetConn: l, addr: addr, } conn := N.NewEnhancePacketConn(l) go func() { for { data, put, remoteAddr, err := conn.WaitReadFrom() if err != nil { if put != nil { put() } if sl.closed { break } continue } handleSocksUDP(l, tunnel, data, put, remoteAddr, additions...) } }() return sl, nil } func handleSocksUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, put func(), addr net.Addr, additions ...inbound.Addition) { target, payload, err := socks5.DecodeUDPPacket(buf) if err != nil { // Unresolved UDP packet, return buffer to the pool if put != nil { put() } return } packet := &packet{ pc: pc, rAddr: addr, payload: payload, put: put, } tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SOCKS5, additions...)) } ================================================ FILE: core/Clash.Meta/listener/socks/utils.go ================================================ package socks import ( "net" "github.com/metacubex/mihomo/transport/socks5" ) type packet struct { pc net.PacketConn rAddr net.Addr payload []byte put func() } func (c *packet) Data() []byte { return c.payload } // WriteBack write UDP packet with source(ip, port) = `addr` func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) if err != nil { return } return c.pc.WriteTo(packet, c.rAddr) } // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { return c.rAddr } func (c *packet) Drop() { if c.put != nil { c.put() c.put = nil } c.payload = nil } func (c *packet) InAddr() net.Addr { return c.pc.LocalAddr() } ================================================ FILE: core/Clash.Meta/listener/sudoku/server.go ================================================ package sudoku import ( "bytes" "errors" "io" "net" "strings" "time" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/inner" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/sudoku" ) type Listener struct { listener net.Listener addr string closed bool protoConf sudoku.ProtocolConfig tunnelSrv *sudoku.HTTPMaskTunnelServer fallback string handler *sing.ListenerHandler } // RawAddress implements C.Listener func (l *Listener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *Listener) Address() string { if l.listener == nil { return "" } return l.listener.Addr().String() } // Close implements C.Listener func (l *Listener) Close() error { l.closed = true if l.listener != nil { return l.listener.Close() } return nil } func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { log.Debugln("[Sudoku] accepted %s", conn.RemoteAddr()) handshakeConn := conn handshakeCfg := &l.protoConf closeConns := func() { _ = handshakeConn.Close() if handshakeConn != conn { _ = conn.Close() } } if l.tunnelSrv != nil { c, cfg, done, err := l.tunnelSrv.WrapConn(conn) if err != nil { closeConns() return } if done { return } if c != nil { handshakeConn = c } if cfg != nil { handshakeCfg = cfg } } if l.fallback != "" { if r, ok := handshakeConn.(interface{ IsHTTPMaskRejected() bool }); ok && r.IsHTTPMaskRejected() { fb, err := inner.HandleTcp(tunnel, l.fallback, "") if err != nil { closeConns() return } N.Relay(handshakeConn, fb) return } } cConn, meta, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg) if err != nil { fallbackAddr := l.fallback var susp *sudoku.SuspiciousError isSuspicious := errors.As(err, &susp) && susp != nil && susp.Conn != nil if isSuspicious { log.Warnln("[Sudoku] suspicious handshake from %s: %v", conn.RemoteAddr(), err) if fallbackAddr != "" { fb, err := inner.HandleTcp(tunnel, fallbackAddr, "") if err == nil { relayToFallback(susp.Conn, conn, fb) return } } } else { log.Debugln("[Sudoku] handshake failed from %s: %v", conn.RemoteAddr(), err) } closeConns() return } session, err := sudoku.ReadServerSession(cConn, meta) if err != nil { log.Warnln("[Sudoku] read session failed from %s: %v", conn.RemoteAddr(), err) _ = cConn.Close() if handshakeConn != conn { _ = conn.Close() } return } switch session.Type { case sudoku.SessionTypeUoT: l.handleUoTSession(session.Conn, tunnel, additions...) case sudoku.SessionTypeMultiplex: mux, err := sudoku.AcceptMultiplexServer(session.Conn) if err != nil { _ = session.Conn.Close() return } defer mux.Close() for { stream, target, err := mux.AcceptTCP() if err != nil { return } targetAddr := socks5.ParseAddr(target) if targetAddr == nil { _ = stream.Close() continue } go l.handler.HandleSocket(targetAddr, stream, additions...) } default: targetAddr := socks5.ParseAddr(session.Target) if targetAddr == nil { log.Warnln("[Sudoku] invalid target from %s: %q", conn.RemoteAddr(), session.Target) _ = session.Conn.Close() return } l.handler.HandleSocket(targetAddr, session.Conn, additions...) //tunnel.HandleTCPConn(inbound.NewSocket(targetAddr, session.Conn, C.SUDOKU, additions...)) } } func (l *Listener) handleUoTSession(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { writer := sudoku.NewUoTPacketConn(conn) remoteAddr := conn.RemoteAddr() connID := utils.NewUUIDV4().String() // make a new SNAT key for { addrStr, payload, err := sudoku.ReadDatagram(conn) if err != nil { if !errors.Is(err, io.EOF) { log.Debugln("[Sudoku][UoT] session closed: %v", err) } _ = conn.Close() return } target := socks5.ParseAddr(addrStr) if target == nil { log.Debugln("[Sudoku][UoT] drop invalid target: %s", addrStr) continue } cPacket := &uotPacket{ payload: payload, writer: writer, rAddr: remoteAddr, } cPacket.rAddr = N.NewCustomAddr(C.SUDOKU.String(), connID, cPacket.rAddr) // for tunnel's handleUDPConn tunnel.HandleUDPPacket(inbound.NewPacket(target, cPacket, C.SUDOKU, additions...)) } } type uotPacket struct { payload []byte writer *sudoku.UoTPacketConn rAddr net.Addr } func (p *uotPacket) Data() []byte { return p.payload } func (p *uotPacket) WriteBack(b []byte, addr net.Addr) (int, error) { return p.writer.WriteTo(b, addr) } func (p *uotPacket) Drop() { p.payload = nil } func (p *uotPacket) LocalAddr() net.Addr { return p.rAddr } func relayToFallback(wrapper net.Conn, rawConn net.Conn, fallback net.Conn) { if wrapper != nil { if recorder, ok := wrapper.(interface{ GetBufferedAndRecorded() []byte }); ok { badData := recorder.GetBufferedAndRecorded() if len(badData) > 0 { _ = fallback.SetWriteDeadline(time.Now().Add(3 * time.Second)) if _, err := io.Copy(fallback, bytes.NewReader(badData)); err != nil { _ = fallback.Close() _ = rawConn.Close() return } _ = fallback.SetWriteDeadline(time.Time{}) } } } N.Relay(rawConn, fallback) } func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-SUDOKU"), inbound.WithSpecialRules(""), } } // Using sing handler for sing-mux support h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.SUDOKU, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } l, err := inbound.Listen("tcp", config.Listen) if err != nil { return nil, err } tableType, err := sudoku.NormalizeTableType(config.TableType) if err != nil { _ = l.Close() return nil, err } defaultConf := sudoku.DefaultConfig() paddingMin, paddingMax := sudoku.ResolvePadding(config.PaddingMin, config.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax) enablePureDownlink := sudoku.DerefBool(config.EnablePureDownlink, defaultConf.EnablePureDownlink) tables, err := sudoku.NewServerTablesWithCustomPatterns(sudoku.ServerAEADSeed(config.Key), tableType, config.CustomTable, config.CustomTables) if err != nil { _ = l.Close() return nil, err } handshakeTimeout := sudoku.DerefInt(config.HandshakeTimeoutSecond, defaultConf.HandshakeTimeoutSeconds) protoConf := sudoku.ProtocolConfig{ Key: config.Key, AEADMethod: defaultConf.AEADMethod, PaddingMin: paddingMin, PaddingMax: paddingMax, EnablePureDownlink: enablePureDownlink, HandshakeTimeoutSeconds: handshakeTimeout, DisableHTTPMask: config.DisableHTTPMask, HTTPMaskMode: config.HTTPMaskMode, HTTPMaskPathRoot: strings.TrimSpace(config.PathRoot), } if len(tables) == 1 { protoConf.Table = tables[0] } else { protoConf.Tables = tables } if config.AEADMethod != "" { protoConf.AEADMethod = config.AEADMethod } sl := &Listener{ listener: l, addr: config.Listen, protoConf: protoConf, handler: h, fallback: strings.TrimSpace(config.Fallback), } if sl.fallback != "" { sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&sl.protoConf) } else { sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf) } go func() { for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } go sl.handleConn(c, tunnel, additions...) } }() return sl, nil } ================================================ FILE: core/Clash.Meta/listener/tproxy/packet.go ================================================ package tproxy import ( "errors" "fmt" "net" "net/netip" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/pool" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) type packet struct { pc net.PacketConn lAddr netip.AddrPort buf []byte tunnel C.Tunnel } func (c *packet) Data() []byte { return c.buf } // WriteBack opens a new socket binding `addr` to write UDP packet back func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { tc, err := createOrGetLocalConn(addr, c.LocalAddr(), c.tunnel) if err != nil { n = 0 return } n, err = tc.Write(b) return } // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { return &net.UDPAddr{IP: c.lAddr.Addr().AsSlice(), Port: int(c.lAddr.Port()), Zone: c.lAddr.Addr().Zone()} } func (c *packet) Drop() { _ = pool.Put(c.buf) c.buf = nil } func (c *packet) InAddr() net.Addr { return c.pc.LocalAddr() } // this function listen at rAddr and write to lAddr // for here, rAddr is the ip/port client want to access // lAddr is the ip/port client opened func createOrGetLocalConn(rAddr, lAddr net.Addr, tunnel C.Tunnel) (*net.UDPConn, error) { remote := rAddr.String() local := lAddr.String() natTable := tunnel.NatTable() localConn := natTable.GetForLocalConn(local, remote) // localConn not exist if localConn == nil { cond, loaded := natTable.GetOrCreateLockForLocalConn(local, remote) if loaded { cond.L.Lock() cond.Wait() // we should get localConn here localConn = natTable.GetForLocalConn(local, remote) if localConn == nil { return nil, fmt.Errorf("localConn is nil, nat entry not exist") } cond.L.Unlock() } else { if cond == nil { return nil, fmt.Errorf("cond is nil, nat entry not exist") } defer func() { natTable.DeleteLockForLocalConn(local, remote) cond.Broadcast() }() conn, err := listenLocalConn(rAddr, lAddr, tunnel) if err != nil { log.Errorln("listenLocalConn failed with error: %s, packet loss (rAddr[%T]=%s lAddr[%T]=%s)", err.Error(), rAddr, remote, lAddr, local) return nil, err } natTable.AddForLocalConn(local, remote, conn) localConn = conn } } return localConn, nil } // this function listen at rAddr // and send what received to program itself, then send to real remote func listenLocalConn(rAddr, lAddr net.Addr, tunnel C.Tunnel) (*net.UDPConn, error) { additions := []inbound.Addition{ inbound.WithInName("DEFAULT-TPROXY"), inbound.WithSpecialRules(""), } lc, err := dialUDP("udp", rAddr.(*net.UDPAddr).AddrPort(), lAddr.(*net.UDPAddr).AddrPort()) if err != nil { return nil, err } go func() { log.Debugln("TProxy listenLocalConn rAddr=%s lAddr=%s", rAddr.String(), lAddr.String()) for { buf := pool.Get(pool.UDPBufferSize) br, err := lc.Read(buf) if err != nil { if errors.Is(err, net.ErrClosed) { log.Debugln("TProxy local conn listener exit.. rAddr=%s lAddr=%s", rAddr.String(), lAddr.String()) pool.Put(buf) return } } // since following localPackets are pass through this socket which listen rAddr // I choose current listener as packet's packet conn handlePacketConn(lc, tunnel, buf[:br], lAddr.(*net.UDPAddr).AddrPort(), rAddr.(*net.UDPAddr).AddrPort(), additions...) } }() return lc, nil } ================================================ FILE: core/Clash.Meta/listener/tproxy/setsockopt_linux.go ================================================ //go:build linux package tproxy import ( "net" "syscall" ) func setsockopt(rc syscall.RawConn, addr string) error { isIPv6 := true host, _, err := net.SplitHostPort(addr) if err != nil { return err } ip := net.ParseIP(host) if ip != nil && ip.To4() != nil { isIPv6 = false } rc.Control(func(fd uintptr) { 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, IPV6_TRANSPARENT, 1) } 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, IPV6_RECVORIGDSTADDR, 1) } if err == nil { _ = setDSCPsockopt(fd, isIPv6) } }) return err } func setDSCPsockopt(fd uintptr, isIPv6 bool) (err error) { if err == nil { err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVTOS, 1) } if err == nil && isIPv6 { err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, syscall.IPV6_RECVTCLASS, 1) } return } ================================================ FILE: core/Clash.Meta/listener/tproxy/setsockopt_other.go ================================================ //go:build !linux package tproxy import ( "errors" "syscall" ) func setsockopt(rc syscall.RawConn, addr string) error { return errors.New("not supported on current platform") } ================================================ FILE: core/Clash.Meta/listener/tproxy/tproxy.go ================================================ package tproxy import ( "context" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/mptcp" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) type Listener struct { listener net.Listener addr string closed bool } // RawAddress implements C.Listener func (l *Listener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *Listener) Address() string { return l.listener.Addr().String() } // Close implements C.Listener func (l *Listener) Close() error { l.closed = true return l.listener.Close() } func (l *Listener) handleTProxy(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { target := socks5.ParseAddrToSocksAddr(conn.LocalAddr()) keepalive.TCPKeepAlive(conn) // TProxy's conn.LocalAddr() is target address, so we set from l.listener additions = append([]inbound.Addition{inbound.WithInAddr(l.listener.Addr())}, additions...) tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.TPROXY, additions...)) } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TPROXY"), inbound.WithSpecialRules(""), } } // Golang will then enable mptcp support for listeners by default when the major version of go.mod is 1.24 or higher. // This can cause tproxy to malfunction on certain Linux kernel versions, so we force to disable mptcp for tproxy. lc := net.ListenConfig{} mptcp.SetNetListenConfig(&lc, false) l, err := lc.Listen(context.Background(), "tcp", addr) if err != nil { return nil, err } tl := l.(*net.TCPListener) rc, err := tl.SyscallConn() if err != nil { return nil, err } err = setsockopt(rc, addr) if err != nil { return nil, err } rl := &Listener{ listener: l, addr: addr, } go func() { for { c, err := l.Accept() if err != nil { if rl.closed { break } continue } go rl.handleTProxy(c, tunnel, additions...) } }() return rl, nil } ================================================ FILE: core/Clash.Meta/listener/tproxy/tproxy_iptables.go ================================================ package tproxy import ( "errors" "fmt" "net" "runtime" "github.com/metacubex/mihomo/common/cmd" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/log" ) var ( dnsPort uint16 tProxyPort uint16 interfaceName string DnsRedirect bool ) const ( PROXY_FWMARK = "0x2d0" PROXY_ROUTE_TABLE = "0x2d0" ) func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dnsredir bool, dport uint16) error { if _, err := cmd.ExecCmd("iptables -V"); err != nil { return fmt.Errorf("current operations system [%s] are not support iptables or command iptables does not exist", runtime.GOOS) } if ifname == "" { return errors.New("the 'interface-name' can not be empty") } interfaceName = ifname tProxyPort = tport DnsRedirect = dnsredir dnsPort = dport // add route execCmd(fmt.Sprintf("ip -f inet rule add fwmark %s lookup %s", PROXY_FWMARK, PROXY_ROUTE_TABLE)) execCmd(fmt.Sprintf("ip -f inet route add local default dev %s table %s", interfaceName, PROXY_ROUTE_TABLE)) // set FORWARD if interfaceName != "lo" { execCmd("sysctl -w net.ipv4.ip_forward=1") execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -o %s -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT", interfaceName)) execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -o %s -j ACCEPT", interfaceName)) execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -i %s ! -o %s -j ACCEPT", interfaceName, interfaceName)) execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -i %s -o %s -j ACCEPT", interfaceName, interfaceName)) } // set mihomo divert execCmd("iptables -t mangle -N mihomo_divert") execCmd("iptables -t mangle -F mihomo_divert") execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_divert -j MARK --set-mark %s", PROXY_FWMARK)) execCmd("iptables -t mangle -A mihomo_divert -j ACCEPT") // set pre routing execCmd("iptables -t mangle -N mihomo_prerouting") execCmd("iptables -t mangle -F mihomo_prerouting") execCmd("iptables -t mangle -A mihomo_prerouting -s 172.17.0.0/16 -j RETURN") if DnsRedirect { execCmd("iptables -t mangle -A mihomo_prerouting -p udp --dport 53 -j ACCEPT") execCmd("iptables -t mangle -A mihomo_prerouting -p tcp --dport 53 -j ACCEPT") } execCmd("iptables -t mangle -A mihomo_prerouting -m addrtype --dst-type LOCAL -j RETURN") addLocalnetworkToChain("mihomo_prerouting", bypass) execCmd("iptables -t mangle -A mihomo_prerouting -p tcp -m socket -j mihomo_divert") execCmd("iptables -t mangle -A mihomo_prerouting -p udp -m socket -j mihomo_divert") execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_prerouting -p tcp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_prerouting -p udp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) execCmd("iptables -t mangle -A PREROUTING -j mihomo_prerouting") if DnsRedirect { execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) } // set post routing if interfaceName != "lo" { execCmd(fmt.Sprintf("iptables -t nat -A POSTROUTING -o %s -m addrtype ! --src-type LOCAL -j MASQUERADE", interfaceName)) } // set output execCmd("iptables -t mangle -N mihomo_output") execCmd("iptables -t mangle -F mihomo_output") execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) if DnsRedirect { execCmd("iptables -t mangle -A mihomo_output -p udp -m multiport --dports 53,123,137 -j ACCEPT") execCmd("iptables -t mangle -A mihomo_output -p tcp --dport 53 -j ACCEPT") } execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type LOCAL -j RETURN") execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type BROADCAST -j RETURN") addLocalnetworkToChain("mihomo_output", bypass) execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -p tcp -j MARK --set-mark %s", PROXY_FWMARK)) execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -p udp -j MARK --set-mark %s", PROXY_FWMARK)) execCmd(fmt.Sprintf("iptables -t mangle -I OUTPUT -o %s -j mihomo_output", interfaceName)) // set dns output if DnsRedirect { execCmd("iptables -t nat -N mihomo_dns_output") execCmd("iptables -t nat -F mihomo_dns_output") execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) execCmd("iptables -t nat -A mihomo_dns_output -s 172.17.0.0/16 -j RETURN") execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p udp -j REDIRECT --to-ports %d", dnsPort)) execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p tcp -j REDIRECT --to-ports %d", dnsPort)) execCmd("iptables -t nat -I OUTPUT -p tcp --dport 53 -j mihomo_dns_output") execCmd("iptables -t nat -I OUTPUT -p udp --dport 53 -j mihomo_dns_output") } return nil } func CleanupTProxyIPTables() { if runtime.GOOS != "linux" || interfaceName == "" || tProxyPort == 0 { return } log.Warnln("Cleanup tproxy linux iptables") dialer.DefaultRoutingMark.CompareAndSwap(2158, 0) if _, err := cmd.ExecCmd("iptables -t mangle -L mihomo_divert"); err != nil { return } // clean route execCmd(fmt.Sprintf("ip -f inet rule del fwmark %s lookup %s", PROXY_FWMARK, PROXY_ROUTE_TABLE)) execCmd(fmt.Sprintf("ip -f inet route del local default dev %s table %s", interfaceName, PROXY_ROUTE_TABLE)) // clean FORWARD if interfaceName != "lo" { execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -i %s ! -o %s -j ACCEPT", interfaceName, interfaceName)) execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -i %s -o %s -j ACCEPT", interfaceName, interfaceName)) execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -o %s -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT", interfaceName)) execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -o %s -j ACCEPT", interfaceName)) } // clean PREROUTING if DnsRedirect { execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) } execCmd("iptables -t mangle -D PREROUTING -j mihomo_prerouting") // clean POSTROUTING if interfaceName != "lo" { execCmd(fmt.Sprintf("iptables -t nat -D POSTROUTING -o %s -m addrtype ! --src-type LOCAL -j MASQUERADE", interfaceName)) } // clean OUTPUT execCmd(fmt.Sprintf("iptables -t mangle -D OUTPUT -o %s -j mihomo_output", interfaceName)) if DnsRedirect { execCmd("iptables -t nat -D OUTPUT -p tcp --dport 53 -j mihomo_dns_output") execCmd("iptables -t nat -D OUTPUT -p udp --dport 53 -j mihomo_dns_output") } // clean chain execCmd("iptables -t mangle -F mihomo_prerouting") execCmd("iptables -t mangle -X mihomo_prerouting") execCmd("iptables -t mangle -F mihomo_divert") execCmd("iptables -t mangle -X mihomo_divert") execCmd("iptables -t mangle -F mihomo_output") execCmd("iptables -t mangle -X mihomo_output") if DnsRedirect { execCmd("iptables -t nat -F mihomo_dns_output") execCmd("iptables -t nat -X mihomo_dns_output") } interfaceName = "" tProxyPort = 0 dnsPort = 0 } func addLocalnetworkToChain(chain string, bypass []string) { for _, bp := range bypass { _, _, err := net.ParseCIDR(bp) if err != nil { log.Warnln("[IPTABLES] %s", err) continue } execCmd(fmt.Sprintf("iptables -t mangle -A %s -d %s -j RETURN", chain, bp)) } execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 0.0.0.0/8 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 10.0.0.0/8 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 100.64.0.0/10 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 127.0.0.0/8 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 169.254.0.0/16 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 172.16.0.0/12 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.0.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.2.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.88.99.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.168.0.0/16 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 198.51.100.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 203.0.113.0/24 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 224.0.0.0/4 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 240.0.0.0/4 -j RETURN", chain)) execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 255.255.255.255/32 -j RETURN", chain)) } func execCmd(cmdStr string) { log.Debugln("[IPTABLES] %s", cmdStr) _, err := cmd.ExecCmd(cmdStr) if err != nil { log.Warnln("[IPTABLES] exec cmd: %v", err) } } ================================================ FILE: core/Clash.Meta/listener/tproxy/udp.go ================================================ package tproxy import ( "net" "net/netip" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/pool" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) type UDPListener struct { packetConn net.PacketConn addr string closed bool } // RawAddress implements C.Listener func (l *UDPListener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *UDPListener) Address() string { return l.packetConn.LocalAddr().String() } // Close implements C.Listener func (l *UDPListener) Close() error { l.closed = true return l.packetConn.Close() } func NewUDP(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TPROXY"), inbound.WithSpecialRules(""), } } l, err := net.ListenPacket("udp", addr) if err != nil { return nil, err } rl := &UDPListener{ packetConn: l, addr: addr, } c := l.(*net.UDPConn) rc, err := c.SyscallConn() if err != nil { return nil, err } err = setsockopt(rc, addr) if err != nil { return nil, err } go func() { oob := make([]byte, 1024) for { buf := pool.Get(pool.UDPBufferSize) n, oobn, _, lAddr, err := c.ReadMsgUDPAddrPort(buf, oob) if err != nil { pool.Put(buf) if rl.closed { break } continue } rAddr, err := getOrigDst(oob[:oobn]) if err != nil { pool.Put(buf) continue } dscp, _ := getDSCP(oob[:oobn]) additions := append(additions, inbound.WithDSCP(dscp)) // don't change outside additions if rAddr.Addr().Is4() { // try to unmap 4in6 address lAddr = netip.AddrPortFrom(lAddr.Addr().Unmap(), lAddr.Port()) } handlePacketConn(l, tunnel, buf[:n], lAddr, rAddr, additions...) } }() return rl, nil } func handlePacketConn(pc net.PacketConn, tunnel C.Tunnel, buf []byte, lAddr, rAddr netip.AddrPort, additions ...inbound.Addition) { target := socks5.AddrFromStdAddrPort(rAddr) pkt := &packet{ pc: pc, lAddr: lAddr, buf: buf, tunnel: tunnel, } tunnel.HandleUDPPacket(inbound.NewPacket(target, pkt, C.TPROXY, additions...)) } ================================================ FILE: core/Clash.Meta/listener/tproxy/udp_linux.go ================================================ //go:build linux package tproxy import ( "fmt" "net" "net/netip" "os" "strconv" "syscall" "golang.org/x/sys/unix" ) const ( IPV6_TRANSPARENT = 0x4b IPV6_RECVORIGDSTADDR = 0x4a ) // dialUDP acts like net.DialUDP for transparent proxy. // It binds to a non-local address(`lAddr`). func dialUDP(network string, lAddr, rAddr netip.AddrPort) (uc *net.UDPConn, err error) { rSockAddr, err := udpAddrToSockAddr(rAddr) if err != nil { return nil, err } lSockAddr, err := udpAddrToSockAddr(lAddr) if err != nil { return nil, err } fd, err := syscall.Socket(udpAddrFamily(network, lAddr, rAddr), syscall.SOCK_DGRAM, 0) if err != nil { return nil, err } defer func() { if err != nil { syscall.Close(fd) } }() if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { return nil, err } if err = syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { return nil, err } if err = syscall.Bind(fd, lSockAddr); err != nil { return nil, err } if err = syscall.Connect(fd, rSockAddr); err != nil { return nil, err } fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-dial-%s", rAddr.String())) defer fdFile.Close() c, err := net.FileConn(fdFile) if err != nil { return nil, err } return c.(*net.UDPConn), nil } func udpAddrToSockAddr(addr netip.AddrPort) (syscall.Sockaddr, error) { if addr.Addr().Is4() { return &syscall.SockaddrInet4{Addr: addr.Addr().As4(), Port: int(addr.Port())}, nil } zoneID, err := strconv.ParseUint(addr.Addr().Zone(), 10, 32) if err != nil { zoneID = 0 } return &syscall.SockaddrInet6{Addr: addr.Addr().As16(), Port: int(addr.Port()), ZoneId: uint32(zoneID)}, nil } func udpAddrFamily(net string, lAddr, rAddr netip.AddrPort) int { switch net[len(net)-1] { case '4': return syscall.AF_INET case '6': return syscall.AF_INET6 } if lAddr.Addr().Is4() && rAddr.Addr().Is4() { return syscall.AF_INET } return syscall.AF_INET6 } func getOrigDst(oob []byte) (netip.AddrPort, error) { // oob contains socket control messages which we need to parse. scms, err := unix.ParseSocketControlMessage(oob) if err != nil { return netip.AddrPort{}, fmt.Errorf("parse control message: %w", err) } // retrieve the destination address from the SCM. var sa unix.Sockaddr for i := range scms { sa, err = unix.ParseOrigDstAddr(&scms[i]) if err == nil { break } } if err != nil { return netip.AddrPort{}, fmt.Errorf("retrieve destination: %w", err) } // encode the destination address into a cmsg. var rAddr netip.AddrPort switch v := sa.(type) { case *unix.SockaddrInet4: rAddr = netip.AddrPortFrom(netip.AddrFrom4(v.Addr), uint16(v.Port)) case *unix.SockaddrInet6: rAddr = netip.AddrPortFrom(netip.AddrFrom16(v.Addr), uint16(v.Port)) default: return netip.AddrPort{}, fmt.Errorf("unsupported address type: %T", v) } return rAddr, nil } func getDSCP(oob []byte) (uint8, error) { scms, err := unix.ParseSocketControlMessage(oob) if err != nil { return 0, fmt.Errorf("parse control message: %w", err) } var dscp uint8 for i := range scms { dscp, err = parseDSCP(&scms[i]) if err == nil { break } } if err != nil { return 0, fmt.Errorf("retrieve DSCP: %w", err) } return dscp, nil } func parseDSCP(m *unix.SocketControlMessage) (uint8, error) { switch { case m.Header.Level == unix.SOL_IP && m.Header.Type == unix.IP_TOS: dscp := uint8(m.Data[0] >> 2) return dscp, nil case m.Header.Level == unix.SOL_IPV6 && m.Header.Type == unix.IPV6_TCLASS: dscp := uint8(m.Data[0] >> 2) return dscp, nil default: return 0, nil } } ================================================ FILE: core/Clash.Meta/listener/tproxy/udp_other.go ================================================ //go:build !linux package tproxy import ( "errors" "net" "net/netip" ) func getOrigDst(oob []byte) (netip.AddrPort, error) { return netip.AddrPort{}, errors.New("UDP redir not supported on current platform") } func getDSCP(oob []byte) (uint8, error) { return 0, errors.New("UDP redir not supported on current platform") } func dialUDP(network string, lAddr, rAddr netip.AddrPort) (*net.UDPConn, error) { return nil, errors.New("UDP redir not supported on current platform") } ================================================ FILE: core/Clash.Meta/listener/trojan/packet.go ================================================ package trojan import ( "errors" "net" ) type packet struct { pc net.PacketConn rAddr net.Addr payload []byte put func() } func (c *packet) Data() []byte { return c.payload } // WriteBack wirtes UDP packet with source(ip, port) = `addr` func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { if addr == nil { err = errors.New("address is invalid") return } return c.pc.WriteTo(b, addr) } // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { return c.rAddr } func (c *packet) Drop() { if c.put != nil { c.put() c.put = nil } c.payload = nil } func (c *packet) InAddr() net.Addr { return c.pc.LocalAddr() } ================================================ FILE: core/Clash.Meta/listener/trojan/server.go ================================================ package trojan import ( "errors" "io" "net" "strings" "time" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/trojan" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" "github.com/metacubex/smux" "github.com/metacubex/tls" ) type Listener struct { closed bool config LC.TrojanServer listeners []net.Listener keys map[[trojan.KeyLength]byte]string pickCipher core.Cipher handler *sing.ListenerHandler } func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TROJAN"), inbound.WithSpecialRules(""), } } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.TROJAN, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } keys := make(map[[trojan.KeyLength]byte]string) for _, user := range config.Users { keys[trojan.Key(user.Password)] = user.Username } var pickCipher core.Cipher if config.TrojanSSOption.Enabled { if config.TrojanSSOption.Password == "" { return nil, errors.New("empty password") } if config.TrojanSSOption.Method == "" { config.TrojanSSOption.Method = "AES-128-GCM" } pickCipher, err = core.PickCipher(config.TrojanSSOption.Method, nil, config.TrojanSSOption.Password) if err != nil { return nil, err } } sl = &Listener{false, config, nil, keys, pickCipher, h} httpServer := http.Server{ IdleTimeout: 30 * time.Second, Protocols: new(http.Protocols), } tlsConfig := &tls.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.RealityConfig.PrivateKey != "" { if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in reality") } if tlsConfig.ClientAuth != tls.NoClientCert { return nil, errors.New("client-auth is unavailable in reality") } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err } } if config.WsPath != "" { httpMux := http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) if err != nil { http.Error(w, err.Error(), 500) return } sl.HandleConn(conn, tunnel, additions...) }) httpServer.Handler = httpMux httpServer.Protocols.SetHTTP1(true) tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } if config.GrpcServiceName != "" { httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, ConnHandler: func(conn net.Conn) { sl.HandleConn(conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, }) httpServer.Protocols.SetHTTP2(true) // SetUnencryptedHTTP2 to ensure we can work in plain http2 and some tls conn is not *tls.Conn (like *reality.Conn) httpServer.Protocols.SetUnencryptedHTTP2(true) tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 } for _, addr := range strings.Split(config.Listen, ",") { addr := addr //TCP l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else if !config.TrojanSSOption.Enabled { return nil, errors.New("disallow using Trojan without both certificates/reality/ss config") } sl.listeners = append(sl.listeners, l) go func() { if httpServer.Handler != nil { _ = httpServer.Serve(l) return } for { c, err := l.Accept() if err != nil { if sl.closed { break } continue } go sl.HandleConn(c, tunnel, additions...) } }() } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, lis := range l.listeners { err := lis.Close() if err != nil { retErr = err } } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.listeners { addrList = append(addrList, lis.Addr()) } return } func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { defer conn.Close() if l.pickCipher != nil { conn = l.pickCipher.StreamConn(conn) } var key [trojan.KeyLength]byte if _, err := io.ReadFull(conn, key[:]); err != nil { //log.Warnln("read key error: %s", err.Error()) return } if user, ok := l.keys[key]; ok { additions = append(additions, inbound.WithInUser(user)) } else { //log.Warnln("no such key") return } var crlf [2]byte if _, err := io.ReadFull(conn, crlf[:]); err != nil { //log.Warnln("read crlf error: %s", err.Error()) return } l.handleConn(false, conn, tunnel, additions...) } func (l *Listener) handleConn(inMux bool, conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { if inMux { defer conn.Close() } command, err := socks5.ReadByte(conn) if err != nil { //log.Warnln("read command error: %s", err.Error()) return } switch command { case trojan.CommandTCP, trojan.CommandUDP, trojan.CommandMux: default: //log.Warnln("unknown command: %d", command) return } target, err := socks5.ReadAddr0(conn) if err != nil { //log.Warnln("read target error: %s", err.Error()) return } if !inMux { var crlf [2]byte if _, err := io.ReadFull(conn, crlf[:]); err != nil { //log.Warnln("read crlf error: %s", err.Error()) return } } switch command { case trojan.CommandTCP: //tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.TROJAN, additions...)) l.handler.HandleSocket(target, conn, additions...) case trojan.CommandUDP: pc := trojan.NewPacketConn(conn) remoteAddr := conn.RemoteAddr() connID := utils.NewUUIDV4().String() // make a new SNAT key for { data, put, addr, err := pc.WaitReadFrom() if err != nil { if put != nil { put() } break } target := socks5.ParseAddrToSocksAddr(addr) cPacket := &packet{ pc: pc, rAddr: remoteAddr, payload: data, put: put, } cPacket.rAddr = N.NewCustomAddr(C.TROJAN.String(), connID, cPacket.rAddr) // for tunnel's handleUDPConn tunnel.HandleUDPPacket(inbound.NewPacket(target, cPacket, C.TROJAN, additions...)) } case trojan.CommandMux: if inMux { //log.Warnln("invalid command: %d", command) return } smuxConfig := smux.DefaultConfig() smuxConfig.KeepAliveDisabled = true session, err := smux.Server(conn, smuxConfig) if err != nil { //log.Warnln("smux server error: %s", err.Error()) return } defer session.Close() for { stream, err := session.AcceptStream() if err != nil { return } go l.handleConn(true, stream, tunnel, additions...) } } } ================================================ FILE: core/Clash.Meta/listener/trusttunnel/server.go ================================================ package trusttunnel import ( "context" "errors" "net" "strings" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/trusttunnel" "github.com/metacubex/tls" ) type Listener struct { closed bool config LC.TrustTunnelServer listeners []net.Listener udpListeners []net.PacketConn tlsConfig *tls.Config services []*trusttunnel.Service } func New(config LC.TrustTunnelServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TRUSTTUNNEL"), inbound.WithSpecialRules(""), } } tlsConfig := &tls.Config{Time: ntp.Now} if config.Certificate != "" && config.PrivateKey != "" { certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } sl = &Listener{ config: config, tlsConfig: tlsConfig, } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.TRUSTTUNNEL, Additions: additions, }) if err != nil { return nil, err } if tlsConfig.GetCertificate == nil { return nil, errors.New("disallow using TrustTunnel without certificates config") } if len(config.Network) == 0 { config.Network = []string{"tcp"} } listenTCP, listenUDP := false, false for _, network := range config.Network { network = strings.ToLower(network) switch { case strings.HasPrefix(network, "tcp"): listenTCP = true case strings.HasPrefix(network, "udp"): listenUDP = true } } for _, addr := range strings.Split(config.Listen, ",") { addr := addr var ( tcpListener net.Listener udpConn net.PacketConn ) if listenTCP { tcpListener, err = inbound.Listen("tcp", addr) if err != nil { _ = sl.Close() return nil, err } sl.listeners = append(sl.listeners, tcpListener) } if listenUDP { udpConn, err = inbound.ListenPacket("udp", addr) if err != nil { _ = sl.Close() return nil, err } if err := sockopt.UDPReuseaddr(udpConn); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } sl.udpListeners = append(sl.udpListeners, udpConn) } service := trusttunnel.NewService(trusttunnel.ServiceOptions{ Ctx: context.Background(), Logger: log.SingLogger, Handler: h, ICMPHandler: nil, QUICCongestionControl: config.CongestionController, QUICCwnd: config.CWND, QUICBBRProfile: config.BBRProfile, }) service.UpdateUsers(config.Users) err = service.Start(tcpListener, udpConn, tlsConfig) if err != nil { _ = sl.Close() return nil, err } sl.services = append(sl.services, service) } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, lis := range l.services { err := lis.Close() if err != nil { retErr = err } } for _, lis := range l.listeners { err := lis.Close() if err != nil { retErr = err } } for _, lis := range l.udpListeners { err := lis.Close() if err != nil { retErr = err } } return retErr } func (l *Listener) Config() string { return l.config.String() } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.listeners { addrList = append(addrList, lis.Addr()) } for _, lis := range l.udpListeners { addrList = append(addrList, lis.LocalAddr()) } return } ================================================ FILE: core/Clash.Meta/listener/tuic/server.go ================================================ package tuic import ( "net" "strings" "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/tuic" "github.com/gofrs/uuid/v5" "github.com/metacubex/quic-go" "github.com/metacubex/tls" "golang.org/x/exp/slices" ) const ServerMaxIncomingStreams = (1 << 32) - 1 type Listener struct { closed bool config LC.TuicServer udpListeners []net.PacketConn servers []*tuic.Server } func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TUIC"), inbound.WithSpecialRules(""), } } h, err := sing.NewListenerHandler(sing.ListenerConfig{ Tunnel: tunnel, Type: C.TUIC, Additions: additions, MuxOption: config.MuxOption, }) if err != nil { return nil, err } tlsConfig := &tls.Config{ Time: ntp.Now, MinVersion: tls.VersionTLS13, } certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey) if err != nil { return nil, err } tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return certLoader() } tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType) if len(config.ClientAuthCert) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { pool, err := ca.LoadCertificates(config.ClientAuthCert) if err != nil { return nil, err } tlsConfig.ClientCAs = pool } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig) if err != nil { return nil, err } } if len(config.ALPN) > 0 { tlsConfig.NextProtos = config.ALPN } else { tlsConfig.NextProtos = []string{"h3"} } if config.MaxIdleTime == 0 { config.MaxIdleTime = 15000 } if config.AuthenticationTimeout == 0 { config.AuthenticationTimeout = 1000 } quicConfig := &quic.Config{ MaxIdleTimeout: time.Duration(config.MaxIdleTime) * time.Millisecond, MaxIncomingStreams: ServerMaxIncomingStreams, MaxIncomingUniStreams: ServerMaxIncomingStreams, EnableDatagrams: true, Allow0RTT: true, DisablePathManager: true, // for port hopping } quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10 quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow packetOverHead := tuic.PacketOverHeadV4 if len(config.Token) == 0 { packetOverHead = tuic.PacketOverHeadV5 } if config.CWND == 0 { config.CWND = 32 } if config.MaxUdpRelayPacketSize == 0 { config.MaxUdpRelayPacketSize = 1500 } maxDatagramFrameSize := config.MaxUdpRelayPacketSize + packetOverHead if maxDatagramFrameSize > 1400 { maxDatagramFrameSize = 1400 } config.MaxUdpRelayPacketSize = maxDatagramFrameSize - packetOverHead quicConfig.MaxDatagramFrameSize = int64(maxDatagramFrameSize) handleTcpFn := func(conn net.Conn, addr socks5.Addr, _additions ...inbound.Addition) error { //newAdditions := additions //if len(_additions) > 0 { // newAdditions = slices.Clone(additions) // newAdditions = append(newAdditions, _additions...) //} //conn, metadata := inbound.NewSocket(addr, conn, C.TUIC, newAdditions...) //go tunnel.HandleTCPConn(conn, metadata) go h.HandleSocket(addr, conn, _additions...) // h.HandleSocket will block, so open a new goroutine return nil } handleUdpFn := func(addr socks5.Addr, packet C.UDPPacket, _additions ...inbound.Addition) error { newAdditions := additions if len(_additions) > 0 { newAdditions = slices.Clone(additions) newAdditions = append(newAdditions, _additions...) } tunnel.HandleUDPPacket(inbound.NewPacket(addr, packet, C.TUIC, newAdditions...)) return nil } option := &tuic.ServerOption{ HandleTcpFn: handleTcpFn, HandleUdpFn: handleUdpFn, TlsConfig: tlsConfig, QuicConfig: quicConfig, CongestionController: config.CongestionController, AuthenticationTimeout: time.Duration(config.AuthenticationTimeout) * time.Millisecond, MaxUdpRelayPacketSize: config.MaxUdpRelayPacketSize, CWND: config.CWND, BBRProfile: config.BBRProfile, } if len(config.Token) > 0 { tokens := make([][32]byte, len(config.Token)) for i, token := range config.Token { tokens[i] = tuic.GenTKN(token) } option.Tokens = tokens } if len(config.Users) > 0 { users := make(map[[16]byte]string) for _uuid, password := range config.Users { users[uuid.FromStringOrNil(_uuid)] = password } option.Users = users } sl := &Listener{false, config, nil, nil} for _, addr := range strings.Split(config.Listen, ",") { addr := addr ul, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } if err := sockopt.UDPReuseaddr(ul); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } sl.udpListeners = append(sl.udpListeners, ul) var server *tuic.Server server, err = tuic.NewServer(option, ul) if err != nil { return nil, err } sl.servers = append(sl.servers, server) go func() { err := server.Serve() if err != nil { if sl.closed { return } } }() } return sl, nil } func (l *Listener) Close() error { l.closed = true var retErr error for _, lis := range l.servers { err := lis.Close() if err != nil { retErr = err } } for _, lis := range l.udpListeners { err := lis.Close() if err != nil { retErr = err } } return retErr } func (l *Listener) Config() LC.TuicServer { return l.config } func (l *Listener) AddrList() (addrList []net.Addr) { for _, lis := range l.udpListeners { addrList = append(addrList, lis.LocalAddr()) } return } ================================================ FILE: core/Clash.Meta/listener/tunnel/packet.go ================================================ package tunnel import ( "net" "github.com/metacubex/mihomo/common/pool" ) type packet struct { pc net.PacketConn rAddr net.Addr payload []byte } func (c *packet) Data() []byte { return c.payload } // WriteBack write UDP packet with source(ip, port) = `addr` func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { return c.pc.WriteTo(b, c.rAddr) } // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { return c.rAddr } func (c *packet) Drop() { _ = pool.Put(c.payload) c.payload = nil } func (c *packet) InAddr() net.Addr { return c.pc.LocalAddr() } ================================================ FILE: core/Clash.Meta/listener/tunnel/tcp.go ================================================ package tunnel import ( "fmt" "net" "github.com/metacubex/mihomo/adapter/inbound" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) type Listener struct { listener net.Listener addr string target socks5.Addr proxy string closed bool } // RawAddress implements C.Listener func (l *Listener) RawAddress() string { return l.addr } // Address implements C.Listener func (l *Listener) Address() string { return l.listener.Addr().String() } // Close implements C.Listener func (l *Listener) Close() error { l.closed = true return l.listener.Close() } func (l *Listener) handleTCP(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { tunnel.HandleTCPConn(inbound.NewSocket(l.target, conn, C.TUNNEL, additions...)) } func New(addr, target, proxy string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } targetAddr := socks5.ParseAddr(target) if targetAddr == nil { return nil, fmt.Errorf("invalid target address %s", target) } rl := &Listener{ listener: l, target: targetAddr, proxy: proxy, addr: addr, } if proxy != "" { additions = append([]inbound.Addition{inbound.WithSpecialProxy(proxy)}, additions...) } go func() { for { c, err := l.Accept() if err != nil { if rl.closed { break } continue } go rl.handleTCP(c, tunnel, additions...) } }() return rl, nil } ================================================ FILE: core/Clash.Meta/listener/tunnel/udp.go ================================================ package tunnel import ( "fmt" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/pool" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) type PacketConn struct { conn net.PacketConn addr string target socks5.Addr proxy string closed bool } // RawAddress implements C.Listener func (l *PacketConn) RawAddress() string { return l.addr } // Address implements C.Listener func (l *PacketConn) Address() string { return l.conn.LocalAddr().String() } // Close implements C.Listener func (l *PacketConn) Close() error { l.closed = true return l.conn.Close() } func NewUDP(addr, target, proxy string, tunnel C.Tunnel, additions ...inbound.Addition) (*PacketConn, error) { l, err := net.ListenPacket("udp", addr) if err != nil { return nil, err } targetAddr := socks5.ParseAddr(target) if targetAddr == nil { return nil, fmt.Errorf("invalid target address %s", target) } sl := &PacketConn{ conn: l, target: targetAddr, proxy: proxy, addr: addr, } if proxy != "" { additions = append([]inbound.Addition{inbound.WithSpecialProxy(proxy)}, additions...) } go func() { for { buf := pool.Get(pool.UDPBufferSize) n, remoteAddr, err := l.ReadFrom(buf) if err != nil { pool.Put(buf) if sl.closed { break } continue } sl.handleUDP(l, tunnel, buf[:n], remoteAddr, additions...) } }() return sl, nil } func (l *PacketConn) handleUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, addr net.Addr, additions ...inbound.Addition) { cPacket := &packet{ pc: pc, rAddr: addr, payload: buf, } tunnel.HandleUDPPacket(inbound.NewPacket(l.target, cPacket, C.TUNNEL, additions...)) } ================================================ FILE: core/Clash.Meta/log/level.go ================================================ package log import ( "errors" "strings" ) // LogLevelMapping is a mapping for LogLevel enum var LogLevelMapping = map[string]LogLevel{ ERROR.String(): ERROR, WARNING.String(): WARNING, INFO.String(): INFO, DEBUG.String(): DEBUG, SILENT.String(): SILENT, } const ( DEBUG LogLevel = iota INFO WARNING ERROR SILENT ) type LogLevel int // UnmarshalText unserialize LogLevel func (l *LogLevel) UnmarshalText(data []byte) error { level, exist := LogLevelMapping[strings.ToLower(string(data))] if !exist { return errors.New("invalid log-level") } *l = level return nil } // MarshalText serialize LogLevel func (l LogLevel) MarshalText() ([]byte, error) { return []byte(l.String()), nil } func (l LogLevel) String() string { switch l { case INFO: return "info" case WARNING: return "warning" case ERROR: return "error" case DEBUG: return "debug" case SILENT: return "silent" default: return "unknown" } } ================================================ FILE: core/Clash.Meta/log/log.go ================================================ package log import ( "fmt" "os" "github.com/metacubex/mihomo/common/observable" log "github.com/sirupsen/logrus" ) var ( logCh = make(chan Event) source = observable.NewObservable[Event](logCh) level = INFO ) func init() { log.SetOutput(os.Stdout) log.SetLevel(log.DebugLevel) log.SetFormatter(&log.TextFormatter{ FullTimestamp: true, TimestampFormat: "2006-01-02T15:04:05.000000000Z07:00", EnvironmentOverrideColors: true, }) } type Event struct { LogLevel LogLevel Payload string } func (e *Event) Type() string { return e.LogLevel.String() } func Infoln(format string, v ...any) { event := newLog(INFO, format, v...) logCh <- event print(event) } func Warnln(format string, v ...any) { event := newLog(WARNING, format, v...) logCh <- event print(event) } func Errorln(format string, v ...any) { event := newLog(ERROR, format, v...) logCh <- event print(event) } func Debugln(format string, v ...any) { event := newLog(DEBUG, format, v...) logCh <- event print(event) } func Fatalln(format string, v ...any) { log.Fatalf(format, v...) } func Subscribe() observable.Subscription[Event] { sub, _ := source.Subscribe() return sub } func UnSubscribe(sub observable.Subscription[Event]) { source.UnSubscribe(sub) } func Level() LogLevel { return level } func SetLevel(newLevel LogLevel) { level = newLevel } func print(data Event) { if data.LogLevel < level { return } switch data.LogLevel { case INFO: log.Infoln(data.Payload) case WARNING: log.Warnln(data.Payload) case ERROR: log.Errorln(data.Payload) case DEBUG: log.Debugln(data.Payload) } } func newLog(logLevel LogLevel, format string, v ...any) Event { return Event{ LogLevel: logLevel, Payload: fmt.Sprintf(format, v...), } } ================================================ FILE: core/Clash.Meta/log/sing.go ================================================ package log import ( "context" "fmt" L "github.com/metacubex/sing/common/logger" ) type singLogger struct{} func (l singLogger) TraceContext(ctx context.Context, args ...any) { Debugln(fmt.Sprint(args...)) } func (l singLogger) DebugContext(ctx context.Context, args ...any) { Debugln(fmt.Sprint(args...)) } func (l singLogger) InfoContext(ctx context.Context, args ...any) { Infoln(fmt.Sprint(args...)) } func (l singLogger) WarnContext(ctx context.Context, args ...any) { Warnln(fmt.Sprint(args...)) } func (l singLogger) ErrorContext(ctx context.Context, args ...any) { Errorln(fmt.Sprint(args...)) } func (l singLogger) FatalContext(ctx context.Context, args ...any) { Fatalln(fmt.Sprint(args...)) } func (l singLogger) PanicContext(ctx context.Context, args ...any) { Fatalln(fmt.Sprint(args...)) } func (l singLogger) Trace(args ...any) { Debugln(fmt.Sprint(args...)) } func (l singLogger) Debug(args ...any) { Debugln(fmt.Sprint(args...)) } func (l singLogger) Info(args ...any) { Infoln(fmt.Sprint(args...)) } func (l singLogger) Warn(args ...any) { Warnln(fmt.Sprint(args...)) } func (l singLogger) Error(args ...any) { Errorln(fmt.Sprint(args...)) } func (l singLogger) Fatal(args ...any) { Fatalln(fmt.Sprint(args...)) } func (l singLogger) Panic(args ...any) { Fatalln(fmt.Sprint(args...)) } type singInfoToDebugLogger struct { singLogger } func (l singInfoToDebugLogger) InfoContext(ctx context.Context, args ...any) { Debugln(fmt.Sprint(args...)) } func (l singInfoToDebugLogger) Info(args ...any) { Debugln(fmt.Sprint(args...)) } var SingLogger L.ContextLogger = singLogger{} var SingInfoToDebugLogger L.ContextLogger = singInfoToDebugLogger{} ================================================ FILE: core/Clash.Meta/main.go ================================================ package main import ( "context" "encoding/base64" "flag" "fmt" "io" "net" "os" "os/signal" "path/filepath" "runtime" "strings" "syscall" "github.com/metacubex/mihomo/common/cmd" "github.com/metacubex/mihomo/component/generator" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/rules/provider" "go.uber.org/automaxprocs/maxprocs" ) var ( version bool testConfig bool geodataMode bool homeDir string configFile string configString string configBytes []byte externalUI string externalController string externalControllerUnix string externalControllerPipe string secret string postUp string postDown string ) func init() { flag.StringVar(&homeDir, "d", os.Getenv("CLASH_HOME_DIR"), "set configuration directory") flag.StringVar(&configFile, "f", os.Getenv("CLASH_CONFIG_FILE"), "specify configuration file") flag.StringVar(&configString, "config", os.Getenv("CLASH_CONFIG_STRING"), "specify base64-encoded configuration string") flag.StringVar(&externalUI, "ext-ui", os.Getenv("CLASH_OVERRIDE_EXTERNAL_UI_DIR"), "override external ui directory") flag.StringVar(&externalController, "ext-ctl", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER"), "override external controller address") flag.StringVar(&externalControllerUnix, "ext-ctl-unix", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_UNIX"), "override external controller unix address") flag.StringVar(&externalControllerPipe, "ext-ctl-pipe", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_PIPE"), "override external controller pipe address") flag.StringVar(&secret, "secret", os.Getenv("CLASH_OVERRIDE_SECRET"), "override secret for RESTful API") flag.StringVar(&postUp, "post-up", os.Getenv("CLASH_POST_UP"), "set post-up script") flag.StringVar(&postDown, "post-down", os.Getenv("CLASH_POST_DOWN"), "set post-down script") flag.BoolVar(&geodataMode, "m", false, "set geodata mode") flag.BoolVar(&version, "v", false, "show current version of mihomo") flag.BoolVar(&testConfig, "t", false, "test configuration and exit") flag.Parse() } func main() { // Defensive programming: panic when code mistakenly calls net.DefaultResolver net.DefaultResolver.PreferGo = true net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { //panic("should never be called") buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { buf = buf[:n] break } buf = make([]byte, 2*len(buf)) } fmt.Fprintf(os.Stderr, "panic: should never be called\n\n%s", buf) // always print all goroutine stack os.Exit(2) return nil, nil } _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) if len(os.Args) > 1 && os.Args[1] == "convert-ruleset" { provider.ConvertMain(os.Args[2:]) return } if len(os.Args) > 1 && os.Args[1] == "generate" { generator.Main(os.Args[2:]) return } if version { fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) if tags := features.Tags(); len(tags) != 0 { fmt.Printf("Use tags: %s\n", strings.Join(tags, ", ")) } return } if homeDir != "" { if !filepath.IsAbs(homeDir) { currentDir, _ := os.Getwd() homeDir = filepath.Join(currentDir, homeDir) } C.SetHomeDir(homeDir) } if geodataMode { geodata.SetGeodataMode(true) } if configString != "" { var err error configBytes, err = base64.StdEncoding.DecodeString(configString) if err != nil { log.Fatalln("Initial configuration error: %s", err.Error()) return } } else if configFile == "-" { var err error configBytes, err = io.ReadAll(os.Stdin) if err != nil { log.Fatalln("Initial configuration error: %s", err.Error()) return } } else { if configFile != "" { if !filepath.IsAbs(configFile) { currentDir, _ := os.Getwd() configFile = filepath.Join(currentDir, configFile) } } else { configFile = filepath.Join(C.Path.HomeDir(), C.Path.Config()) } C.SetConfig(configFile) if err := config.Init(C.Path.HomeDir()); err != nil { log.Fatalln("Initial configuration directory error: %s", err.Error()) } } if testConfig { if len(configBytes) != 0 { if _, err := executor.ParseWithBytes(configBytes); err != nil { log.Errorln(err.Error()) fmt.Println("configuration test failed") os.Exit(1) } } else { if _, err := executor.Parse(); err != nil { log.Errorln(err.Error()) fmt.Printf("configuration file %s test failed\n", C.Path.Config()) os.Exit(1) } } fmt.Printf("configuration file %s test is successful\n", C.Path.Config()) return } var options []hub.Option if externalUI != "" { options = append(options, hub.WithExternalUI(externalUI)) } if externalController != "" { options = append(options, hub.WithExternalController(externalController)) } if externalControllerUnix != "" { options = append(options, hub.WithExternalControllerUnix(externalControllerUnix)) } if externalControllerPipe != "" { options = append(options, hub.WithExternalControllerPipe(externalControllerPipe)) } if secret != "" { options = append(options, hub.WithSecret(secret)) } if err := hub.Parse(configBytes, options...); err != nil { log.Fatalln("Parse config error: %s", err.Error()) } if updater.GeoAutoUpdate() { updater.RegisterGeoUpdater() } if postDown != "" { defer func() { if _, err := cmd.ExecShell(postDown); err != nil { log.Errorln("post-down script error: %s", err.Error()) } }() } if postUp != "" { if _, err := cmd.ExecShell(postUp); err != nil { log.Fatalln("post-up script error: %s", err.Error()) } } defer executor.Shutdown() termSign := make(chan os.Signal, 1) hupSign := make(chan os.Signal, 1) signal.Notify(termSign, syscall.SIGINT, syscall.SIGTERM) signal.Notify(hupSign, syscall.SIGHUP) for { select { case <-termSign: return case <-hupSign: if err := hub.Parse(configBytes, options...); err != nil { log.Errorln("Parse config error: %s", err.Error()) } } } } ================================================ FILE: core/Clash.Meta/ntp/ntp/service.go ================================================ package ntp import ( "context" "sync" "time" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" mihomoNtp "github.com/metacubex/mihomo/ntp" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/ntp" ) var globalSrv *Service var globalMu sync.Mutex type Service struct { server M.Socksaddr dialer proxydialer.SingDialer ticker *time.Ticker ctx context.Context cancel context.CancelFunc syncSystemTime bool } func ReCreateNTPService(server string, interval time.Duration, dialerProxy string, syncSystemTime bool) { globalMu.Lock() defer globalMu.Unlock() if globalSrv != nil { globalSrv.Stop() } if server == "" || interval <= 0 { return } ctx, cancel := context.WithCancel(context.Background()) var cDialer C.Dialer = dialer.NewDialer() if dialerProxy != "" { cDialer = proxydialer.NewByName(dialerProxy) } globalSrv = &Service{ server: M.ParseSocksaddr(server), dialer: proxydialer.NewSingDialer(cDialer), ticker: time.NewTicker(interval * time.Minute), ctx: ctx, cancel: cancel, syncSystemTime: syncSystemTime, } globalSrv.Start() } func (srv *Service) Start() { log.Infoln("NTP service start, sync system time is %t", srv.syncSystemTime) go srv.loopUpdate() } func (srv *Service) Stop() { log.Infoln("NTP service stop") srv.cancel() } func (srv *Service) update() error { var response *ntp.Response var err error for i := 0; i < 3; i++ { response, err = ntp.Exchange(srv.ctx, srv.dialer, srv.server) if err != nil { if srv.ctx.Err() != nil { return nil } continue } offset := response.ClockOffset if offset > time.Duration(0) { log.Infoln("System clock is ahead of NTP time by %s", offset) } else if offset < time.Duration(0) { log.Infoln("System clock is behind NTP time by %s", -offset) } mihomoNtp.SetOffset(offset) if srv.syncSystemTime { timeNow := response.Time syncErr := setSystemTime(timeNow) if syncErr == nil { log.Infoln("Sync system time success: %s", timeNow.Local().Format(ntp.TimeLayout)) } else { log.Errorln("Write time to system: %s", syncErr) srv.syncSystemTime = false } } return nil } return err } func (srv *Service) loopUpdate() { defer mihomoNtp.SetOffset(0) defer srv.ticker.Stop() for { err := srv.update() if err != nil { log.Warnln("Sync time failed: %s", err) } select { case <-srv.ctx.Done(): return case <-srv.ticker.C: } } } ================================================ FILE: core/Clash.Meta/ntp/ntp/time_stub.go ================================================ //go:build !(windows || linux || darwin) package ntp import ( "os" "time" ) func setSystemTime(nowTime time.Time) error { return os.ErrInvalid } ================================================ FILE: core/Clash.Meta/ntp/ntp/time_unix.go ================================================ //go:build linux || darwin package ntp import ( "time" "golang.org/x/sys/unix" ) func setSystemTime(nowTime time.Time) error { timeVal := unix.NsecToTimeval(nowTime.UnixNano()) return unix.Settimeofday(&timeVal) } ================================================ FILE: core/Clash.Meta/ntp/ntp/time_windows.go ================================================ package ntp import ( "time" "unsafe" "golang.org/x/sys/windows" ) func setSystemTime(nowTime time.Time) error { var systemTime windows.Systemtime systemTime.Year = uint16(nowTime.Year()) systemTime.Month = uint16(nowTime.Month()) systemTime.Day = uint16(nowTime.Day()) systemTime.Hour = uint16(nowTime.Hour()) systemTime.Minute = uint16(nowTime.Minute()) systemTime.Second = uint16(nowTime.Second()) systemTime.Milliseconds = uint16(nowTime.UnixMilli() - nowTime.Unix()*1000) dllKernel32 := windows.NewLazySystemDLL("kernel32.dll") proc := dllKernel32.NewProc("SetSystemTime") _, _, err := proc.Call( uintptr(unsafe.Pointer(&systemTime)), ) if err != nil && err.Error() != "The operation completed successfully." { return err } return nil } ================================================ FILE: core/Clash.Meta/ntp/time.go ================================================ // Package ntp provide time.Now // // DON'T import other package in mihomo to keep minimal internal dependencies package ntp import ( "time" "sync/atomic" ) var _offset atomic.Int64 // [time.Duration] func SetOffset(offset time.Duration) { _offset.Store(int64(offset)) } func GetOffset() time.Duration { return time.Duration(_offset.Load()) } func Now() time.Time { now := time.Now() if offset := GetOffset(); offset != 0 { now = now.Add(offset) } return now } ================================================ FILE: core/Clash.Meta/rules/common/base.go ================================================ package common import ( "errors" "strings" C "github.com/metacubex/mihomo/constant" "golang.org/x/exp/slices" ) var ( errPayload = errors.New("payloadRule error") ) // params var ( NoResolve = "no-resolve" Src = "src" ) type Base struct { } func (b *Base) ProviderNames() []string { return nil } func ParseParams(params []string) (isSrc bool, noResolve bool) { isSrc = slices.Contains(params, Src) if isSrc { noResolve = true } else { noResolve = slices.Contains(params, NoResolve) } return } func trimArr(arr []string) (r []string) { for _, e := range arr { r = append(r, strings.Trim(e, " ")) } return } // ParseRulePayload parse rule format like: // `tp,payload,target(,params...)` or `tp,payload(,params...)` // needTarget control the format contains `target` in string func ParseRulePayload(ruleRaw string, needTarget bool) (tp, payload, target string, params []string) { item := trimArr(strings.Split(ruleRaw, ",")) tp = strings.ToUpper(item[0]) if len(item) > 1 { switch tp { case "MATCH": // MATCH doesn't contain payload and params target = item[1] case "NOT", "OR", "AND", "SUB-RULE", "DOMAIN-REGEX", "PROCESS-NAME-REGEX", "PROCESS-PATH-REGEX": // some type of rules that has comma in payload and don't need params if needTarget { l := len(item) target = item[l-1] // don't have params so target must at the end of slices item = item[:l-1] // remove the target from slices } payload = strings.Join(item[1:], ",") default: payload = item[1] if len(item) > 2 { if needTarget { target = item[2] if len(item) > 3 { params = item[3:] } } else { params = item[2:] } } } } return } type ParseRuleFunc func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (C.Rule, error) ================================================ FILE: core/Clash.Meta/rules/common/domain.go ================================================ package common import ( "strings" C "github.com/metacubex/mihomo/constant" ) type Domain struct { Base domain string adapter string } func (d *Domain) RuleType() C.RuleType { return C.Domain } func (d *Domain) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { return metadata.RuleHost() == d.domain, d.adapter } func (d *Domain) Adapter() string { return d.adapter } func (d *Domain) Payload() string { return d.domain } func NewDomain(domain string, adapter string) *Domain { return &Domain{ Base: Base{}, domain: strings.ToLower(domain), adapter: adapter, } } var _ C.Rule = (*Domain)(nil) ================================================ FILE: core/Clash.Meta/rules/common/domain_keyword.go ================================================ package common import ( "strings" C "github.com/metacubex/mihomo/constant" ) type DomainKeyword struct { Base keyword string adapter string } func (dk *DomainKeyword) RuleType() C.RuleType { return C.DomainKeyword } func (dk *DomainKeyword) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { domain := metadata.RuleHost() return strings.Contains(domain, dk.keyword), dk.adapter } func (dk *DomainKeyword) Adapter() string { return dk.adapter } func (dk *DomainKeyword) Payload() string { return dk.keyword } func NewDomainKeyword(keyword string, adapter string) *DomainKeyword { return &DomainKeyword{ Base: Base{}, keyword: strings.ToLower(keyword), adapter: adapter, } } var _ C.Rule = (*DomainKeyword)(nil) ================================================ FILE: core/Clash.Meta/rules/common/domain_regex.go ================================================ package common import ( C "github.com/metacubex/mihomo/constant" "github.com/dlclark/regexp2" ) type DomainRegex struct { Base regex *regexp2.Regexp adapter string } func (dr *DomainRegex) RuleType() C.RuleType { return C.DomainRegex } func (dr *DomainRegex) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { domain := metadata.RuleHost() match, _ := dr.regex.MatchString(domain) return match, dr.adapter } func (dr *DomainRegex) Adapter() string { return dr.adapter } func (dr *DomainRegex) Payload() string { return dr.regex.String() } func NewDomainRegex(regex string, adapter string) (*DomainRegex, error) { r, err := regexp2.Compile(regex, regexp2.IgnoreCase) if err != nil { return nil, err } return &DomainRegex{ Base: Base{}, regex: r, adapter: adapter, }, nil } var _ C.Rule = (*DomainRegex)(nil) ================================================ FILE: core/Clash.Meta/rules/common/domain_suffix.go ================================================ package common import ( "strings" C "github.com/metacubex/mihomo/constant" ) type DomainSuffix struct { Base suffix string adapter string } func (ds *DomainSuffix) RuleType() C.RuleType { return C.DomainSuffix } func (ds *DomainSuffix) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { domain := metadata.RuleHost() return strings.HasSuffix(domain, "."+ds.suffix) || domain == ds.suffix, ds.adapter } func (ds *DomainSuffix) Adapter() string { return ds.adapter } func (ds *DomainSuffix) Payload() string { return ds.suffix } func NewDomainSuffix(suffix string, adapter string) *DomainSuffix { return &DomainSuffix{ Base: Base{}, suffix: strings.ToLower(suffix), adapter: adapter, } } var _ C.Rule = (*DomainSuffix)(nil) ================================================ FILE: core/Clash.Meta/rules/common/domain_wildcard.go ================================================ package common import ( "strings" "github.com/metacubex/mihomo/component/wildcard" C "github.com/metacubex/mihomo/constant" ) type DomainWildcard struct { Base pattern string adapter string } func (dw *DomainWildcard) RuleType() C.RuleType { return C.DomainWildcard } func (dw *DomainWildcard) Match(metadata *C.Metadata, _ C.RuleMatchHelper) (bool, string) { return wildcard.Match(dw.pattern, metadata.Host), dw.adapter } func (dw *DomainWildcard) Adapter() string { return dw.adapter } func (dw *DomainWildcard) Payload() string { return dw.pattern } var _ C.Rule = (*DomainWildcard)(nil) func NewDomainWildcard(pattern string, adapter string) (*DomainWildcard, error) { pattern = strings.ToLower(pattern) return &DomainWildcard{ Base: Base{}, pattern: pattern, adapter: adapter, }, nil } var _ C.Rule = (*DomainWildcard)(nil) ================================================ FILE: core/Clash.Meta/rules/common/dscp.go ================================================ package common import ( "fmt" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" ) type DSCP struct { Base ranges utils.IntRanges[uint8] payload string adapter string } func (d *DSCP) RuleType() C.RuleType { return C.DSCP } func (d *DSCP) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { return d.ranges.Check(metadata.DSCP), d.adapter } func (d *DSCP) Adapter() string { return d.adapter } func (d *DSCP) Payload() string { return d.payload } func NewDSCP(dscp string, adapter string) (*DSCP, error) { ranges, err := utils.NewUnsignedRanges[uint8](dscp) if err != nil { return nil, fmt.Errorf("parse DSCP rule fail: %w", err) } for _, r := range ranges { if r.End() > 63 { return nil, fmt.Errorf("DSCP couldn't be negative or exceed 63") } } return &DSCP{ Base: Base{}, payload: dscp, ranges: ranges, adapter: adapter, }, nil } var _ C.Rule = (*DSCP)(nil) ================================================ FILE: core/Clash.Meta/rules/common/final.go ================================================ package common import ( C "github.com/metacubex/mihomo/constant" ) type Match struct { Base adapter string } func (f *Match) RuleType() C.RuleType { return C.MATCH } func (f *Match) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { return true, f.adapter } func (f *Match) Adapter() string { return f.adapter } func (f *Match) Payload() string { return "" } func NewMatch(adapter string) *Match { return &Match{ Base: Base{}, adapter: adapter, } } var _ C.Rule = (*Match)(nil) ================================================ FILE: core/Clash.Meta/rules/common/geoip.go ================================================ package common import ( "errors" "fmt" "net/netip" "strings" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/geodata/router" "github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "golang.org/x/exp/slices" ) type GEOIP struct { Base country string adapter string noResolveIP bool isSourceIP bool } var _ C.Rule = (*GEOIP)(nil) func (g *GEOIP) RuleType() C.RuleType { if g.isSourceIP { return C.SrcGEOIP } return C.GEOIP } func (g *GEOIP) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if !g.noResolveIP && !g.isSourceIP && helper.ResolveIP != nil { helper.ResolveIP() } ip := metadata.DstIP if g.isSourceIP { ip = metadata.SrcIP } if !ip.IsValid() { return false, "" } if g.country == "lan" { return g.isLan(ip), g.adapter } if geodata.GeodataMode() { if g.isSourceIP { if slices.Contains(metadata.SrcGeoIP, g.country) { return true, g.adapter } } else { if slices.Contains(metadata.DstGeoIP, g.country) { return true, g.adapter } } matcher, err := g.getIPMatcher() if err != nil { return false, "" } match := matcher.Match(ip) if match { if g.isSourceIP { metadata.SrcGeoIP = append(metadata.SrcGeoIP, g.country) } else { metadata.DstGeoIP = append(metadata.DstGeoIP, g.country) } } return match, g.adapter } if g.isSourceIP { if metadata.SrcGeoIP != nil { return slices.Contains(metadata.SrcGeoIP, g.country), g.adapter } } else { if metadata.DstGeoIP != nil { return slices.Contains(metadata.DstGeoIP, g.country), g.adapter } } codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) if g.isSourceIP { metadata.SrcGeoIP = codes } else { metadata.DstGeoIP = codes } if slices.Contains(codes, g.country) { return true, g.adapter } return false, "" } // MatchIp implements C.IpMatcher func (g *GEOIP) MatchIp(ip netip.Addr) bool { if !ip.IsValid() { return false } if g.country == "lan" { return g.isLan(ip) } if geodata.GeodataMode() { matcher, err := g.getIPMatcher() if err != nil { return false } return matcher.Match(ip) } codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) return slices.Contains(codes, g.country) } // MatchIp implements C.IpMatcher func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool { if !ip.IsValid() { return false } if g.isLan(ip) { // compatible with original behavior return false } if g.country == "lan" { return !g.isLan(ip) } if geodata.GeodataMode() { matcher, err := g.getIPMatcher() if err != nil { return false } return !matcher.Match(ip) } codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) return !slices.Contains(codes, g.country) } type dnsFallbackFilter struct { *GEOIP } func (g *GEOIP) DnsFallbackFilter() C.IpMatcher { // for dns.fallback-filter.geoip return dnsFallbackFilter{GEOIP: g} } func (g *GEOIP) isLan(ip netip.Addr) bool { return ip.IsPrivate() || ip.IsUnspecified() || ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || resolver.IsFakeBroadcastIP(ip) } func (g *GEOIP) Adapter() string { return g.adapter } func (g *GEOIP) Payload() string { return g.country } func (g *GEOIP) GetCountry() string { return g.country } func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) { if geodata.GeodataMode() { return g.getIPMatcher() } return nil, errors.New("not geodata mode") } func (g *GEOIP) getIPMatcher() (router.IPMatcher, error) { geoIPMatcher, err := geodata.LoadGeoIPMatcher(g.country) if err != nil { return nil, fmt.Errorf("[GeoIP] %w", err) } return geoIPMatcher, nil } func (g *GEOIP) GetRecodeSize() int { // skip pseudorule lan if g.country == "lan" { return 0 } if matcher, err := g.GetIPMatcher(); err == nil { return matcher.Count() } return 0 } func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP, error) { country = strings.ToLower(country) geoip := &GEOIP{ Base: Base{}, country: country, adapter: adapter, noResolveIP: noResolveIP, isSourceIP: isSrc, } if country == "lan" { return geoip, nil } if err := geodata.InitGeoIP(); err != nil { log.Errorln("can't initial GeoIP: %s", err) return nil, err } if geodata.GeodataMode() { geoIPMatcher, err := geoip.getIPMatcher() // test load if err != nil { return nil, err } log.Infoln("Finished initial GeoIP rule %s => %s, records: %d", country, adapter, geoIPMatcher.Count()) } return geoip, nil } var _ C.Rule = (*GEOIP)(nil) ================================================ FILE: core/Clash.Meta/rules/common/geosite.go ================================================ package common import ( "fmt" "github.com/metacubex/mihomo/component/geodata" _ "github.com/metacubex/mihomo/component/geodata/memconservative" "github.com/metacubex/mihomo/component/geodata/router" _ "github.com/metacubex/mihomo/component/geodata/standard" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) type GEOSITE struct { Base country string adapter string recodeSize int } func (gs *GEOSITE) RuleType() C.RuleType { return C.GEOSITE } func (gs *GEOSITE) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { return gs.MatchDomain(metadata.RuleHost()), gs.adapter } // MatchDomain implements C.DomainMatcher func (gs *GEOSITE) MatchDomain(domain string) bool { if len(domain) == 0 { return false } matcher, err := gs.GetDomainMatcher() if err != nil { return false } return matcher.ApplyDomain(domain) } func (gs *GEOSITE) Adapter() string { return gs.adapter } func (gs *GEOSITE) Payload() string { return gs.country } func (gs *GEOSITE) GetDomainMatcher() (router.DomainMatcher, error) { matcher, err := geodata.LoadGeoSiteMatcher(gs.country) if err != nil { return nil, fmt.Errorf("load GeoSite data error, %w", err) } return matcher, nil } func (gs *GEOSITE) GetRecodeSize() int { if matcher, err := gs.GetDomainMatcher(); err == nil { return matcher.Count() } return 0 } func NewGEOSITE(country string, adapter string) (*GEOSITE, error) { if err := geodata.InitGeoSite(); err != nil { log.Errorln("can't initial GeoSite: %s", err) return nil, err } geoSite := &GEOSITE{ Base: Base{}, country: country, adapter: adapter, } matcher, err := geoSite.GetDomainMatcher() // test load if err != nil { return nil, err } log.Infoln("Finished initial GeoSite rule %s => %s, records: %d", country, adapter, matcher.Count()) return geoSite, nil } var _ C.Rule = (*GEOSITE)(nil) ================================================ FILE: core/Clash.Meta/rules/common/in_name.go ================================================ package common import ( "fmt" "strings" C "github.com/metacubex/mihomo/constant" ) type InName struct { Base names []string adapter string payload string } func (u *InName) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { for _, name := range u.names { if metadata.InName == name { return true, u.adapter } } return false, "" } func (u *InName) RuleType() C.RuleType { return C.InName } func (u *InName) Adapter() string { return u.adapter } func (u *InName) Payload() string { return u.payload } func NewInName(iNames, adapter string) (*InName, error) { names := strings.Split(iNames, "/") for i, name := range names { name = strings.TrimSpace(name) if len(name) == 0 { return nil, fmt.Errorf("in name couldn't be empty") } names[i] = name } return &InName{ Base: Base{}, names: names, adapter: adapter, payload: iNames, }, nil } var _ C.Rule = (*InName)(nil) ================================================ FILE: core/Clash.Meta/rules/common/in_type.go ================================================ package common import ( "fmt" "strings" C "github.com/metacubex/mihomo/constant" ) type InType struct { Base types []C.Type adapter string payload string } func (u *InType) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { for _, tp := range u.types { if metadata.Type == tp { return true, u.adapter } } return false, "" } func (u *InType) RuleType() C.RuleType { return C.InType } func (u *InType) Adapter() string { return u.adapter } func (u *InType) Payload() string { return u.payload } func NewInType(iTypes, adapter string) (*InType, error) { types := strings.Split(iTypes, "/") for i, tp := range types { tp = strings.TrimSpace(tp) if len(tp) == 0 { return nil, fmt.Errorf("in type couldn't be empty") } types[i] = tp } tps, err := parseInTypes(types) if err != nil { return nil, err } return &InType{ Base: Base{}, types: tps, adapter: adapter, payload: strings.ToUpper(iTypes), }, nil } func parseInTypes(tps []string) (res []C.Type, err error) { for _, tp := range tps { utp := strings.ToUpper(tp) var r *C.Type if utp == "SOCKS" { r, _ = C.ParseType("SOCKS4") res = append(res, *r) r, _ = C.ParseType("SOCKS5") res = append(res, *r) } else { r, err = C.ParseType(utp) if err != nil { return } res = append(res, *r) } } return } var _ C.Rule = (*InType)(nil) ================================================ FILE: core/Clash.Meta/rules/common/in_user.go ================================================ package common import ( "fmt" "strings" C "github.com/metacubex/mihomo/constant" ) type InUser struct { Base users []string adapter string payload string } func (u *InUser) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { for _, user := range u.users { if metadata.InUser == user { return true, u.adapter } } return false, "" } func (u *InUser) RuleType() C.RuleType { return C.InUser } func (u *InUser) Adapter() string { return u.adapter } func (u *InUser) Payload() string { return u.payload } func NewInUser(iUsers, adapter string) (*InUser, error) { users := strings.Split(iUsers, "/") for i, user := range users { user = strings.TrimSpace(user) if len(user) == 0 { return nil, fmt.Errorf("in user couldn't be empty") } users[i] = user } return &InUser{ Base: Base{}, users: users, adapter: adapter, payload: iUsers, }, nil } var _ C.Rule = (*InUser)(nil) ================================================ FILE: core/Clash.Meta/rules/common/ipasn.go ================================================ package common import ( "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/mmdb" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) type ASN struct { Base asn string adapter string noResolveIP bool isSourceIP bool } func (a *ASN) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if !a.noResolveIP && !a.isSourceIP && helper.ResolveIP != nil { helper.ResolveIP() } ip := metadata.DstIP if a.isSourceIP { ip = metadata.SrcIP } if !ip.IsValid() { return false, "" } asn, aso := mmdb.ASNInstance().LookupASN(ip.AsSlice()) if a.isSourceIP { metadata.SrcIPASN = asn + " " + aso } else { metadata.DstIPASN = asn + " " + aso } return a.asn == asn, a.adapter } func (a *ASN) RuleType() C.RuleType { if a.isSourceIP { return C.SrcIPASN } return C.IPASN } func (a *ASN) Adapter() string { return a.adapter } func (a *ASN) Payload() string { return a.asn } func (a *ASN) GetASN() string { return a.asn } func NewIPASN(asn string, adapter string, isSrc, noResolveIP bool) (*ASN, error) { if err := geodata.InitASN(); err != nil { log.Errorln("can't initial ASN: %s", err) return nil, err } return &ASN{ Base: Base{}, asn: asn, adapter: adapter, noResolveIP: noResolveIP, isSourceIP: isSrc, }, nil } var _ C.Rule = (*ASN)(nil) ================================================ FILE: core/Clash.Meta/rules/common/ipcidr.go ================================================ package common import ( "net/netip" C "github.com/metacubex/mihomo/constant" ) type IPCIDROption func(*IPCIDR) func WithIPCIDRSourceIP(b bool) IPCIDROption { return func(i *IPCIDR) { i.isSourceIP = b } } func WithIPCIDRNoResolve(noResolve bool) IPCIDROption { return func(i *IPCIDR) { i.noResolveIP = noResolve } } type IPCIDR struct { Base ipnet netip.Prefix adapter string isSourceIP bool noResolveIP bool } func (i *IPCIDR) RuleType() C.RuleType { if i.isSourceIP { return C.SrcIPCIDR } return C.IPCIDR } func (i *IPCIDR) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if !i.noResolveIP && !i.isSourceIP && helper.ResolveIP != nil { helper.ResolveIP() } ip := metadata.DstIP if i.isSourceIP { ip = metadata.SrcIP } return ip.IsValid() && i.ipnet.Contains(ip.WithZone("")), i.adapter } func (i *IPCIDR) Adapter() string { return i.adapter } func (i *IPCIDR) Payload() string { return i.ipnet.String() } func NewIPCIDR(s string, adapter string, opts ...IPCIDROption) (*IPCIDR, error) { ipnet, err := netip.ParsePrefix(s) if err != nil { return nil, errPayload } ipcidr := &IPCIDR{ Base: Base{}, ipnet: ipnet, adapter: adapter, } for _, o := range opts { o(ipcidr) } return ipcidr, nil } var _ C.Rule = (*IPCIDR)(nil) ================================================ FILE: core/Clash.Meta/rules/common/ipsuffix.go ================================================ package common import ( "net/netip" C "github.com/metacubex/mihomo/constant" ) type IPSuffix struct { Base ipBytes []byte bits int payload string adapter string isSourceIP bool noResolveIP bool } func (is *IPSuffix) RuleType() C.RuleType { if is.isSourceIP { return C.SrcIPSuffix } return C.IPSuffix } func (is *IPSuffix) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if !is.noResolveIP && !is.isSourceIP && helper.ResolveIP != nil { helper.ResolveIP() } ip := metadata.DstIP if is.isSourceIP { ip = metadata.SrcIP } mIPBytes := ip.AsSlice() if len(is.ipBytes) != len(mIPBytes) { return false, "" } size := len(mIPBytes) bits := is.bits for i := bits / 8; i > 0; i-- { if is.ipBytes[size-i] != mIPBytes[size-i] { return false, "" } } if (is.ipBytes[size-bits/8-1] << (8 - bits%8)) != (mIPBytes[size-bits/8-1] << (8 - bits%8)) { return false, "" } return true, is.adapter } func (is *IPSuffix) Adapter() string { return is.adapter } func (is *IPSuffix) Payload() string { return is.payload } func NewIPSuffix(payload, adapter string, isSrc, noResolveIP bool) (*IPSuffix, error) { ipnet, err := netip.ParsePrefix(payload) if err != nil { return nil, errPayload } return &IPSuffix{ Base: Base{}, payload: payload, ipBytes: ipnet.Addr().AsSlice(), bits: ipnet.Bits(), adapter: adapter, isSourceIP: isSrc, noResolveIP: noResolveIP, }, nil } var _ C.Rule = (*IPSuffix)(nil) ================================================ FILE: core/Clash.Meta/rules/common/network_type.go ================================================ package common import ( "fmt" "strings" C "github.com/metacubex/mihomo/constant" ) type NetworkType struct { Base network C.NetWork adapter string } func NewNetworkType(network, adapter string) (*NetworkType, error) { ntType := NetworkType{ Base: Base{}, } ntType.adapter = adapter switch strings.ToUpper(network) { case "TCP": ntType.network = C.TCP case "UDP": ntType.network = C.UDP default: return nil, fmt.Errorf("unsupported network type, only TCP/UDP") } return &ntType, nil } func (n *NetworkType) RuleType() C.RuleType { return C.Network } func (n *NetworkType) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { return n.network == metadata.NetWork, n.adapter } func (n *NetworkType) Adapter() string { return n.adapter } func (n *NetworkType) Payload() string { return n.network.String() } var _ C.Rule = (*NetworkType)(nil) ================================================ FILE: core/Clash.Meta/rules/common/port.go ================================================ package common import ( "fmt" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" ) type Port struct { Base adapter string port string ruleType C.RuleType portRanges utils.IntRanges[uint16] } func (p *Port) RuleType() C.RuleType { return p.ruleType } func (p *Port) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { targetPort := metadata.DstPort switch p.ruleType { case C.InPort: targetPort = metadata.InPort case C.SrcPort: targetPort = metadata.SrcPort } return p.portRanges.Check(targetPort), p.adapter } func (p *Port) Adapter() string { return p.adapter } func (p *Port) Payload() string { return p.port } func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) { portRanges, err := utils.NewUnsignedRanges[uint16](port) if err != nil { return nil, fmt.Errorf("%w, %w", errPayload, err) } if len(portRanges) == 0 { return nil, errPayload } return &Port{ Base: Base{}, adapter: adapter, port: port, ruleType: ruleType, portRanges: portRanges, }, nil } var _ C.Rule = (*Port)(nil) ================================================ FILE: core/Clash.Meta/rules/common/process.go ================================================ package common import ( "strings" "github.com/metacubex/mihomo/component/wildcard" C "github.com/metacubex/mihomo/constant" "github.com/dlclark/regexp2" ) type Process struct { Base pattern string adapter string ruleType C.RuleType regexp *regexp2.Regexp } func (ps *Process) Payload() string { return ps.pattern } func (ps *Process) Adapter() string { return ps.adapter } func (ps *Process) RuleType() C.RuleType { return ps.ruleType } func (ps *Process) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if helper.FindProcess != nil { helper.FindProcess() } var target string switch ps.ruleType { case C.ProcessName, C.ProcessNameRegex, C.ProcessNameWildcard: target = metadata.Process default: target = metadata.ProcessPath } switch ps.ruleType { case C.ProcessNameRegex, C.ProcessPathRegex: match, _ := ps.regexp.MatchString(target) return match, ps.adapter case C.ProcessNameWildcard, C.ProcessPathWildcard: return wildcard.Match(strings.ToLower(ps.pattern), strings.ToLower(target)), ps.adapter default: return strings.EqualFold(target, ps.pattern), ps.adapter } } func NewProcess(pattern string, adapter string, ruleType C.RuleType) (*Process, error) { ps := &Process{ Base: Base{}, pattern: pattern, adapter: adapter, ruleType: ruleType, } switch ps.ruleType { case C.ProcessNameRegex, C.ProcessPathRegex: r, err := regexp2.Compile(pattern, regexp2.IgnoreCase) if err != nil { return nil, err } ps.regexp = r default: } return ps, nil } var _ C.Rule = (*Process)(nil) ================================================ FILE: core/Clash.Meta/rules/common/uid.go ================================================ package common import ( "fmt" "runtime" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) type Uid struct { Base uids utils.IntRanges[uint32] oUid string adapter string } func NewUid(oUid, adapter string) (*Uid, error) { if !(runtime.GOOS == "linux" || runtime.GOOS == "android") { return nil, fmt.Errorf("uid rule not support this platform") } uidRange, err := utils.NewUnsignedRanges[uint32](oUid) if err != nil { return nil, fmt.Errorf("%w, %w", errPayload, err) } if len(uidRange) == 0 { return nil, errPayload } return &Uid{ Base: Base{}, adapter: adapter, oUid: oUid, uids: uidRange, }, nil } func (u *Uid) RuleType() C.RuleType { return C.Uid } func (u *Uid) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if helper.FindProcess != nil { helper.FindProcess() } if metadata.Uid != 0 { if u.uids.Check(metadata.Uid) { return true, u.adapter } } log.Warnln("[UID] could not get uid from %s", metadata.String()) return false, "" } func (u *Uid) Adapter() string { return u.adapter } func (u *Uid) Payload() string { return u.oUid } var _ C.Rule = (*Uid)(nil) ================================================ FILE: core/Clash.Meta/rules/logic/logic.go ================================================ package logic import ( "fmt" "sort" "strings" "sync" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/rules/common" ) type Logic struct { common.Base payload string adapter string ruleType C.RuleType rules []C.Rule subRules map[string][]C.Rule payloadOnce sync.Once } func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule common.ParseRuleFunc) (*Logic, error) { logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.SubRules, subRules: subRules} err := logic.parsePayload(fmt.Sprintf("(%s)", payload), parseRule) if err != nil { return nil, err } if len(logic.rules) != 1 { return nil, fmt.Errorf("Sub-Rule rule must contain one rule") } return logic, nil } func NewNOT(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.NOT} err := logic.parsePayload(payload, parseRule) if err != nil { return nil, err } if len(logic.rules) != 1 { return nil, fmt.Errorf("not rule must contain one rule") } return logic, nil } func NewOR(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.OR} err := logic.parsePayload(payload, parseRule) if err != nil { return nil, err } return logic, nil } func NewAND(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.AND} err := logic.parsePayload(payload, parseRule) if err != nil { return nil, err } return logic, nil } type Range struct { start int end int } func (r Range) containRange(preStart, preEnd int) bool { return preStart < r.start && preEnd > r.end } func (logic *Logic) payloadToRule(subPayload string, parseRule common.ParseRuleFunc) (C.Rule, error) { tp, payload, target, param := common.ParseRulePayload(subPayload, false) switch tp { case "MATCH", "SUB-RULE": return nil, fmt.Errorf("unsupported rule type [%s] on logic rule", tp) case "": return nil, fmt.Errorf("[%s] format is error", subPayload) } return parseRule(tp, payload, target, param, nil) } func (logic *Logic) format(payload string) ([]Range, error) { stack := make([]int, 0) subRanges := make([]Range, 0) for i, c := range payload { if c == '(' { stack = append(stack, i) // push } else if c == ')' { if len(stack) == 0 { return nil, fmt.Errorf("missing '('") } back := len(stack) - 1 start := stack[back] // back stack = stack[:back] // pop subRanges = append(subRanges, Range{ start: start, end: i, }) } } if len(stack) != 0 { return nil, fmt.Errorf("format error is missing )") } sort.Slice(subRanges, func(i, j int) bool { return subRanges[i].start < subRanges[j].start }) return subRanges, nil } func (logic *Logic) findSubRuleRange(payload string, ruleRanges []Range) []Range { payloadLen := len(payload) subRuleRange := make([]Range, 0) for _, rr := range ruleRanges { if rr.start == 0 && rr.end == payloadLen-1 { // 最大范围跳过 continue } containInSub := false for _, r := range subRuleRange { if rr.containRange(r.start, r.end) { // The subRuleRange contains a range of rr, which is the next level node of the tree containInSub = true break } } if !containInSub { subRuleRange = append(subRuleRange, rr) } } return subRuleRange } func (logic *Logic) parsePayload(payload string, parseRule common.ParseRuleFunc) error { if !strings.HasPrefix(payload, "(") || !strings.HasSuffix(payload, ")") { // the payload must be "(xxx)" format return fmt.Errorf("payload format error") } subAllRanges, err := logic.format(payload) if err != nil { return err } rules := make([]C.Rule, 0, len(subAllRanges)) subRanges := logic.findSubRuleRange(payload, subAllRanges) for _, subRange := range subRanges { subPayload := payload[subRange.start+1 : subRange.end] rule, err := logic.payloadToRule(subPayload, parseRule) if err != nil { return err } rules = append(rules, rule) } logic.rules = rules return nil } func (logic *Logic) RuleType() C.RuleType { return logic.ruleType } func matchSubRules(metadata *C.Metadata, name string, subRules map[string][]C.Rule, helper C.RuleMatchHelper) (bool, string) { for _, rule := range subRules[name] { if m, a := rule.Match(metadata, helper); m { if rule.RuleType() == C.SubRules { return matchSubRules(metadata, rule.Adapter(), subRules, helper) } else { return m, a } } } return false, "" } func (logic *Logic) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { switch logic.ruleType { case C.SubRules: if m, _ := logic.rules[0].Match(metadata, helper); m { return matchSubRules(metadata, logic.adapter, logic.subRules, helper) } return false, "" case C.NOT: if m, _ := logic.rules[0].Match(metadata, helper); !m { return true, logic.adapter } return false, "" case C.OR: for _, rule := range logic.rules { if m, _ := rule.Match(metadata, helper); m { return true, logic.adapter } } return false, "" case C.AND: for _, rule := range logic.rules { if m, _ := rule.Match(metadata, helper); !m { return false, logic.adapter } } return true, logic.adapter default: return false, "" } } func (logic *Logic) Adapter() string { return logic.adapter } func (logic *Logic) Payload() string { logic.payloadOnce.Do(func() { // a little bit expensive, so only computed once switch logic.ruleType { case C.NOT: logic.payload = fmt.Sprintf("(!(%s,%s))", logic.rules[0].RuleType(), logic.rules[0].Payload()) case C.OR: payloads := make([]string, 0, len(logic.rules)) for _, rule := range logic.rules { payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) } logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " || ")) case C.AND: payloads := make([]string, 0, len(logic.rules)) for _, rule := range logic.rules { payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) } logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " && ")) default: } }) return logic.payload } func (logic *Logic) ProviderNames() (names []string) { for _, rule := range logic.rules { names = append(names, rule.ProviderNames()...) } return } var _ C.Rule = (*Logic)(nil) ================================================ FILE: core/Clash.Meta/rules/logic_test/logic_test.go ================================================ package logic_test import ( "testing" // https://github.com/golang/go/wiki/CodeReviewComments#import-dot . "github.com/metacubex/mihomo/rules/logic" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/rules" "github.com/stretchr/testify/assert" ) var ParseRule = rules.ParseRule func TestAND(t *testing.T) { and, err := NewAND("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) assert.Equal(t, nil, err) assert.Equal(t, "DIRECT", and.Adapter()) m, _ := and.Match(&C.Metadata{ Host: "baidu.com", NetWork: C.TCP, DstPort: 20000, }, C.RuleMatchHelper{}) assert.Equal(t, true, m) and, err = NewAND("(DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) assert.NotEqual(t, nil, err) and, err = NewAND("((AND,(DOMAIN,baidu.com),(NETWORK,TCP)),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) assert.Equal(t, nil, err) } func TestNOT(t *testing.T) { not, err := NewNOT("((DST-PORT,6000-6500))", "REJECT", ParseRule) assert.Equal(t, nil, err) m, _ := not.Match(&C.Metadata{ DstPort: 6100, }, C.RuleMatchHelper{}) assert.Equal(t, false, m) _, err = NewNOT("(DST-PORT,5600-6666)", "DIRECT", ParseRule) assert.NotEqual(t, nil, err) _, err = NewNOT("DST-PORT,5600-6666", "DIRECT", ParseRule) assert.NotEqual(t, nil, err) _, err = NewNOT("((DST-PORT,5600-6666),(DOMAIN,baidu.com))", "DIRECT", ParseRule) assert.NotEqual(t, nil, err) _, err = NewNOT("(())", "DIRECT", ParseRule) assert.NotEqual(t, nil, err) } func TestOR(t *testing.T) { or, err := NewOR("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) assert.Equal(t, nil, err) m, _ := or.Match(&C.Metadata{ NetWork: C.TCP, }, C.RuleMatchHelper{}) assert.Equal(t, true, m) } ================================================ FILE: core/Clash.Meta/rules/parser.go ================================================ package rules import ( "fmt" C "github.com/metacubex/mihomo/constant" RC "github.com/metacubex/mihomo/rules/common" "github.com/metacubex/mihomo/rules/logic" RP "github.com/metacubex/mihomo/rules/provider" ) func ParseRule(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error) { if tp != "MATCH" && payload == "" { // only MATCH allowed doesn't contain payload return nil, fmt.Errorf("missing subsequent parameters: %s", tp) } switch tp { case "DOMAIN": parsed = RC.NewDomain(payload, target) case "DOMAIN-SUFFIX": parsed = RC.NewDomainSuffix(payload, target) case "DOMAIN-KEYWORD": parsed = RC.NewDomainKeyword(payload, target) case "DOMAIN-REGEX": parsed, parseErr = RC.NewDomainRegex(payload, target) case "DOMAIN-WILDCARD": parsed, parseErr = RC.NewDomainWildcard(payload, target) case "GEOSITE": parsed, parseErr = RC.NewGEOSITE(payload, target) case "GEOIP": isSrc, noResolve := RC.ParseParams(params) parsed, parseErr = RC.NewGEOIP(payload, target, isSrc, noResolve) case "SRC-GEOIP": parsed, parseErr = RC.NewGEOIP(payload, target, true, true) case "IP-ASN": isSrc, noResolve := RC.ParseParams(params) parsed, parseErr = RC.NewIPASN(payload, target, isSrc, noResolve) case "SRC-IP-ASN": parsed, parseErr = RC.NewIPASN(payload, target, true, true) case "IP-CIDR", "IP-CIDR6": isSrc, noResolve := RC.ParseParams(params) parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(isSrc), RC.WithIPCIDRNoResolve(noResolve)) case "SRC-IP-CIDR": parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) case "IP-SUFFIX": isSrc, noResolve := RC.ParseParams(params) parsed, parseErr = RC.NewIPSuffix(payload, target, isSrc, noResolve) case "SRC-IP-SUFFIX": parsed, parseErr = RC.NewIPSuffix(payload, target, true, true) case "SRC-PORT": parsed, parseErr = RC.NewPort(payload, target, C.SrcPort) case "DST-PORT": parsed, parseErr = RC.NewPort(payload, target, C.DstPort) case "IN-PORT": parsed, parseErr = RC.NewPort(payload, target, C.InPort) case "DSCP": parsed, parseErr = RC.NewDSCP(payload, target) case "PROCESS-NAME": parsed, parseErr = RC.NewProcess(payload, target, C.ProcessName) case "PROCESS-PATH": parsed, parseErr = RC.NewProcess(payload, target, C.ProcessPath) case "PROCESS-NAME-REGEX": parsed, parseErr = RC.NewProcess(payload, target, C.ProcessNameRegex) case "PROCESS-PATH-REGEX": parsed, parseErr = RC.NewProcess(payload, target, C.ProcessPathRegex) case "PROCESS-NAME-WILDCARD": parsed, parseErr = RC.NewProcess(payload, target, C.ProcessNameWildcard) case "PROCESS-PATH-WILDCARD": parsed, parseErr = RC.NewProcess(payload, target, C.ProcessPathWildcard) case "NETWORK": parsed, parseErr = RC.NewNetworkType(payload, target) case "UID": parsed, parseErr = RC.NewUid(payload, target) case "IN-TYPE": parsed, parseErr = RC.NewInType(payload, target) case "IN-USER": parsed, parseErr = RC.NewInUser(payload, target) case "IN-NAME": parsed, parseErr = RC.NewInName(payload, target) case "SUB-RULE": parsed, parseErr = logic.NewSubRule(payload, target, subRules, ParseRule) case "AND": parsed, parseErr = logic.NewAND(payload, target, ParseRule) case "OR": parsed, parseErr = logic.NewOR(payload, target, ParseRule) case "NOT": parsed, parseErr = logic.NewNOT(payload, target, ParseRule) case "RULE-SET": isSrc, noResolve := RC.ParseParams(params) parsed, parseErr = RP.NewRuleSet(payload, target, isSrc, noResolve) case "MATCH": parsed = RC.NewMatch(target) parseErr = nil default: parseErr = fmt.Errorf("unsupported rule type: %s", tp) } if parseErr != nil { return nil, parseErr } return } var _ RC.ParseRuleFunc = ParseRule ================================================ FILE: core/Clash.Meta/rules/provider/classical_strategy.go ================================================ package provider import ( "fmt" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/rules/common" ) type classicalStrategy struct { rules []C.Rule count int parse common.ParseRuleFunc } func (c *classicalStrategy) Behavior() P.RuleBehavior { return P.Classical } func (c *classicalStrategy) Match(metadata *C.Metadata, helper C.RuleMatchHelper) bool { for _, rule := range c.rules { if m, _ := rule.Match(metadata, helper); m { return true } } return false } func (c *classicalStrategy) Count() int { return c.count } func (c *classicalStrategy) Reset() { c.rules = nil c.count = 0 } func (c *classicalStrategy) Insert(rule string) { r, err := c.payloadToRule(rule) if err != nil { log.Warnln("parse classical rule [%s] error: %s", rule, err.Error()) } else { c.rules = append(c.rules, r) c.count++ } } func (c *classicalStrategy) payloadToRule(rule string) (C.Rule, error) { tp, payload, target, params := common.ParseRulePayload(rule, false) switch tp { case "MATCH", "RULE-SET", "SUB-RULE": return nil, fmt.Errorf("unsupported rule type on classical rule-set: %s", tp) } return c.parse(tp, payload, target, params, nil) } func (c *classicalStrategy) FinishInsert() {} func NewClassicalStrategy(parse common.ParseRuleFunc) *classicalStrategy { return &classicalStrategy{rules: []C.Rule{}, parse: parse} } ================================================ FILE: core/Clash.Meta/rules/provider/domain_strategy.go ================================================ package provider import ( "errors" "io" "strings" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" "golang.org/x/exp/slices" ) type domainStrategy struct { count int domainTrie *trie.DomainTrie[struct{}] domainSet *trie.DomainSet } func (d *domainStrategy) Behavior() P.RuleBehavior { return P.Domain } func (d *domainStrategy) Match(metadata *C.Metadata, helper C.RuleMatchHelper) bool { return d.domainSet != nil && d.domainSet.Has(metadata.RuleHost()) } func (d *domainStrategy) Count() int { return d.count } func (d *domainStrategy) Reset() { d.domainTrie = trie.New[struct{}]() d.domainSet = nil d.count = 0 } func (d *domainStrategy) Insert(rule string) { if strings.ContainsRune(rule, '/') { log.Warnln("invalid domain:[%s]", rule) return } err := d.domainTrie.Insert(rule, struct{}{}) if err != nil { log.Warnln("invalid domain:[%s]", rule) } else { d.count++ } } func (d *domainStrategy) FinishInsert() { d.domainSet = d.domainTrie.NewDomainSet() d.domainTrie = nil } func (d *domainStrategy) FromMrs(r io.Reader, count int) error { domainSet, err := trie.ReadDomainSetBin(r) if err != nil { return err } d.count = count d.domainSet = domainSet return nil } func (d *domainStrategy) WriteMrs(w io.Writer) error { if d.domainSet == nil { return errors.New("nil domainSet") } return d.domainSet.WriteBin(w) } func (d *domainStrategy) DumpMrs(f func(key string) bool) { if d.domainSet != nil { var keys []string d.domainSet.Foreach(func(key string) bool { keys = append(keys, key) return true }) slices.Sort(keys) for _, key := range keys { if _, ok := slices.BinarySearch(keys, "+."+key); ok { continue // ignore the rules added by trie internal processing } if !f(key) { return } } } } var _ mrsRuleStrategy = (*domainStrategy)(nil) func NewDomainStrategy() *domainStrategy { return &domainStrategy{} } ================================================ FILE: core/Clash.Meta/rules/provider/ipcidr_strategy.go ================================================ package provider import ( "errors" "io" "net/netip" "github.com/metacubex/mihomo/component/cidr" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" "go4.org/netipx" ) type ipcidrStrategy struct { count int cidrSet *cidr.IpCidrSet //trie *trie.IpCidrTrie } func (i *ipcidrStrategy) Behavior() P.RuleBehavior { return P.IPCIDR } func (i *ipcidrStrategy) Match(metadata *C.Metadata, helper C.RuleMatchHelper) bool { if helper.ResolveIP != nil { helper.ResolveIP() } // return i.trie != nil && i.trie.IsContain(metadata.DstIP.AsSlice()) return i.cidrSet != nil && i.cidrSet.IsContain(metadata.DstIP) } func (i *ipcidrStrategy) Count() int { return i.count } func (i *ipcidrStrategy) Reset() { // i.trie = trie.NewIpCidrTrie() i.cidrSet = cidr.NewIpCidrSet() i.count = 0 } func (i *ipcidrStrategy) Insert(rule string) { //err := i.trie.AddIpCidrForString(rule) err := i.cidrSet.AddIpCidrForString(rule) if err != nil { log.Warnln("invalid Ipcidr:[%s]", rule) } else { i.count++ } } func (i *ipcidrStrategy) FinishInsert() { i.cidrSet.Merge() } func (i *ipcidrStrategy) FromMrs(r io.Reader, count int) error { cidrSet, err := cidr.ReadIpCidrSet(r) if err != nil { return err } i.count = count i.cidrSet = cidrSet return nil } func (i *ipcidrStrategy) WriteMrs(w io.Writer) error { if i.cidrSet == nil { return errors.New("nil cidrSet") } return i.cidrSet.WriteBin(w) } func (i *ipcidrStrategy) DumpMrs(f func(key string) bool) { if i.cidrSet != nil { i.cidrSet.Foreach(func(prefix netip.Prefix) bool { return f(prefix.String()) }) } } func (i *ipcidrStrategy) ToIpCidr() *netipx.IPSet { return i.cidrSet.ToIPSet() } func NewIPCidrStrategy() *ipcidrStrategy { return &ipcidrStrategy{} } ================================================ FILE: core/Clash.Meta/rules/provider/mrs_converter.go ================================================ package provider import ( "encoding/binary" "errors" "fmt" "io" "os" P "github.com/metacubex/mihomo/constant/provider" "github.com/klauspost/compress/zstd" ) func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io.Writer) (err error) { strategy := newStrategy(behavior, nil) strategy, err = rulesParse(buf, strategy, format) if err != nil { return err } if strategy.Count() == 0 { return errors.New("empty rule") } if _strategy, ok := strategy.(mrsRuleStrategy); ok { if format == P.MrsRule { // export to TextRule _strategy.DumpMrs(func(key string) bool { _, err = fmt.Fprintln(w, key) if err != nil { return false } return true }) return nil } var encoder *zstd.Encoder encoder, err = zstd.NewWriter(w) if err != nil { return err } defer func() { zstdErr := encoder.Close() if err == nil { err = zstdErr } }() // header _, err = encoder.Write(MrsMagicBytes[:]) if err != nil { return err } // behavior _behavior := []byte{behavior.Byte()} _, err = encoder.Write(_behavior[:]) if err != nil { return err } // count count := int64(_strategy.Count()) err = binary.Write(encoder, binary.BigEndian, count) if err != nil { return err } // extra (reserved for future using) var extra []byte err = binary.Write(encoder, binary.BigEndian, int64(len(extra))) if err != nil { return err } _, err = encoder.Write(extra) if err != nil { return err } return _strategy.WriteMrs(encoder) } else { return ErrInvalidFormat } } func ConvertMain(args []string) { if len(args) > 3 { behavior, err := P.ParseBehavior(args[0]) if err != nil { panic(err) } format, err := P.ParseRuleFormat(args[1]) if err != nil { panic(err) } source := args[2] target := args[3] sourceFile, err := os.ReadFile(source) if err != nil { panic(err) } targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { panic(err) } err = ConvertToMrs(sourceFile, behavior, format, targetFile) if err != nil { panic(err) } err = targetFile.Close() if err != nil { panic(err) } } else { panic("Usage: convert-ruleset ") } } ================================================ FILE: core/Clash.Meta/rules/provider/mrs_reader.go ================================================ package provider import ( "bytes" "encoding/binary" "errors" "fmt" "io" "github.com/klauspost/compress/zstd" ) var MrsMagicBytes = [4]byte{'M', 'R', 'S', 1} // MRSv1 func rulesMrsParse(buf []byte, strategy ruleStrategy) (ruleStrategy, error) { if _strategy, ok := strategy.(mrsRuleStrategy); ok { reader, err := zstd.NewReader(bytes.NewReader(buf)) if err != nil { return nil, err } defer reader.Close() // header var header [4]byte _, err = io.ReadFull(reader, header[:]) if err != nil { return nil, err } if header != MrsMagicBytes { return nil, fmt.Errorf("invalid MrsMagic bytes") } // behavior var _behavior [1]byte _, err = io.ReadFull(reader, _behavior[:]) if err != nil { return nil, err } if _behavior[0] != strategy.Behavior().Byte() { return nil, fmt.Errorf("invalid behavior") } // count var count int64 err = binary.Read(reader, binary.BigEndian, &count) if err != nil { return nil, err } // extra (reserved for future using) var length int64 err = binary.Read(reader, binary.BigEndian, &length) if err != nil { return nil, err } if length < 0 { return nil, errors.New("length is invalid") } if length > 0 { extra := make([]byte, length) _, err = io.ReadFull(reader, extra) if err != nil { return nil, err } } err = _strategy.FromMrs(reader, int(count)) return strategy, err } else { return nil, ErrInvalidFormat } } ================================================ FILE: core/Clash.Meta/rules/provider/parse.go ================================================ package provider import ( "fmt" "time" "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/rules/common" ) type ruleProviderSchema struct { Type string `provider:"type"` Behavior string `provider:"behavior"` Path string `provider:"path,omitempty"` URL string `provider:"url,omitempty"` Proxy string `provider:"proxy,omitempty"` Format string `provider:"format,omitempty"` Interval int `provider:"interval,omitempty"` SizeLimit int64 `provider:"size-limit,omitempty"` Payload []string `provider:"payload,omitempty"` Header map[string][]string `provider:"header,omitempty"` } func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRuleFunc) (P.RuleProvider, error) { schema := &ruleProviderSchema{} decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) if err := decoder.Decode(mapping, schema); err != nil { return nil, err } behavior, err := P.ParseBehavior(schema.Behavior) if err != nil { return nil, err } format, err := P.ParseRuleFormat(schema.Format) if err != nil { return nil, err } var vehicle P.Vehicle switch schema.Type { case "file": path := C.Path.Resolve(schema.Path) if !C.Path.IsSafePath(path) { return nil, C.Path.ErrNotSafePath(path) } vehicle = resource.NewFileVehicle(path) case "http": path := C.Path.GetPathByHash("rules", schema.URL) if schema.Path != "" { path = C.Path.Resolve(schema.Path) if !C.Path.IsSafePath(path) { return nil, C.Path.ErrNotSafePath(path) } } vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit) case "inline": return NewInlineProvider(name, behavior, schema.Payload, parse), nil default: return nil, fmt.Errorf("unsupported vehicle type: %s", schema.Type) } interval := time.Duration(uint(schema.Interval)) * time.Second return NewRuleSetProvider(name, behavior, format, interval, vehicle, schema.Payload, parse), nil } ================================================ FILE: core/Clash.Meta/rules/provider/patch_android.go ================================================ //go:build android package provider import "time" var ( suspended bool ) type UpdatableProvider interface { UpdatedAt() time.Time } func Suspend(s bool) { suspended = s } ================================================ FILE: core/Clash.Meta/rules/provider/provider.go ================================================ package provider import ( "bytes" "encoding/json" "errors" "io" "runtime" "strings" "time" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/yaml" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/rules/common" ) var tunnel P.Tunnel func SetTunnel(t P.Tunnel) { tunnel = t } type RulePayload struct { /** key: Domain or IP Cidr value: Rule type or is empty */ Payload []string `yaml:"payload"` Rules []string `yaml:"rules"` } type providerForApi struct { Behavior string `json:"behavior"` Format string `json:"format"` Name string `json:"name"` RuleCount int `json:"ruleCount"` Type string `json:"type"` VehicleType string `json:"vehicleType"` UpdatedAt time.Time `json:"updatedAt"` Payload []string `json:"payload,omitempty"` } type ruleStrategy interface { Behavior() P.RuleBehavior Match(metadata *C.Metadata, helper C.RuleMatchHelper) bool Count() int Reset() Insert(rule string) FinishInsert() } type mrsRuleStrategy interface { ruleStrategy FromMrs(r io.Reader, count int) error WriteMrs(w io.Writer) error DumpMrs(f func(key string) bool) } type baseProvider struct { behavior P.RuleBehavior strategy ruleStrategy } func (bp *baseProvider) Type() P.ProviderType { return P.Rule } func (bp *baseProvider) Behavior() P.RuleBehavior { return bp.behavior } func (bp *baseProvider) Count() int { return bp.strategy.Count() } func (bp *baseProvider) Match(metadata *C.Metadata, helper C.RuleMatchHelper) bool { return bp.strategy != nil && bp.strategy.Match(metadata, helper) } func (bp *baseProvider) Strategy() any { return bp.strategy } type ruleSetProvider struct { baseProvider *resource.Fetcher[ruleStrategy] format P.RuleFormat } type RuleSetProvider struct { *ruleSetProvider } func (rp *ruleSetProvider) Initial() error { _, err := rp.Fetcher.Initial() return err } func (rp *ruleSetProvider) Update() error { _, _, err := rp.Fetcher.Update() return err } func (rp *ruleSetProvider) MarshalJSON() ([]byte, error) { return json.Marshal( providerForApi{ Behavior: rp.behavior.String(), Format: rp.format.String(), Name: rp.Fetcher.Name(), RuleCount: rp.strategy.Count(), Type: rp.Type().String(), UpdatedAt: rp.UpdatedAt(), VehicleType: rp.VehicleType().String(), }) } func (rp *RuleSetProvider) Close() error { runtime.SetFinalizer(rp, nil) return rp.ruleSetProvider.Close() } func NewRuleSetProvider(name string, behavior P.RuleBehavior, format P.RuleFormat, interval time.Duration, vehicle P.Vehicle, payload []string, parse common.ParseRuleFunc) P.RuleProvider { rp := &ruleSetProvider{ baseProvider: baseProvider{ behavior: behavior, }, format: format, } onUpdate := func(strategy ruleStrategy) { rp.strategy = strategy tunnel.RuleUpdateCallback().Emit(rp) } rp.strategy = newStrategy(behavior, parse) if len(payload) > 0 { // using as fallback rules rp.strategy = rulesParseInline(payload, rp.strategy) } rp.Fetcher = resource.NewFetcher(name, interval, vehicle, func(bytes []byte) (ruleStrategy, error) { return rulesParse(bytes, newStrategy(behavior, parse), format) }, onUpdate) wrapper := &RuleSetProvider{ rp, } runtime.SetFinalizer(wrapper, (*RuleSetProvider).Close) return wrapper } func newStrategy(behavior P.RuleBehavior, parse common.ParseRuleFunc) ruleStrategy { switch behavior { case P.Domain: strategy := NewDomainStrategy() return strategy case P.IPCIDR: strategy := NewIPCidrStrategy() return strategy case P.Classical: strategy := NewClassicalStrategy(parse) return strategy default: return nil } } var ( ErrNoPayload = errors.New("file must have a `payload` field") ErrInvalidFormat = errors.New("invalid format") ) func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStrategy, error) { strategy.Reset() if format == P.MrsRule { return rulesMrsParse(buf, strategy) } schema := &RulePayload{} firstLineBuffer := pool.GetBuffer() defer pool.PutBuffer(firstLineBuffer) firstLineLength := 0 s := 0 // search start index for s < len(buf) { // search buffer for a new line. line := buf[s:] if i := bytes.IndexByte(line, '\n'); i >= 0 { i += s line = buf[s : i+1] s = i + 1 } else { s = len(buf) // stop loop in next step if firstLineLength == 0 && format == P.YamlRule { // no head or only one line body return nil, ErrNoPayload } } var str string switch format { case P.TextRule: str = string(line) str = strings.TrimSpace(str) if len(str) == 0 { continue } if str[0] == '#' { // comment continue } if strings.HasPrefix(str, "//") { // comment in Premium core continue } case P.YamlRule: trimLine := bytes.TrimSpace(line) if len(trimLine) == 0 { continue } if trimLine[0] == '#' { // comment continue } firstLineBuffer.Write(line) if firstLineLength == 0 { // find payload head firstLineLength = firstLineBuffer.Len() firstLineBuffer.WriteString(" - ''") // a test line err := yaml.Unmarshal(firstLineBuffer.Bytes(), schema) firstLineBuffer.Truncate(firstLineLength) if err == nil && (len(schema.Rules) > 0 || len(schema.Payload) > 0) { // found continue } // not found or err!=nil firstLineBuffer.Truncate(0) firstLineLength = 0 continue } // parse payload body err := yaml.Unmarshal(firstLineBuffer.Bytes(), schema) firstLineBuffer.Truncate(firstLineLength) if err != nil { continue } if len(schema.Rules) > 0 { str = schema.Rules[0] } if len(schema.Payload) > 0 { str = schema.Payload[0] } default: return nil, ErrInvalidFormat } if str == "" { continue } strategy.Insert(str) } strategy.FinishInsert() return strategy, nil } func rulesParseInline(rs []string, strategy ruleStrategy) ruleStrategy { strategy.Reset() for _, r := range rs { if r != "" { strategy.Insert(r) } } strategy.FinishInsert() return strategy } type InlineProvider struct { *inlineProvider } type inlineProvider struct { baseProvider name string updateAt time.Time payload []string } func (i *inlineProvider) Name() string { return i.name } func (i *inlineProvider) Initial() error { return nil } func (i *inlineProvider) Update() error { // make api update happy i.updateAt = time.Now() return nil } func (i *inlineProvider) VehicleType() P.VehicleType { return P.Inline } func (i *inlineProvider) MarshalJSON() ([]byte, error) { return json.Marshal( providerForApi{ Behavior: i.behavior.String(), Name: i.Name(), RuleCount: i.strategy.Count(), Type: i.Type().String(), VehicleType: i.VehicleType().String(), UpdatedAt: i.updateAt, Payload: i.payload, }) } func NewInlineProvider(name string, behavior P.RuleBehavior, payload []string, parse common.ParseRuleFunc) P.RuleProvider { ip := &inlineProvider{ baseProvider: baseProvider{ behavior: behavior, strategy: newStrategy(behavior, parse), }, payload: payload, name: name, updateAt: time.Now(), } ip.strategy = rulesParseInline(payload, ip.strategy) wrapper := &InlineProvider{ ip, } //runtime.SetFinalizer(wrapper, (*InlineProvider).Close) return wrapper } ================================================ FILE: core/Clash.Meta/rules/provider/rule_set.go ================================================ package provider import ( "net/netip" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/rules/common" ) type RuleSet struct { common.Base ruleProviderName string adapter string isSrc bool noResolveIP bool } func (rs *RuleSet) RuleType() C.RuleType { return C.RuleSet } func (rs *RuleSet) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if provider, ok := rs.getProvider(); ok { if rs.isSrc { metadata.SwapSrcDst() defer metadata.SwapSrcDst() helper.ResolveIP = nil // src mode should not resolve ip } else if rs.noResolveIP { helper.ResolveIP = nil } return provider.Match(metadata, helper), rs.adapter } return false, "" } // MatchDomain implements C.DomainMatcher func (rs *RuleSet) MatchDomain(domain string) bool { ok, _ := rs.Match(&C.Metadata{Host: domain}, C.RuleMatchHelper{}) return ok } // MatchIp implements C.IpMatcher func (rs *RuleSet) MatchIp(ip netip.Addr) bool { ok, _ := rs.Match(&C.Metadata{DstIP: ip}, C.RuleMatchHelper{}) return ok } func (rs *RuleSet) Adapter() string { return rs.adapter } func (rs *RuleSet) Payload() string { return rs.ruleProviderName } func (rs *RuleSet) ProviderNames() []string { return []string{rs.ruleProviderName} } func (rs *RuleSet) getProvider() (P.RuleProvider, bool) { pp, ok := tunnel.RuleProviders()[rs.ruleProviderName] return pp, ok } func NewRuleSet(ruleProviderName string, adapter string, isSrc bool, noResolveIP bool) (*RuleSet, error) { rs := &RuleSet{ Base: common.Base{}, ruleProviderName: ruleProviderName, adapter: adapter, isSrc: isSrc, noResolveIP: noResolveIP, } return rs, nil } var _ C.Rule = (*RuleSet)(nil) ================================================ FILE: core/Clash.Meta/rules/wrapper/wrapper.go ================================================ package wrapper import ( "sync/atomic" "time" C "github.com/metacubex/mihomo/constant" ) type RuleWrapper struct { C.Rule disabled atomic.Bool hitCount atomic.Uint64 hitAt atomicTime missCount atomic.Uint64 missAt atomicTime } func (r *RuleWrapper) IsDisabled() bool { return r.disabled.Load() } func (r *RuleWrapper) SetDisabled(v bool) { r.disabled.Store(v) } func (r *RuleWrapper) HitCount() uint64 { return r.hitCount.Load() } func (r *RuleWrapper) HitAt() time.Time { return r.hitAt.Load() } func (r *RuleWrapper) MissCount() uint64 { return r.missCount.Load() } func (r *RuleWrapper) MissAt() time.Time { return r.missAt.Load() } func (r *RuleWrapper) Unwrap() C.Rule { return r.Rule } func (r *RuleWrapper) Hit() { r.hitCount.Add(1) r.hitAt.Store(time.Now()) } func (r *RuleWrapper) Miss() { r.missCount.Add(1) r.missAt.Store(time.Now()) } func (r *RuleWrapper) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) { if r.IsDisabled() { return false, "" } ok, adapter := r.Rule.Match(metadata, helper) if ok { r.Hit() } else { r.Miss() } return ok, adapter } func NewRuleWrapper(rule C.Rule) C.RuleWrapper { return &RuleWrapper{Rule: rule} } // atomicTime is a wrapper of [atomic.Int64] to provide atomic time storage. // it only saves unix nanosecond export from time.Time. // unlike atomic.TypedValue[time.Time] always escapes a new time.Time to heap when storing. // that will lead to higher GC pressure during high frequency writes. // be careful, it discards monotime so should not be used for internal time comparisons. type atomicTime struct { i atomic.Int64 } func (t *atomicTime) Load() time.Time { return time.Unix(0, t.i.Load()) } func (t *atomicTime) Store(v time.Time) { t.i.Store(v.UnixNano()) } func (t *atomicTime) Swap(v time.Time) time.Time { return time.Unix(0, t.i.Swap(v.UnixNano())) } func (t *atomicTime) CompareAndSwap(old, new time.Time) bool { return t.i.CompareAndSwap(old.UnixNano(), new.UnixNano()) } func (t *atomicTime) MarshalText() ([]byte, error) { return t.Load().MarshalText() } func (t *atomicTime) UnmarshalText(text []byte) error { var v time.Time if err := v.UnmarshalText(text); err != nil { return err } t.Store(v) return nil } ================================================ FILE: core/Clash.Meta/test/.golangci.yaml ================================================ linters: disable-all: true enable: - gofumpt - govet - gci - staticcheck linters-settings: gci: sections: - standard - prefix(github.com/metacubex/mihomo) - default staticcheck: go: '1.19' ================================================ FILE: core/Clash.Meta/test/Makefile ================================================ lint: GOOS=darwin golangci-lint run ./... GOOS=linux golangci-lint run ./... test: go test -p 1 -v ./... benchmark: go test -benchmem -run=^$$ -bench . ================================================ FILE: core/Clash.Meta/test/clash_test.go ================================================ package main import ( "context" "crypto/md5" "crypto/rand" "errors" "fmt" "io" "net" "net/netip" "os" "path/filepath" "runtime" "sync" "testing" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/metacubex/mihomo/adapter/outbound" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/transport/socks5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( ImageShadowsocks = "mritd/shadowsocks:latest" ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest" ImageVmess = "v2fly/v2fly-core:v4.45.2" ImageVmessLatest = "sagernet/v2fly-core:latest" ImageVless = "teddysun/xray:latest" ImageTrojan = "trojangfw/trojan:latest" ImageTrojanGo = "p4gefau1t/trojan-go:latest" ImageSnell = "ghcr.io/icpz/snell-server:latest" ImageXray = "teddysun/xray:latest" ImageHysteria = "tobyxdd/hysteria:latest" ) var ( waitTime = time.Second localIP = netip.MustParseAddr("127.0.0.1") defaultExposedPorts = nat.PortSet{ "10002/tcp": struct{}{}, "10002/udp": struct{}{}, } defaultPortBindings = nat.PortMap{ "10002/tcp": []nat.PortBinding{ {HostPort: "10002", HostIP: "0.0.0.0"}, }, "10002/udp": []nat.PortBinding{ {HostPort: "10002", HostIP: "0.0.0.0"}, }, } isDarwin = runtime.GOOS == "darwin" ) func init() { currentDir, err := os.Getwd() if err != nil { panic(err) } homeDir := filepath.Join(currentDir, "config") C.SetHomeDir(homeDir) if isDarwin { localIP, err = defaultRouteIP() if err != nil { panic(err) } } c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } defer c.Close() list, err := c.ImageList(context.Background(), types.ImageListOptions{All: true}) if err != nil { panic(err) } imageExist := func(image string) bool { for _, item := range list { for _, tag := range item.RepoTags { if image == tag { return true } } } return false } images := []string{ ImageShadowsocks, ImageShadowsocksRust, ImageVmess, ImageVless, ImageTrojan, ImageTrojanGo, ImageSnell, ImageXray, ImageHysteria, } for _, image := range images { if imageExist(image) { continue } println("pulling image:", image) imageStream, err := c.ImagePull(context.Background(), image, types.ImagePullOptions{}) if err != nil { panic(err) } io.Copy(io.Discard, imageStream) } } var clean = ` port: 0 socks-port: 0 mixed-port: 0 redir-port: 0 tproxy-port: 0 dns: enable: false ` func cleanup() { parseAndApply(clean) } func parseAndApply(cfgStr string) error { cfg, err := executor.ParseWithBytes([]byte(cfgStr)) if err != nil { return err } executor.ApplyConfig(cfg, true) return nil } 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 testPingPongWithSocksPort(t *testing.T, port int) { pingCh, pongCh, test := newPingPongPair() go func() { l, err := Listen("tcp", ":10001") require.NoError(t, err) defer l.Close() c, err := l.Accept() require.NoError(t, err) buf := make([]byte, 4) _, err = io.ReadFull(c, buf) require.NoError(t, err) pingCh <- buf _, err = c.Write([]byte("pong")) require.NoError(t, err) }() go func() { c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) require.NoError(t, err) defer c.Close() _, err = socks5.ClientHandshake(c, socks5.ParseAddr("127.0.0.1:10001"), socks5.CmdConnect, nil) require.NoError(t, err) _, err = c.Write([]byte("ping")) require.NoError(t, err) buf := make([]byte, 4) _, err = io.ReadFull(c, buf) require.NoError(t, err) pongCh <- buf }() test(t) } func testPingPongWithConn(t *testing.T, cc func() net.Conn) error { l, err := Listen("tcp", ":10001") if err != nil { return err } defer l.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 } }() c := cc() defer c.Close() go func() { if _, err := c.Write([]byte("ping")); err != nil { return } buf := make([]byte, 4) if _, err := io.ReadFull(c, buf); err != nil { t.Error(err) return } pongCh <- buf }() return test(t) } func testPingPongWithPacketConn(t *testing.T, pc net.PacketConn) error { l, err := ListenPacket("udp", ":10001") require.NoError(t, err) defer l.Close() rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: 10001} 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 } }() 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, cc func() net.Conn) error { l, err := Listen("tcp", ":10001") 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 } 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, } }() c := cc() defer c.Close() 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, pc net.PacketConn) error { l, err := ListenPacket("udp", ":10001") require.NoError(t, err) defer l.Close() rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: 10001} times := 50 chunkSize := int64(1024) pingCh, pongCh, test := newLargeDataPair() writeRandData := func(pc net.PacketConn, addr net.Addr) (map[int][]byte, error) { hashMap := map[int][]byte{} mux := sync.Mutex{} go func() { for i := 0; i < times; i++ { buf := make([]byte, chunkSize) if _, err := rand.Read(buf[1:]); err != nil { t.Log(err.Error()) return } 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()) return } } }() 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, } }() 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, pc net.PacketConn) error { 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 testSuit(t *testing.T, proxy C.ProxyAdapter) { assert.NoError(t, testPingPongWithConn(t, func() net.Conn { conn, err := proxy.DialContext(context.Background(), &C.Metadata{ Host: localIP.String(), DstPort: 10001, }) require.NoError(t, err) return conn })) assert.NoError(t, testLargeDataWithConn(t, func() net.Conn { conn, err := proxy.DialContext(context.Background(), &C.Metadata{ Host: localIP.String(), DstPort: 10001, }) require.NoError(t, err) return conn })) if !proxy.SupportUDP() { return } pc, err := proxy.ListenPacketContext(context.Background(), &C.Metadata{ NetWork: C.UDP, DstIP: localIP, DstPort: 10001, }) require.NoError(t, err) defer pc.Close() assert.NoError(t, testPingPongWithPacketConn(t, pc)) pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ NetWork: C.UDP, DstIP: localIP, DstPort: 10001, }) require.NoError(t, err) defer pc.Close() assert.NoError(t, testLargeDataWithPacketConn(t, pc)) pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ NetWork: C.UDP, DstIP: localIP, DstPort: 10001, }) require.NoError(t, err) defer pc.Close() assert.NoError(t, testPacketConnTimeout(t, pc)) } func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) { l, err := Listen("tcp", ":10001") require.NoError(b, err) defer l.Close() chunkSize := int64(16 * 1024) chunk := make([]byte, chunkSize) rand.Read(chunk) go func() { c, err := l.Accept() if err != nil { return } defer c.Close() go func() { for { _, err := c.Write(chunk) if err != nil { return } } }() io.Copy(io.Discard, c) }() conn, err := proxy.DialContext(context.Background(), &C.Metadata{ Host: localIP.String(), DstPort: 10001, }) require.NoError(b, err) _, err = conn.Write([]byte("skip protocol handshake")) require.NoError(b, err) b.Run("Write", func(b *testing.B) { b.SetBytes(chunkSize) for i := 0; i < b.N; i++ { conn.Write(chunk) } }) b.Run("Read", func(b *testing.B) { b.SetBytes(chunkSize) buf := make([]byte, chunkSize) for i := 0; i < b.N; i++ { io.ReadFull(conn, buf) } }) } func TestMihomo_Basic(t *testing.T) { basic := ` mixed-port: 10000 log-level: silent ` err := parseAndApply(basic) require.NoError(t, err) defer cleanup() require.True(t, TCPing(net.JoinHostPort(localIP.String(), "10000"))) testPingPongWithSocksPort(t, 10000) } func Benchmark_Direct(b *testing.B) { proxy := outbound.NewDirect() benchmarkProxy(b, proxy) } ================================================ FILE: core/Clash.Meta/test/config/example.org-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQ+c++LkDTdaw5 5spCu9MWMcvVdrYBZZ5qZy7DskphSUSQp25cIu34GJXVPNxtbWx1CQCmdLlwqXvo PfUt5/pz9qsfhdAbzFduZQgGd7GTQOTJBDrAhm2+iVsQyGHHhF68muN+SgT+AtRE sJyZoHNYtjjWEIHQ++FHEDqwUVnj6Ut99LHlyfCjOZ5+WyBiKCjyMNots/gDep7R i4X2kMTqNMIIqPUcAaP5EQk41bJbFhKe915qN9b1dRISKFKmiWeOsxgTB/O/EaL5 LsBYwZ/BiIMDk30aZvzRJeloasIR3z4hrKQqBfB0lfeIdiPpJIs5rXJQEiWH89ge gplsLbfrAgMBAAECggEBAKpMGaZzDPMF/v8Ee6lcZM2+cMyZPALxa+JsCakCvyh+ y7hSKVY+RM0cQ+YM/djTBkJtvrDniEMuasI803PAitI7nwJGSuyMXmehP6P9oKFO jeLeZn6ETiSqzKJlmYE89vMeCevdqCnT5mW/wy5Smg0eGj0gIJpM2S3PJPSQpv9Z ots0JXkwooJcpGWzlwPkjSouY2gDbE4Coi+jmYLNjA1k5RbggcutnUCZZkJ6yMNv H52VjnkffpAFHRouK/YgF+5nbMyyw5YTLOyTWBq7qfBMsXynkWLU73GC/xDZa3yG o/Ph2knXCjgLmCRessTOObdOXedjnGWIjiqF8fVboDECgYEA6x5CteYiwthDBULZ CG5nE9VKkRHJYdArm+VjmGbzK51tKli112avmU4r3ol907+mEa4tWLkPqdZrrL49 aHltuHizZJixJcw0rcI302ot/Ov0gkF9V55gnAQS/Kemvx9FHWm5NHdYvbObzj33 bYRLJBtJWzYg9M8Bw9ZrUnegc/MCgYEA44kq5OSYCbyu3eaX8XHTtFhuQHNFjwl7 Xk/Oel6PVZzmt+oOlDHnOfGSB/KpR3YXxFRngiiPZzbrOwFyPGe7HIfg03HAXiJh ivEfrPHbQqQUI/4b44GpDy6bhNtz777ivFGYEt21vpwd89rFiye+RkqF8eL/evxO pUayDZYvwikCgYEA07wFoZ/lkAiHmpZPsxsRcrfzFd+pto9splEWtumHdbCo3ajT 4W5VFr9iHF8/VFDT8jokFjFaXL1/bCpKTOqFl8oC68XiSkKy8gPkmFyXm5y2LhNi GGTFZdr5alRkgttbN5i9M/WCkhvMZRhC2Xp43MRB9IUzeqNtWHqhXbvjYGcCgYEA vTMOztviLJ6PjYa0K5lp31l0+/SeD21j/y0/VPOSHi9kjeN7EfFZAw6DTkaSShDB fIhutYVCkSHSgfMW6XGb3gKCiW/Z9KyEDYOowicuGgDTmoYu7IOhbzVjLhtJET7Z zJvQZ0eiW4f3RBFTF/4JMuu+6z7FD6ADSV06qx+KQNkCgYBw26iQxmT5e/4kVv8X DzBJ1HuliKBnnzZA1YRjB4H8F6Yrq+9qur1Lurez4YlbkGV8yPFt+Iu82ViUWL28 9T7Jgp3TOpf8qOqsWFv8HldpEZbE0Tcib4x6s+zOg/aw0ac/xOPY1sCVFB81VODP XCar+uxMBXI1zbXqd9QdEwy4Ig== -----END PRIVATE KEY----- ================================================ FILE: core/Clash.Meta/test/config/example.org.pem ================================================ -----BEGIN CERTIFICATE----- MIIESzCCArOgAwIBAgIQIi5xRZvFZaSweWU9Y5mExjANBgkqhkiG9w0BAQsFADCB hzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMS4wLAYDVQQLDCVkcmVh bWFjcm9ARHJlYW1hY3JvLmxvY2FsIChEcmVhbWFjcm8pMTUwMwYDVQQDDCxta2Nl cnQgZHJlYW1hY3JvQERyZWFtYWNyby5sb2NhbCAoRHJlYW1hY3JvKTAeFw0yMTAz MTcxNDQwMzZaFw0yMzA2MTcxNDQwMzZaMFkxJzAlBgNVBAoTHm1rY2VydCBkZXZl bG9wbWVudCBjZXJ0aWZpY2F0ZTEuMCwGA1UECwwlZHJlYW1hY3JvQERyZWFtYWNy by5sb2NhbCAoRHJlYW1hY3JvKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAND5z74uQNN1rDnmykK70xYxy9V2tgFlnmpnLsOySmFJRJCnblwi7fgYldU8 3G1tbHUJAKZ0uXCpe+g99S3n+nP2qx+F0BvMV25lCAZ3sZNA5MkEOsCGbb6JWxDI YceEXrya435KBP4C1ESwnJmgc1i2ONYQgdD74UcQOrBRWePpS330seXJ8KM5nn5b IGIoKPIw2i2z+AN6ntGLhfaQxOo0wgio9RwBo/kRCTjVslsWEp73Xmo31vV1EhIo UqaJZ46zGBMH878RovkuwFjBn8GIgwOTfRpm/NEl6WhqwhHfPiGspCoF8HSV94h2 I+kkizmtclASJYfz2B6CmWwtt+sCAwEAAaNgMF4wDgYDVR0PAQH/BAQDAgWgMBMG A1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFO800LQ6Pa85RH4EbMmFH6ln F150MBYGA1UdEQQPMA2CC2V4YW1wbGUub3JnMA0GCSqGSIb3DQEBCwUAA4IBgQAP TsF53h7bvJcUXT3Y9yZ2vnW6xr9r92tNnM1Gfo3D2Yyn9oLf2YrfJng6WZ04Fhqa Wh0HOvE0n6yPNpm/Q7mh64DrgolZ8Ce5H4RTJDAabHU9XhEzfGSVtzRSFsz+szu1 Y30IV+08DxxqMmNPspYdpAET2Lwyk2WhnARGiGw11CRkQCEkVEe6d702vS9UGBUz Du6lmCYCm0SbFrZ0CGgmHSHoTcCtf3EjVam7dPg3yWiPbWjvhXxgip6hz9sCqkhG WA5f+fPgSZ1I9U4i+uYnqjfrzwgC08RwUYordm15F6gPvXw+KVwDO8yUYQoEH0b6 AFJtbzoAXDysvBC6kWYFFOr62EaisaEkELTS/NrPD9ux1eKbxcxHCwEtVjgC0CL6 gAxEAQ+9maJMbrAFhsOBbGGFC+mMCGg4eEyx6+iMB0oQe0W7QFeRUAFi7Ptc/ocS tZ9lbrfX1/wrcTTWIYWE+xH6oeb4fhs29kxjHcf2l+tQzmpl0aP3Z/bMW4BSB+w= -----END CERTIFICATE----- ================================================ FILE: core/Clash.Meta/test/config/hysteria.json ================================================ { "listen": ":10002", "cert": "/home/ubuntu/my.crt", "key": "/home/ubuntu/my.key", "obfs": "fuck me till the daylight", "up_mbps": 100, "down_mbps": 100 } ================================================ FILE: core/Clash.Meta/test/config/snell-http.conf ================================================ [snell-server] listen = 0.0.0.0:10002 psk = password obfs = http ================================================ FILE: core/Clash.Meta/test/config/snell-tls.conf ================================================ [snell-server] listen = 0.0.0.0:10002 psk = password obfs = tls ================================================ FILE: core/Clash.Meta/test/config/snell.conf ================================================ [snell-server] listen = 0.0.0.0:10002 psk = password ================================================ FILE: core/Clash.Meta/test/config/trojan-grpc.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "trojan", "settings": { "clients": [ { "password": "example", "email": "grpc@example.com" } ] }, "streamSettings": { "network": "grpc", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] }, "grpcSettings": { "serviceName": "example" } } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/trojan-ws.json ================================================ { "run_type": "server", "local_addr": "0.0.0.0", "local_port": 10002, "disable_http_check": true, "password": [ "example" ], "websocket": { "enabled": true, "path": "/", "host": "example.org" }, "ssl": { "verify": true, "cert": "/fullchain.pem", "key": "/privkey.pem", "sni": "example.org" } } ================================================ FILE: core/Clash.Meta/test/config/trojan-xtls.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "trojan", "settings": { "clients": [ { "password": "example", "email": "xtls@example.com", "flow": "xtls-rprx-direct", "level": 0 } ] }, "streamSettings": { "network": "tcp", "security": "xtls", "xtlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] } } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/trojan.json ================================================ { "run_type": "server", "local_addr": "0.0.0.0", "local_port": 10002, "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: core/Clash.Meta/test/config/vless-tls.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vless", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "level": 0, "email": "love@example.com" } ], "decryption": "none" }, "streamSettings": { "network": "tcp", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] } } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/vless-ws.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vless", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "level": 0, "email": "ws@example.com" } ], "decryption": "none" }, "streamSettings": { "network": "ws", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] } } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: core/Clash.Meta/test/config/vless-xtls.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vless", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "email": "xtls@example.com", "flow": "xtls-rprx-direct", "level": 0 } ], "decryption": "none" }, "streamSettings": { "network": "tcp", "security": "xtls", "xtlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] } } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/vmess-grpc.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "grpc", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] }, "grpcSettings": { "serviceName": "example!" } } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/vmess-http.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "tcp", "tcpSettings": { "header": { "type": "http", "response": { "version": "1.1", "status": "200", "reason": "OK", "headers": { "Content-Type": [ "application/octet-stream", "video/mpeg", "application/x-msdownload", "text/html", "application/x-shockwave-flash" ], "Transfer-Encoding": [ "chunked" ], "Connection": [ "keep-alive" ], "Pragma": "no-cache" } } } }, "security": "none" } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/vmess-http2.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "http", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] }, "httpSettings": { "host": [ "example.org" ], "path": "/test" } } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/vmess-tls.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "tcp", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] } } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/vmess-ws-0rtt.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "ws", "security": "none", "wsSettings": { "maxEarlyData": 128, "earlyDataHeaderName": "Sec-WebSocket-Protocol" } } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: core/Clash.Meta/test/config/vmess-ws-tls.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "ws", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/etc/ssl/v2ray/fullchain.pem", "keyFile": "/etc/ssl/v2ray/privkey.pem" } ] } } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: core/Clash.Meta/test/config/vmess-ws.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "ws", "security": "none" } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: core/Clash.Meta/test/config/vmess.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "tcp" } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/config/xray-shadowsocks.json ================================================ { "inbounds": [ { "port": 10002, "listen": "0.0.0.0", "protocol": "shadowsocks", "settings": { "network": "tcp,udp", "clients": [ { "method": "aes-128-gcm", "level": 0, "password": "FzcLbKs2dY9mhL" } ] } } ], "outbounds": [ { "protocol": "freedom" } ], "log": { "loglevel": "debug" } } ================================================ FILE: core/Clash.Meta/test/dns_test.go ================================================ package main import ( "testing" "time" "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func exchange(address, domain string, tp uint16) ([]dns.RR, error) { client := dns.Client{} query := &dns.Msg{} query.SetQuestion(dns.Fqdn(domain), tp) r, _, err := client.Exchange(query, address) if err != nil { return nil, err } return r.Answer, nil } func TestMihomo_DNS(t *testing.T) { basic := ` log-level: silent dns: enable: true listen: 0.0.0.0:8553 nameserver: - 119.29.29.29 ` err := parseAndApply(basic) require.NoError(t, err) defer cleanup() time.Sleep(waitTime) rr, err := exchange("127.0.0.1:8553", "1.1.1.1.nip.io", dns.TypeA) assert.NoError(t, err) assert.NotEmptyf(t, rr, "record empty") record := rr[0].(*dns.A) assert.Equal(t, record.A.String(), "1.1.1.1") rr, err = exchange("127.0.0.1:8553", "2606-4700-4700--1111.sslip.io", dns.TypeAAAA) assert.NoError(t, err) assert.Empty(t, rr) } func TestMihomo_DNSHostAndFakeIP(t *testing.T) { basic := ` log-level: silent hosts: foo.mihomo.dev: 1.1.1.1 dns: enable: true listen: 0.0.0.0:8553 ipv6: true enhanced-mode: fake-ip fake-ip-range: 198.18.0.1/16 fake-ip-filter: - .sslip.io nameserver: - 119.29.29.29 ` err := parseAndApply(basic) require.NoError(t, err) defer cleanup() time.Sleep(waitTime) type domainPair struct { domain string ip string } list := []domainPair{ {"foo.org", "198.18.0.4"}, {"bar.org", "198.18.0.5"}, {"foo.org", "198.18.0.4"}, {"foo.mihomo.dev", "1.1.1.1"}, } for _, pair := range list { rr, err := exchange("127.0.0.1:8553", pair.domain, dns.TypeA) assert.NoError(t, err) assert.NotEmpty(t, rr) record := rr[0].(*dns.A) assert.Equal(t, record.A.String(), pair.ip) } rr, err := exchange("127.0.0.1:8553", "2606-4700-4700--1111.sslip.io", dns.TypeAAAA) assert.NoError(t, err) assert.NotEmpty(t, rr) assert.Equal(t, rr[0].(*dns.AAAA).AAAA.String(), "2606:4700:4700::1111") } ================================================ FILE: core/Clash.Meta/test/docker_test.go ================================================ package main import ( "context" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" ) func startContainer(cfg *container.Config, hostCfg *container.HostConfig, name string) (string, error) { c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return "", err } defer c.Close() if !isDarwin { hostCfg.NetworkMode = "host" } container, err := c.ContainerCreate(context.Background(), cfg, hostCfg, nil, nil, name) if err != nil { return "", err } if err = c.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{}); err != nil { return "", err } response, err := c.ContainerAttach(context.Background(), container.ID, types.ContainerAttachOptions{ Stdout: true, Stderr: true, Logs: true, }) if err != nil { return "", err } go func() { response.Reader.WriteTo(os.Stderr) }() return container.ID, nil } func cleanContainer(id string) error { c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return err } defer c.Close() removeOpts := types.ContainerRemoveOptions{Force: true} return c.ContainerRemove(context.Background(), id, removeOpts) } ================================================ FILE: core/Clash.Meta/test/go.mod ================================================ module mihomo-test go 1.20 require ( github.com/docker/docker v20.10.21+incompatible github.com/docker/go-connections v0.4.0 github.com/metacubex/mihomo v0.0.0 github.com/miekg/dns v1.1.57 github.com/stretchr/testify v1.8.4 golang.org/x/net v0.18.0 ) replace github.com/metacubex/mihomo => ../ require ( github.com/3andne/restls-client-go v0.1.6 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/RyuaNerin/go-krypto v1.0.2 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/coreos/go-iptables v0.7.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.4.0 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gaukas/godicttls v0.0.4 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.1 // indirect github.com/gofrs/uuid/v5 v5.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c // indirect github.com/josharian/native v1.1.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gvisor v0.0.0-20231001104248-0f672c3fb8d8 // indirect github.com/metacubex/quic-go v0.40.1-0.20231130135418-0c1b47cf9394 // indirect github.com/metacubex/sing-quic v0.0.0-20231130141855-0022295e524b // indirect github.com/metacubex/sing-shadowsocks v0.2.5 // indirect github.com/metacubex/sing-shadowsocks2 v0.1.4 // indirect github.com/metacubex/sing-tun v0.1.15-0.20231103033938-170591e8d5bd // indirect github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74 // indirect github.com/metacubex/sing-wireguard v0.0.0-20231001110902-321836559170 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/openacid/low v0.1.21 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c // indirect github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 // indirect github.com/sagernet/sing-shadowtls v0.1.4 // indirect github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect github.com/samber/lo v1.38.1 // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/shirou/gopsutil/v3 v3.23.10 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/zhangyunhao116/fastrand v0.3.0 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect go.uber.org/mock v0.3.0 // indirect go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect golang.org/x/crypto v0.16.0 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.15.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect ) ================================================ FILE: core/Clash.Meta/test/go.sum ================================================ github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/RyuaNerin/go-krypto v1.0.2 h1:9KiZrrBs+tDrQ66dNy4nrX6SzntKtSKdm0wKHhdB4WM= github.com/RyuaNerin/go-krypto v1.0.2/go.mod h1:17LzMeJCgzGTkPH3TmfzRnEJ/yA7ErhTPp9sxIqONtA= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 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/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 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/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU= github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= github.com/gofrs/uuid/v5 v5.0.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/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM= github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20231001104248-0f672c3fb8d8 h1:npBvaPAT145UY8682AzpUMWpdIxJti/WPLjy7gCiYYs= github.com/metacubex/gvisor v0.0.0-20231001104248-0f672c3fb8d8/go.mod h1:ZR6Gas7P1GcADCVBc1uOrA0bLQqDDyp70+63fD/BE2c= github.com/metacubex/quic-go v0.40.1-0.20231130135418-0c1b47cf9394 h1:dIT+KB2hknBCrwVAXPeY9tpzzkOZP5m40yqUteRT6/Y= github.com/metacubex/quic-go v0.40.1-0.20231130135418-0c1b47cf9394/go.mod h1:F/t8VnA47xoia8ABlNA4InkZjssvFJ5p6E6jKdbkgAs= github.com/metacubex/sing-quic v0.0.0-20231130141855-0022295e524b h1:7XXoEePvxfkQN9b2wB8UXU3uzb9uL8syEFF7A9VAKKQ= github.com/metacubex/sing-quic v0.0.0-20231130141855-0022295e524b/go.mod h1:Gu5/zqZDd5G1AUtoV2yjAPWOEy7zwbU2DBUjdxJh0Kw= github.com/metacubex/sing-shadowsocks v0.2.5 h1:O2RRSHlKGEpAVG/OHJQxyHqDy8uvvdCW/oW2TDBOIhc= github.com/metacubex/sing-shadowsocks v0.2.5/go.mod h1:Xz2uW9BEYGEoA8B4XEpoxt7ERHClFCwsMAvWaruoyMo= github.com/metacubex/sing-shadowsocks2 v0.1.4 h1:OOCf8lgsVcpTOJUeaFAMzyKVebaQOBnKirDdUdBoKIE= github.com/metacubex/sing-shadowsocks2 v0.1.4/go.mod h1:Qz028sLfdY3qxGRm9FDI+IM2Ae3ty2wR7HIzD/56h/k= github.com/metacubex/sing-tun v0.1.15-0.20231103033938-170591e8d5bd h1:k0+92eARqyTAovGhg2AxdsMWHjUsdiGCnR5NuXF3CQY= github.com/metacubex/sing-tun v0.1.15-0.20231103033938-170591e8d5bd/go.mod h1:Q7zmpJ+qOvMMXyUoYlxGQuWkqALUpXzFSSqO+KLPyzA= github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74 h1:FtupiyFkaVjFvRa7B/uDtRWg5BNsoyPC9MTev3sDasY= github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74/go.mod h1:8EWBZpc+qNvf5gmvjAtMHK1/DpcWqzfcBL842K00BsM= github.com/metacubex/sing-wireguard v0.0.0-20231001110902-321836559170 h1:DBGA0hmrP4pVIwLiXUONdphjcppED+plmVaKf1oqkwk= github.com/metacubex/sing-wireguard v0.0.0-20231001110902-321836559170/go.mod h1:/VbJfbdLnANE+SKXyMk/96sTRrD4GdFLh5mkegqqFcY= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= 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/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I= github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= 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.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 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/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c h1:uask61Pxc3nGqsOSjqnBKrwfODWRoEa80lXm04LNk0E= github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as= github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0= github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGVnWH5A8eR7JhNnIV3rGQmBxA7cw6Q= github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/shirou/gopsutil/v3 v3.23.10 h1:/N42opWlYzegYaVkWejXWJpbzKv2JDy3mrgGzKsh9hM= github.com/shirou/gopsutil/v3 v3.23.10/go.mod h1:JIE26kpucQi+innVlAUnIEOSBhBUkirr5b44yr55+WE= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU= github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo= github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig= github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ= go4.org/netipx v0.0.0-20230824141953-6213f710f925/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.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= 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= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 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.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= ================================================ FILE: core/Clash.Meta/test/hysteria_test.go ================================================ package main import ( "fmt" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/metacubex/mihomo/adapter/outbound" C "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/assert" ) func TestMihomo_Hysteria(t *testing.T) { cfg := &container.Config{ Image: ImageHysteria, ExposedPorts: defaultExposedPorts, Cmd: []string{"server"}, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/config.json", C.Path.Resolve("hysteria.json")), fmt.Sprintf("%s:/home/ubuntu/my.crt", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/home/ubuntu/my.key", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "hysteria") if err != nil { assert.FailNow(t, err.Error()) } t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewHysteria(outbound.HysteriaOption{ Name: "hysteria", Server: localIP.String(), Port: 10002, Obfs: "fuck me till the daylight", Up: "100", Down: "100", SkipCertVerify: true, }) if err != nil { assert.FailNow(t, err.Error()) } time.Sleep(waitTime) testSuit(t, proxy) } ================================================ FILE: core/Clash.Meta/test/snell_test.go ================================================ package main import ( "fmt" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/metacubex/mihomo/adapter/outbound" C "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/require" ) func TestMihomo_SnellObfsHTTP(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, Cmd: []string{"-c", "/config.conf"}, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-http.conf"))}, } id, err := startContainer(cfg, hostCfg, "snell-http") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewSnell(outbound.SnellOption{ Name: "snell", Server: localIP.String(), Port: 10002, Psk: "password", ObfsOpts: map[string]any{ "mode": "http", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_SnellObfsTLS(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, Cmd: []string{"-c", "/config.conf"}, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-tls.conf"))}, } id, err := startContainer(cfg, hostCfg, "snell-tls") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewSnell(outbound.SnellOption{ Name: "snell", Server: localIP.String(), Port: 10002, Psk: "password", ObfsOpts: map[string]any{ "mode": "tls", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_Snell(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, Cmd: []string{"-c", "/config.conf"}, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell.conf"))}, } id, err := startContainer(cfg, hostCfg, "snell") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewSnell(outbound.SnellOption{ Name: "snell", Server: localIP.String(), Port: 10002, Psk: "password", }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_Snellv3(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, Cmd: []string{"-c", "/config.conf"}, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell.conf"))}, } id, err := startContainer(cfg, hostCfg, "snell") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewSnell(outbound.SnellOption{ Name: "snell", Server: localIP.String(), Port: 10002, Psk: "password", UDP: true, Version: 3, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func Benchmark_Snell(b *testing.B) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, Cmd: []string{"-c", "/config.conf"}, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-http.conf"))}, } id, err := startContainer(cfg, hostCfg, "snell-bench") require.NoError(b, err) b.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewSnell(outbound.SnellOption{ Name: "snell", Server: localIP.String(), Port: 10002, Psk: "password", ObfsOpts: map[string]any{ "mode": "http", }, }) require.NoError(b, err) time.Sleep(waitTime) benchmarkProxy(b, proxy) } ================================================ FILE: core/Clash.Meta/test/ss_test.go ================================================ package main import ( "crypto/rand" "encoding/base64" "fmt" "net" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/metacubex/mihomo/adapter/outbound" C "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/require" ) func TestMihomo_Shadowsocks(t *testing.T) { for _, method := range []string{ "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", } { t.Run(method, func(t *testing.T) { testMihomo_Shadowsocks(t, method, "FzcLbKs2dY9mhL") }) } for _, method := range []string{ "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", } { t.Run(method, func(t *testing.T) { testMihomo_ShadowsocksRust(t, method, "FzcLbKs2dY9mhL") }) } } func TestMihomo_Shadowsocks2022(t *testing.T) { for _, method := range []string{ "2022-blake3-aes-128-gcm", } { t.Run(method, func(t *testing.T) { testMihomo_ShadowsocksRust(t, method, mkKey(16)) }) } for _, method := range []string{ "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305", } { t.Run(method, func(t *testing.T) { testMihomo_ShadowsocksRust(t, method, mkKey(32)) }) } } func mkKey(bits int) string { k := make([]byte, bits) rand.Read(k) return base64.StdEncoding.EncodeToString(k) } func testMihomo_Shadowsocks(t *testing.T, method string, password string) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ "SS_MODULE=ss-server", "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m " + method + " -k " + password, }, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, } id, err := startContainer(cfg, hostCfg, "ss") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ Name: "ss", Server: localIP.String(), Port: 10002, Password: password, Cipher: method, UDP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func testMihomo_ShadowsocksRust(t *testing.T, method string, password string) { cfg := &container.Config{ Image: ImageShadowsocksRust, Entrypoint: []string{"ssserver"}, Cmd: []string{"-s", "0.0.0.0:10002", "-m", method, "-k", password, "-U", "-v"}, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, } id, err := startContainer(cfg, hostCfg, "ss-rust") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ Name: "ss", Server: localIP.String(), Port: 10002, Password: password, Cipher: method, UDP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_ShadowsocksObfsHTTP(t *testing.T) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ "SS_MODULE=ss-server", "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL --plugin obfs-server --plugin-opts obfs=http", }, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, } id, err := startContainer(cfg, hostCfg, "ss-obfs-http") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ Name: "ss", Server: localIP.String(), Port: 10002, Password: "FzcLbKs2dY9mhL", Cipher: "chacha20-ietf-poly1305", UDP: true, Plugin: "obfs", PluginOpts: map[string]any{ "mode": "http", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_ShadowsocksObfsTLS(t *testing.T) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ "SS_MODULE=ss-server", "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL --plugin obfs-server --plugin-opts obfs=tls", }, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, } id, err := startContainer(cfg, hostCfg, "ss-obfs-tls") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ Name: "ss", Server: localIP.String(), Port: 10002, Password: "FzcLbKs2dY9mhL", Cipher: "chacha20-ietf-poly1305", UDP: true, Plugin: "obfs", PluginOpts: map[string]any{ "mode": "tls", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_ShadowsocksV2RayPlugin(t *testing.T) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ "SS_MODULE=ss-server", "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL --plugin v2ray-plugin --plugin-opts=server", }, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, } id, err := startContainer(cfg, hostCfg, "ss-v2ray-plugin") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ Name: "ss", Server: localIP.String(), Port: 10002, Password: "FzcLbKs2dY9mhL", Cipher: "chacha20-ietf-poly1305", UDP: true, Plugin: "v2ray-plugin", PluginOpts: map[string]any{ "mode": "websocket", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func Benchmark_Shadowsocks(b *testing.B) { cfg := &container.Config{ Image: ImageShadowsocksRust, Entrypoint: []string{"ssserver"}, Cmd: []string{"-s", "0.0.0.0:10002", "-m", "aes-256-gcm", "-k", "FzcLbKs2dY9mhL", "-U"}, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, } id, err := startContainer(cfg, hostCfg, "ss-bench") require.NoError(b, err) b.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ Name: "ss", Server: localIP.String(), Port: 10002, Password: "FzcLbKs2dY9mhL", Cipher: "aes-256-gcm", UDP: true, }) require.NoError(b, err) require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002"))) benchmarkProxy(b, proxy) } func TestMihomo_ShadowsocksUoT(t *testing.T) { configPath := C.Path.Resolve("xray-shadowsocks.json") cfg := &container.Config{ Image: ImageVless, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/etc/xray/config.json", configPath)}, } id, err := startContainer(cfg, hostCfg, "xray-ss") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ Name: "ss", Server: localIP.String(), Port: 10002, Password: "FzcLbKs2dY9mhL", Cipher: "aes-128-gcm", UDP: true, UDPOverTCP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } ================================================ FILE: core/Clash.Meta/test/trojan_test.go ================================================ package main import ( "fmt" "net" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/metacubex/mihomo/adapter/outbound" C "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/require" ) func TestMihomo_Trojan(t *testing.T) { cfg := &container.Config{ Image: ImageTrojan, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/config/config.json", C.Path.Resolve("trojan.json")), fmt.Sprintf("%s:/path/to/certificate.crt", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/path/to/private.key", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "trojan") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewTrojan(outbound.TrojanOption{ Name: "trojan", Server: localIP.String(), Port: 10002, Password: "password", SNI: "example.org", SkipCertVerify: true, UDP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_TrojanGrpc(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/xray/config.json", C.Path.Resolve("trojan-grpc.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "trojan-grpc") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewTrojan(outbound.TrojanOption{ Name: "trojan", Server: localIP.String(), Port: 10002, Password: "example", SNI: "example.org", SkipCertVerify: true, UDP: true, Network: "grpc", GrpcOpts: outbound.GrpcOptions{ GrpcServiceName: "example", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_TrojanWebsocket(t *testing.T) { cfg := &container.Config{ Image: ImageTrojanGo, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/trojan-go/config.json", C.Path.Resolve("trojan-ws.json")), fmt.Sprintf("%s:/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "trojan-ws") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewTrojan(outbound.TrojanOption{ Name: "trojan", Server: localIP.String(), Port: 10002, Password: "example", SNI: "example.org", SkipCertVerify: true, UDP: true, Network: "ws", }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_TrojanXTLS(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/xray/config.json", C.Path.Resolve("trojan-xtls.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "trojan-xtls") if err != nil { require.NoError(t, err) } defer cleanContainer(id) proxy, err := outbound.NewTrojan(outbound.TrojanOption{ Name: "trojan", Server: localIP.String(), Port: 10002, Password: "example", SNI: "example.org", SkipCertVerify: true, UDP: true, Network: "tcp", Flow: "xtls-rprx-direct", FlowShow: true, }) if err != nil { require.NoError(t, err) } time.Sleep(waitTime) testSuit(t, proxy) } func Benchmark_Trojan(b *testing.B) { cfg := &container.Config{ Image: ImageTrojan, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/config/config.json", C.Path.Resolve("trojan.json")), fmt.Sprintf("%s:/path/to/certificate.crt", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/path/to/private.key", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "trojan-bench") require.NoError(b, err) b.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewTrojan(outbound.TrojanOption{ Name: "trojan", Server: localIP.String(), Port: 10002, Password: "password", SNI: "example.org", SkipCertVerify: true, UDP: true, }) require.NoError(b, err) require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002"))) benchmarkProxy(b, proxy) } ================================================ FILE: core/Clash.Meta/test/util.go ================================================ package main import ( "context" "net" "time" ) func Listen(network, address string) (net.Listener, error) { lc := net.ListenConfig{} 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(time.Millisecond * 200) } return nil, lastErr } func ListenPacket(network, address string) (net.PacketConn, error) { var lastErr error for i := 0; i < 5; i++ { l, err := net.ListenPacket(network, address) if err == nil { return l, nil } lastErr = err time.Sleep(time.Millisecond * 200) } return nil, lastErr } func TCPing(addr string) bool { for i := 0; i < 10; i++ { conn, err := net.Dial("tcp", addr) if err == nil { conn.Close() return true } time.Sleep(time.Millisecond * 500) } return false } ================================================ FILE: core/Clash.Meta/test/util_darwin_test.go ================================================ package main import ( "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 { a, _ := netip.AddrFromSlice(ip) return a, nil } } return netip.Addr{}, err } 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: core/Clash.Meta/test/util_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: core/Clash.Meta/test/vless_test.go ================================================ package main import ( "fmt" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/metacubex/mihomo/adapter/outbound" C "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/assert" ) // TODO: fix udp test func TestMihomo_VlessTLS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-tls.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "vless-tls") if err != nil { assert.FailNow(t, err.Error()) } defer cleanContainer(id) proxy, err := outbound.NewVless(outbound.VlessOption{ Name: "vless", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", TLS: true, SkipCertVerify: true, ServerName: "example.org", UDP: true, }) if err != nil { assert.FailNow(t, err.Error()) } time.Sleep(waitTime) testSuit(t, proxy) } // TODO: fix udp test func TestMihomo_VlessXTLS(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/xray/config.json", C.Path.Resolve("vless-xtls.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "vless-xtls") if err != nil { assert.FailNow(t, err.Error()) } defer cleanContainer(id) proxy, err := outbound.NewVless(outbound.VlessOption{ Name: "vless", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", TLS: true, SkipCertVerify: true, ServerName: "example.org", UDP: true, Flow: "xtls-rprx-direct", }) if err != nil { assert.FailNow(t, err.Error()) } time.Sleep(waitTime) testSuit(t, proxy) } // TODO: fix udp test func TestMihomo_VlessWS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-ws.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "vless-ws") if err != nil { assert.FailNow(t, err.Error()) } defer cleanContainer(id) proxy, err := outbound.NewVless(outbound.VlessOption{ Name: "vless", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", TLS: true, SkipCertVerify: true, ServerName: "example.org", Network: "ws", UDP: true, }) if err != nil { assert.FailNow(t, err.Error()) } time.Sleep(waitTime) testSuit(t, proxy) } ================================================ FILE: core/Clash.Meta/test/vmess_test.go ================================================ package main import ( "fmt" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/metacubex/mihomo/adapter/outbound" C "github.com/metacubex/mihomo/constant" "github.com/stretchr/testify/require" ) func TestMihomo_Vmess(t *testing.T) { configPath := C.Path.Resolve("vmess.json") cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, } id, err := startContainer(cfg, hostCfg, "vmess") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", UDP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessAuthenticatedLength(t *testing.T) { configPath := C.Path.Resolve("vmess.json") cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, } id, err := startContainer(cfg, hostCfg, "vmess") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", UDP: true, AuthenticatedLength: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessPacketAddr(t *testing.T) { configPath := C.Path.Resolve("vmess.json") cfg := &container.Config{ Image: ImageVmessLatest, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, } id, err := startContainer(cfg, hostCfg, "vmess") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", UDP: true, PacketAddr: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessTLS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-tls.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "vmess-tls") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", TLS: true, SkipCertVerify: true, ServerName: "example.org", UDP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessHTTP2(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-http2.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "vmess-http2") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", Network: "h2", TLS: true, SkipCertVerify: true, ServerName: "example.org", UDP: true, HTTP2Opts: outbound.HTTP2Options{ Host: []string{"example.org"}, Path: "/test", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessHTTP(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-http.json")), }, } id, err := startContainer(cfg, hostCfg, "vmess-http") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", Network: "http", UDP: true, HTTPOpts: outbound.HTTPOptions{ Method: "GET", Path: []string{"/"}, Headers: map[string][]string{ "Host": {"www.amazon.com"}, "User-Agent": { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36 Edg/84.0.522.49", }, "Accept-Encoding": { "gzip, deflate", }, "Connection": { "keep-alive", }, "Pragma": {"no-cache"}, }, }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessWebsocket(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-ws.json")), }, } id, err := startContainer(cfg, hostCfg, "vmess-ws") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", Network: "ws", UDP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessWebsocketTLS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-ws-tls.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "vmess-ws") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", Network: "ws", TLS: true, SkipCertVerify: true, UDP: true, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessGrpc(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-grpc.json")), fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), }, } id, err := startContainer(cfg, hostCfg, "vmess-grpc") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", Network: "grpc", TLS: true, SkipCertVerify: true, UDP: true, ServerName: "example.org", GrpcOpts: outbound.GrpcOptions{ GrpcServiceName: "example!", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessWebsocket0RTT(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-ws-0rtt.json")), }, } id, err := startContainer(cfg, hostCfg, "vmess-ws-0rtt") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", Network: "ws", UDP: true, ServerName: "example.org", WSOpts: outbound.WSOptions{ MaxEarlyData: 2048, EarlyDataHeaderName: "Sec-WebSocket-Protocol", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func TestMihomo_VmessWebsocketXray0RTT(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{ fmt.Sprintf("%s:/etc/xray/config.json", C.Path.Resolve("vmess-ws-0rtt.json")), }, } id, err := startContainer(cfg, hostCfg, "vmess-xray-ws-0rtt") require.NoError(t, err) t.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", Network: "ws", UDP: true, ServerName: "example.org", WSOpts: outbound.WSOptions{ Path: "/?ed=2048", }, }) require.NoError(t, err) time.Sleep(waitTime) testSuit(t, proxy) } func Benchmark_Vmess(b *testing.B) { configPath := C.Path.Resolve("vmess.json") cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, } hostCfg := &container.HostConfig{ PortBindings: defaultPortBindings, Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, } id, err := startContainer(cfg, hostCfg, "vmess-bench") require.NoError(b, err) b.Cleanup(func() { cleanContainer(id) }) proxy, err := outbound.NewVmess(outbound.VmessOption{ Name: "vmess", Server: localIP.String(), Port: 10002, UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", Cipher: "auto", AlterID: 0, UDP: true, }) require.NoError(b, err) time.Sleep(waitTime) benchmarkProxy(b, proxy) } ================================================ FILE: core/Clash.Meta/transport/anytls/client.go ================================================ package anytls import ( "context" "crypto/sha256" "encoding/binary" "net" "sync/atomic" "time" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/transport/anytls/padding" "github.com/metacubex/mihomo/transport/anytls/session" "github.com/metacubex/mihomo/transport/vmess" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" ) type ClientConfig struct { Password string IdleSessionCheckInterval time.Duration IdleSessionTimeout time.Duration MinIdleSession int Server M.Socksaddr Dialer N.Dialer TLSConfig *vmess.TLSConfig } type Client struct { passwordSha256 []byte tlsConfig *vmess.TLSConfig dialer N.Dialer server M.Socksaddr sessionClient *session.Client padding atomic.Pointer[padding.PaddingFactory] } func NewClient(ctx context.Context, config ClientConfig) *Client { pw := sha256.Sum256([]byte(config.Password)) c := &Client{ passwordSha256: pw[:], tlsConfig: config.TLSConfig, dialer: config.Dialer, server: config.Server, } // Initialize the padding state of this client padding.UpdatePaddingScheme(padding.DefaultPaddingScheme, &c.padding) c.sessionClient = session.NewClient(ctx, c.createOutboundTLSConnection, &c.padding, config.IdleSessionCheckInterval, config.IdleSessionTimeout, config.MinIdleSession) return c } func (c *Client) CreateProxy(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { conn, err := c.sessionClient.CreateStream(ctx) if err != nil { return nil, err } err = M.SocksaddrSerializer.WriteAddrPort(conn, destination) if err != nil { conn.Close() return nil, err } return conn, nil } func (c *Client) createOutboundTLSConnection(ctx context.Context) (net.Conn, error) { conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) if err != nil { return nil, err } b := buf.NewPacket() defer b.Release() b.Write(c.passwordSha256) var paddingLen int if pad := c.padding.Load().GenerateRecordPayloadSizes(0); len(pad) > 0 { paddingLen = pad[0] } binary.BigEndian.PutUint16(b.Extend(2), uint16(paddingLen)) if paddingLen > 0 { b.WriteZeroN(paddingLen) } tlsConn, err := vmess.StreamTLSConn(ctx, conn, c.tlsConfig) if err != nil { conn.Close() return nil, err } _, err = b.WriteTo(tlsConn) if err != nil { tlsConn.Close() return nil, err } return tlsConn, nil } func (h *Client) Close() error { return h.sessionClient.Close() } ================================================ FILE: core/Clash.Meta/transport/anytls/padding/padding.go ================================================ package padding import ( "crypto/md5" "crypto/rand" "fmt" "math/big" "strconv" "strings" "sync/atomic" "github.com/metacubex/mihomo/transport/anytls/util" ) const CheckMark = -1 var DefaultPaddingScheme = []byte(`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`) type PaddingFactory struct { scheme util.StringMap RawScheme []byte Stop uint32 Md5 string } func UpdatePaddingScheme(rawScheme []byte, to *atomic.Pointer[PaddingFactory]) bool { if p := NewPaddingFactory(rawScheme); p != nil { to.Store(p) return true } return false } func NewPaddingFactory(rawScheme []byte) *PaddingFactory { p := &PaddingFactory{ RawScheme: rawScheme, Md5: fmt.Sprintf("%x", md5.Sum(rawScheme)), } scheme := util.StringMapFromBytes(rawScheme) if len(scheme) == 0 { return nil } if stop, err := strconv.Atoi(scheme["stop"]); err == nil { p.Stop = uint32(stop) } else { return nil } p.scheme = scheme return p } func (p *PaddingFactory) GenerateRecordPayloadSizes(pkt uint32) (pktSizes []int) { if s, ok := p.scheme[strconv.Itoa(int(pkt))]; ok { sRanges := strings.Split(s, ",") for _, sRange := range sRanges { sRangeMinMax := strings.Split(sRange, "-") if len(sRangeMinMax) == 2 { _min, err := strconv.ParseInt(sRangeMinMax[0], 10, 64) if err != nil { continue } _max, err := strconv.ParseInt(sRangeMinMax[1], 10, 64) if err != nil { continue } if _min > _max { _min, _max = _max, _min } if _min <= 0 || _max <= 0 { continue } if _min == _max { pktSizes = append(pktSizes, int(_min)) } else { i, _ := rand.Int(rand.Reader, big.NewInt(_max-_min)) pktSizes = append(pktSizes, int(i.Int64()+_min)) } } else if sRange == "c" { pktSizes = append(pktSizes, CheckMark) } } } return } ================================================ FILE: core/Clash.Meta/transport/anytls/pipe/deadline.go ================================================ package pipe import ( "sync" "time" ) // PipeDeadline is an abstraction for handling timeouts. type PipeDeadline struct { mu sync.Mutex // Guards timer and cancel timer *time.Timer cancel chan struct{} // Must be non-nil } func MakePipeDeadline() PipeDeadline { return PipeDeadline{cancel: make(chan struct{})} } // Set sets the point in time when the deadline will time out. // A timeout event is signaled by closing the channel returned by waiter. // Once a timeout has occurred, the deadline can be refreshed by specifying a // t value in the future. // // A zero value for t prevents timeout. func (d *PipeDeadline) Set(t time.Time) { d.mu.Lock() defer d.mu.Unlock() if d.timer != nil && !d.timer.Stop() { <-d.cancel // Wait for the timer callback to finish and close cancel } d.timer = nil // Time is zero, then there is no deadline. closed := isClosedChan(d.cancel) if t.IsZero() { if closed { d.cancel = make(chan struct{}) } return } // Time in the future, setup a timer to cancel in the future. if dur := time.Until(t); dur > 0 { if closed { d.cancel = make(chan struct{}) } d.timer = time.AfterFunc(dur, func() { close(d.cancel) }) return } // Time in the past, so close immediately. if !closed { close(d.cancel) } } // Wait returns a channel that is closed when the deadline is exceeded. func (d *PipeDeadline) Wait() chan struct{} { d.mu.Lock() defer d.mu.Unlock() return d.cancel } func isClosedChan(c <-chan struct{}) bool { select { case <-c: return true default: return false } } ================================================ FILE: core/Clash.Meta/transport/anytls/pipe/io_pipe.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. // Pipe adapter to connect code expecting an io.Reader // with code expecting an io.Writer. package pipe import ( "io" "os" "sync" "time" ) // onceError is an object that will only store an error once. type onceError struct { sync.Mutex // guards following err error } func (a *onceError) Store(err error) { a.Lock() defer a.Unlock() if a.err != nil { return } a.err = err } func (a *onceError) Load() error { a.Lock() defer a.Unlock() return a.err } // A pipe is the shared pipe structure underlying PipeReader and PipeWriter. type pipe struct { wrMu sync.Mutex // Serializes Write operations wrCh chan []byte rdCh chan int once sync.Once // Protects closing done done chan struct{} rerr onceError werr onceError readDeadline PipeDeadline writeDeadline PipeDeadline } func (p *pipe) read(b []byte) (n int, err error) { select { case <-p.done: return 0, p.readCloseError() case <-p.readDeadline.Wait(): return 0, os.ErrDeadlineExceeded default: } select { case bw := <-p.wrCh: nr := copy(b, bw) p.rdCh <- nr return nr, nil case <-p.done: return 0, p.readCloseError() case <-p.readDeadline.Wait(): return 0, os.ErrDeadlineExceeded } } func (p *pipe) closeRead(err error) error { if err == nil { err = io.ErrClosedPipe } p.rerr.Store(err) p.once.Do(func() { close(p.done) }) return nil } func (p *pipe) write(b []byte) (n int, err error) { select { case <-p.done: return 0, p.writeCloseError() case <-p.writeDeadline.Wait(): return 0, os.ErrDeadlineExceeded default: p.wrMu.Lock() defer p.wrMu.Unlock() } for once := true; once || len(b) > 0; once = false { select { case p.wrCh <- b: nw := <-p.rdCh b = b[nw:] n += nw case <-p.done: return n, p.writeCloseError() case <-p.writeDeadline.Wait(): return n, os.ErrDeadlineExceeded } } return n, nil } func (p *pipe) closeWrite(err error) error { if err == nil { err = io.EOF } p.werr.Store(err) p.once.Do(func() { close(p.done) }) return nil } // readCloseError is considered internal to the pipe type. func (p *pipe) readCloseError() error { rerr := p.rerr.Load() if werr := p.werr.Load(); rerr == nil && werr != nil { return werr } return io.ErrClosedPipe } // writeCloseError is considered internal to the pipe type. func (p *pipe) writeCloseError() error { werr := p.werr.Load() if rerr := p.rerr.Load(); werr == nil && rerr != nil { return rerr } return io.ErrClosedPipe } // A PipeReader is the read half of a pipe. type PipeReader struct{ pipe } // Read implements the standard Read interface: // it reads data from the pipe, blocking until a writer // arrives or the write end is closed. // If the write end is closed with an error, that error is // returned as err; otherwise err is EOF. func (r *PipeReader) Read(data []byte) (n int, err error) { return r.pipe.read(data) } // Close closes the reader; subsequent writes to the // write half of the pipe will return the error [ErrClosedPipe]. func (r *PipeReader) Close() error { return r.CloseWithError(nil) } // CloseWithError closes the reader; subsequent writes // to the write half of the pipe will return the error err. // // CloseWithError never overwrites the previous error if it exists // and always returns nil. func (r *PipeReader) CloseWithError(err error) error { return r.pipe.closeRead(err) } // A PipeWriter is the write half of a pipe. type PipeWriter struct{ r PipeReader } // Write implements the standard Write interface: // it writes data to the pipe, blocking until one or more readers // have consumed all the data or the read end is closed. // If the read end is closed with an error, that err is // returned as err; otherwise err is [ErrClosedPipe]. func (w *PipeWriter) Write(data []byte) (n int, err error) { return w.r.pipe.write(data) } // Close closes the writer; subsequent reads from the // read half of the pipe will return no bytes and EOF. func (w *PipeWriter) Close() error { return w.CloseWithError(nil) } // CloseWithError closes the writer; subsequent reads from the // read half of the pipe will return no bytes and the error err, // or EOF if err is nil. // // CloseWithError never overwrites the previous error if it exists // and always returns nil. func (w *PipeWriter) CloseWithError(err error) error { return w.r.pipe.closeWrite(err) } // Pipe creates a synchronous in-memory pipe. // It can be used to connect code expecting an [io.Reader] // with code expecting an [io.Writer]. // // Reads and Writes on the pipe are matched one to one // except when multiple Reads are needed to consume a single Write. // That is, each Write to the [PipeWriter] blocks until it has satisfied // one or more Reads from the [PipeReader] that fully consume // the written data. // The data is copied directly from the Write to the corresponding // Read (or Reads); there is no internal buffering. // // It is safe to call Read and Write in parallel with each other or with Close. // Parallel calls to Read and parallel calls to Write are also safe: // the individual calls will be gated sequentially. // // Added SetReadDeadline and SetWriteDeadline methods based on `io.Pipe`. func Pipe() (*PipeReader, *PipeWriter) { pw := &PipeWriter{r: PipeReader{pipe: pipe{ wrCh: make(chan []byte), rdCh: make(chan int), done: make(chan struct{}), readDeadline: MakePipeDeadline(), writeDeadline: MakePipeDeadline(), }}} return &pw.r, pw } func (p *PipeReader) SetReadDeadline(t time.Time) error { if isClosedChan(p.done) { return io.ErrClosedPipe } p.readDeadline.Set(t) return nil } func (p *PipeWriter) SetWriteDeadline(t time.Time) error { if isClosedChan(p.r.done) { return io.ErrClosedPipe } p.r.writeDeadline.Set(t) return nil } ================================================ FILE: core/Clash.Meta/transport/anytls/session/client.go ================================================ package session import ( "context" "fmt" "io" "math" "net" "sync" "sync/atomic" "time" "github.com/metacubex/mihomo/transport/anytls/padding" "github.com/metacubex/mihomo/transport/anytls/skiplist" "github.com/metacubex/mihomo/transport/anytls/util" ) type Client struct { die context.Context dieCancel context.CancelFunc dialOut util.DialOutFunc sessionCounter atomic.Uint64 idleSession *skiplist.SkipList[uint64, *Session] idleSessionLock sync.Mutex sessions map[uint64]*Session sessionsLock sync.Mutex padding *atomic.Pointer[padding.PaddingFactory] idleSessionTimeout time.Duration minIdleSession int } func NewClient(ctx context.Context, dialOut util.DialOutFunc, _padding *atomic.Pointer[padding.PaddingFactory], idleSessionCheckInterval, idleSessionTimeout time.Duration, minIdleSession int) *Client { c := &Client{ sessions: make(map[uint64]*Session), dialOut: dialOut, padding: _padding, idleSessionTimeout: idleSessionTimeout, minIdleSession: minIdleSession, } if idleSessionCheckInterval <= time.Second*5 { idleSessionCheckInterval = time.Second * 30 } if c.idleSessionTimeout <= time.Second*5 { c.idleSessionTimeout = time.Second * 30 } c.die, c.dieCancel = context.WithCancel(ctx) c.idleSession = skiplist.NewSkipList[uint64, *Session]() util.StartRoutine(c.die, idleSessionCheckInterval, c.idleCleanup) return c } func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) { select { case <-c.die.Done(): return nil, io.ErrClosedPipe default: } var session *Session var stream *Stream var err error session = c.getIdleSession() if session == nil { session, err = c.createSession(ctx) } if session == nil { return nil, fmt.Errorf("failed to create session: %w", err) } stream, err = session.OpenStream() if err != nil { session.Close() return nil, fmt.Errorf("failed to create stream: %w", err) } stream.dieHook = func() { // If Session is not closed, put this Stream to pool if !session.IsClosed() { select { case <-c.die.Done(): // Now client has been closed go session.Close() default: c.idleSessionLock.Lock() session.idleSince = time.Now() c.idleSession.Insert(math.MaxUint64-session.seq, session) c.idleSessionLock.Unlock() } } } return stream, nil } func (c *Client) getIdleSession() (idle *Session) { c.idleSessionLock.Lock() if !c.idleSession.IsEmpty() { it := c.idleSession.Iterate() idle = it.Value() c.idleSession.Remove(it.Key()) } c.idleSessionLock.Unlock() return } func (c *Client) createSession(ctx context.Context) (*Session, error) { underlying, err := c.dialOut(ctx) if err != nil { return nil, err } session := NewClientSession(underlying, c.padding) session.seq = c.sessionCounter.Add(1) session.dieHook = func() { c.idleSessionLock.Lock() c.idleSession.Remove(math.MaxUint64 - session.seq) c.idleSessionLock.Unlock() c.sessionsLock.Lock() delete(c.sessions, session.seq) c.sessionsLock.Unlock() } c.sessionsLock.Lock() c.sessions[session.seq] = session c.sessionsLock.Unlock() session.Run() return session, nil } func (c *Client) Close() error { c.dieCancel() c.sessionsLock.Lock() sessionToClose := make([]*Session, 0, len(c.sessions)) for _, session := range c.sessions { sessionToClose = append(sessionToClose, session) } c.sessions = make(map[uint64]*Session) c.sessionsLock.Unlock() for _, session := range sessionToClose { session.Close() } return nil } func (c *Client) idleCleanup() { c.idleCleanupExpTime(time.Now().Add(-c.idleSessionTimeout)) } func (c *Client) idleCleanupExpTime(expTime time.Time) { activeCount := 0 sessionToClose := make([]*Session, 0, c.idleSession.Len()) c.idleSessionLock.Lock() it := c.idleSession.Iterate() for it.IsNotEnd() { session := it.Value() key := it.Key() it.MoveToNext() if !session.idleSince.Before(expTime) { activeCount++ continue } if activeCount < c.minIdleSession { session.idleSince = time.Now() activeCount++ continue } sessionToClose = append(sessionToClose, session) c.idleSession.Remove(key) } c.idleSessionLock.Unlock() for _, session := range sessionToClose { session.Close() } } ================================================ FILE: core/Clash.Meta/transport/anytls/session/frame.go ================================================ package session import ( "encoding/binary" ) const ( // cmds cmdWaste = 0 // Paddings cmdSYN = 1 // stream open cmdPSH = 2 // data push cmdFIN = 3 // stream close, a.k.a EOF mark cmdSettings = 4 // Settings (Client send to Server) cmdAlert = 5 // Alert cmdUpdatePaddingScheme = 6 // update padding scheme // Since version 2 cmdSYNACK = 7 // Server reports to the client that the stream has been opened cmdHeartRequest = 8 // Keep alive command cmdHeartResponse = 9 // Keep alive command cmdServerSettings = 10 // Settings (Server send to client) ) const ( headerOverHeadSize = 1 + 4 + 2 ) // frame defines a packet from or to be multiplexed into a single connection type frame struct { cmd byte // 1 sid uint32 // 4 data []byte // 2 + len(data) } func newFrame(cmd byte, sid uint32) frame { return frame{cmd: cmd, sid: sid} } type rawHeader [headerOverHeadSize]byte func (h rawHeader) Cmd() byte { return h[0] } func (h rawHeader) StreamID() uint32 { return binary.BigEndian.Uint32(h[1:]) } func (h rawHeader) Length() uint16 { return binary.BigEndian.Uint16(h[5:]) } ================================================ FILE: core/Clash.Meta/transport/anytls/session/session.go ================================================ package session import ( "crypto/md5" "encoding/binary" "fmt" "io" "net" "runtime/debug" "strconv" "sync" "sync/atomic" "time" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/anytls/padding" "github.com/metacubex/mihomo/transport/anytls/util" ) type Session struct { conn net.Conn connLock sync.Mutex streams map[uint32]*Stream streamId atomic.Uint32 streamLock sync.RWMutex dieOnce sync.Once die chan struct{} dieHook func() synDone func() synDoneLock sync.Mutex // pool seq uint64 idleSince time.Time padding *atomic.Pointer[padding.PaddingFactory] peerVersion byte // client isClient bool sendPadding bool buffering bool buffer []byte pktCounter atomic.Uint32 // server onNewStream func(stream *Stream) } func NewClientSession(conn net.Conn, _padding *atomic.Pointer[padding.PaddingFactory]) *Session { s := &Session{ conn: conn, isClient: true, sendPadding: true, padding: _padding, } s.die = make(chan struct{}) s.streams = make(map[uint32]*Stream) return s } func NewServerSession(conn net.Conn, onNewStream func(stream *Stream), _padding *atomic.Pointer[padding.PaddingFactory]) *Session { s := &Session{ conn: conn, onNewStream: onNewStream, padding: _padding, } s.die = make(chan struct{}) s.streams = make(map[uint32]*Stream) return s } func (s *Session) Run() { if !s.isClient { s.recvLoop() return } settings := util.StringMap{ "v": "2", "client": "mihomo/" + constant.Version, "padding-md5": s.padding.Load().Md5, } f := newFrame(cmdSettings, 0) f.data = settings.ToBytes() s.buffering = true s.writeControlFrame(f) go s.recvLoop() } // IsClosed does a safe check to see if we have shutdown func (s *Session) IsClosed() bool { select { case <-s.die: return true default: return false } } // Close is used to close the session and all streams. func (s *Session) Close() error { var once bool s.dieOnce.Do(func() { close(s.die) once = true }) if once { if s.dieHook != nil { s.dieHook() s.dieHook = nil } s.streamLock.Lock() for _, stream := range s.streams { stream.closeLocally() } s.streams = make(map[uint32]*Stream) s.streamLock.Unlock() return s.conn.Close() } else { return io.ErrClosedPipe } } // OpenStream is used to create a new stream for CLIENT func (s *Session) OpenStream() (*Stream, error) { if s.IsClosed() { return nil, io.ErrClosedPipe } sid := s.streamId.Add(1) stream := newStream(sid, s) if sid >= 2 && s.peerVersion >= 2 { s.synDoneLock.Lock() if s.synDone != nil { s.synDone() } s.synDone = util.NewDeadlineWatcher(time.Second*3, func() { s.Close() }) s.synDoneLock.Unlock() } if _, err := s.writeControlFrame(newFrame(cmdSYN, sid)); err != nil { return nil, err } s.buffering = false // proxy Write it's SocksAddr to flush the buffer s.streamLock.Lock() defer s.streamLock.Unlock() select { case <-s.die: return nil, io.ErrClosedPipe default: s.streams[sid] = stream return stream, nil } } func (s *Session) recvLoop() error { defer func() { if r := recover(); r != nil { log.Errorln("[BUG] %v %s", r, string(debug.Stack())) } }() defer s.Close() var receivedSettingsFromClient bool var hdr rawHeader for { if s.IsClosed() { return io.ErrClosedPipe } // read header first if _, err := io.ReadFull(s.conn, hdr[:]); err == nil { sid := hdr.StreamID() switch hdr.Cmd() { case cmdPSH: if hdr.Length() > 0 { buffer := pool.Get(int(hdr.Length())) if _, err := io.ReadFull(s.conn, buffer); err == nil { s.streamLock.RLock() stream, ok := s.streams[sid] s.streamLock.RUnlock() if ok { stream.pipeW.Write(buffer) } pool.Put(buffer) } else { pool.Put(buffer) return err } } case cmdSYN: // should be server only if !s.isClient && !receivedSettingsFromClient { f := newFrame(cmdAlert, 0) f.data = []byte("client did not send its settings") s.writeControlFrame(f) return nil } s.streamLock.Lock() if _, ok := s.streams[sid]; !ok { stream := newStream(sid, s) s.streams[sid] = stream go func() { if s.onNewStream != nil { s.onNewStream(stream) } else { stream.Close() } }() } s.streamLock.Unlock() case cmdSYNACK: // should be client only s.synDoneLock.Lock() if s.synDone != nil { s.synDone() s.synDone = nil } s.synDoneLock.Unlock() if hdr.Length() > 0 { buffer := pool.Get(int(hdr.Length())) if _, err := io.ReadFull(s.conn, buffer); err != nil { pool.Put(buffer) return err } // report error s.streamLock.RLock() stream, ok := s.streams[sid] s.streamLock.RUnlock() if ok { stream.closeWithError(fmt.Errorf("remote: %s", string(buffer))) } pool.Put(buffer) } case cmdFIN: s.streamLock.Lock() stream, ok := s.streams[sid] delete(s.streams, sid) s.streamLock.Unlock() if ok { stream.closeLocally() } case cmdWaste: if hdr.Length() > 0 { buffer := pool.Get(int(hdr.Length())) if _, err := io.ReadFull(s.conn, buffer); err != nil { pool.Put(buffer) return err } pool.Put(buffer) } case cmdSettings: if hdr.Length() > 0 { buffer := pool.Get(int(hdr.Length())) if _, err := io.ReadFull(s.conn, buffer); err != nil { pool.Put(buffer) return err } if !s.isClient { receivedSettingsFromClient = true m := util.StringMapFromBytes(buffer) paddingF := s.padding.Load() if m["padding-md5"] != paddingF.Md5 { f := newFrame(cmdUpdatePaddingScheme, 0) f.data = paddingF.RawScheme _, err = s.writeControlFrame(f) if err != nil { pool.Put(buffer) return err } } // check client's version if v, err := strconv.Atoi(m["v"]); err == nil && v >= 2 { s.peerVersion = byte(v) // send cmdServerSettings f := newFrame(cmdServerSettings, 0) f.data = util.StringMap{ "v": "2", }.ToBytes() _, err = s.writeControlFrame(f) if err != nil { pool.Put(buffer) return err } } } pool.Put(buffer) } case cmdAlert: if hdr.Length() > 0 { buffer := pool.Get(int(hdr.Length())) if _, err := io.ReadFull(s.conn, buffer); err != nil { pool.Put(buffer) return err } if s.isClient { log.Errorln("[Alert from server] %s", string(buffer)) } pool.Put(buffer) return nil } case cmdUpdatePaddingScheme: if hdr.Length() > 0 { // `rawScheme` Do not use buffer to prevent subsequent misuse rawScheme := make([]byte, int(hdr.Length())) if _, err := io.ReadFull(s.conn, rawScheme); err != nil { return err } if s.isClient { if padding.UpdatePaddingScheme(rawScheme, s.padding) { log.Debugln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) } else { log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme)) } } } case cmdHeartRequest: if _, err := s.writeControlFrame(newFrame(cmdHeartResponse, sid)); err != nil { return err } case cmdHeartResponse: // Active keepalive checking is not implemented yet break case cmdServerSettings: if hdr.Length() > 0 { buffer := pool.Get(int(hdr.Length())) if _, err := io.ReadFull(s.conn, buffer); err != nil { pool.Put(buffer) return err } if s.isClient { // check server's version m := util.StringMapFromBytes(buffer) if v, err := strconv.Atoi(m["v"]); err == nil { s.peerVersion = byte(v) } } pool.Put(buffer) } default: // I don't know what command it is (can't have data) } } else { return err } } } func (s *Session) streamClosed(sid uint32) error { if s.IsClosed() { return io.ErrClosedPipe } _, err := s.writeControlFrame(newFrame(cmdFIN, sid)) s.streamLock.Lock() delete(s.streams, sid) s.streamLock.Unlock() return err } func (s *Session) writeDataFrame(sid uint32, data []byte) (int, error) { dataLen := len(data) buffer := buf.NewSize(dataLen + headerOverHeadSize) buffer.WriteByte(cmdPSH) binary.BigEndian.PutUint32(buffer.Extend(4), sid) binary.BigEndian.PutUint16(buffer.Extend(2), uint16(dataLen)) buffer.Write(data) _, err := s.writeConn(buffer.Bytes()) buffer.Release() if err != nil { return 0, err } return dataLen, nil } func (s *Session) writeControlFrame(frame frame) (int, error) { dataLen := len(frame.data) buffer := buf.NewSize(dataLen + headerOverHeadSize) buffer.WriteByte(frame.cmd) binary.BigEndian.PutUint32(buffer.Extend(4), frame.sid) binary.BigEndian.PutUint16(buffer.Extend(2), uint16(dataLen)) buffer.Write(frame.data) s.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) _, err := s.writeConn(buffer.Bytes()) buffer.Release() if err != nil { s.Close() return 0, err } s.conn.SetWriteDeadline(time.Time{}) return dataLen, nil } func (s *Session) writeConn(b []byte) (n int, err error) { s.connLock.Lock() defer s.connLock.Unlock() if s.buffering { s.buffer = append(s.buffer, b...) return len(b), nil } else if len(s.buffer) > 0 { b = append(s.buffer, b...) s.buffer = nil } // calulate & send padding if s.sendPadding { pkt := s.pktCounter.Add(1) paddingF := s.padding.Load() if pkt < paddingF.Stop { pktSizes := paddingF.GenerateRecordPayloadSizes(pkt) for _, l := range pktSizes { remainPayloadLen := len(b) if l == padding.CheckMark { if remainPayloadLen == 0 { break } else { continue } } if remainPayloadLen > l { // this packet is all payload _, err = s.conn.Write(b[:l]) if err != nil { return 0, err } n += l b = b[l:] } else if remainPayloadLen > 0 { // this packet contains padding and the last part of payload paddingLen := l - remainPayloadLen - headerOverHeadSize if paddingLen > 0 { padding := make([]byte, headerOverHeadSize+paddingLen) padding[0] = cmdWaste binary.BigEndian.PutUint32(padding[1:5], 0) binary.BigEndian.PutUint16(padding[5:7], uint16(paddingLen)) b = append(b, padding...) } _, err = s.conn.Write(b) if err != nil { return 0, err } n += remainPayloadLen b = nil } else { // this packet is all padding padding := make([]byte, headerOverHeadSize+l) padding[0] = cmdWaste binary.BigEndian.PutUint32(padding[1:5], 0) binary.BigEndian.PutUint16(padding[5:7], uint16(l)) _, err = s.conn.Write(padding) if err != nil { return 0, err } b = nil } } // maybe still remain payload to write if len(b) == 0 { return } else { n2, err := s.conn.Write(b) return n + n2, err } } else { s.sendPadding = false } } return s.conn.Write(b) } ================================================ FILE: core/Clash.Meta/transport/anytls/session/stream.go ================================================ package session import ( "io" "net" "os" "sync" "time" "github.com/metacubex/mihomo/transport/anytls/pipe" ) // Stream implements net.Conn type Stream struct { id uint32 sess *Session pipeR *pipe.PipeReader pipeW *pipe.PipeWriter writeDeadline pipe.PipeDeadline dieOnce sync.Once dieHook func() dieErr error reportOnce sync.Once } // newStream initiates a Stream struct func newStream(id uint32, sess *Session) *Stream { s := new(Stream) s.id = id s.sess = sess s.pipeR, s.pipeW = pipe.Pipe() s.writeDeadline = pipe.MakePipeDeadline() return s } // Read implements net.Conn func (s *Stream) Read(b []byte) (n int, err error) { n, err = s.pipeR.Read(b) if n == 0 && s.dieErr != nil { err = s.dieErr } return } // Write implements net.Conn func (s *Stream) Write(b []byte) (n int, err error) { select { case <-s.writeDeadline.Wait(): return 0, os.ErrDeadlineExceeded default: } if s.dieErr != nil { return 0, s.dieErr } n, err = s.sess.writeDataFrame(s.id, b) return } // Close implements net.Conn func (s *Stream) Close() error { return s.closeWithError(io.ErrClosedPipe) } // closeLocally only closes Stream and don't notify remote peer func (s *Stream) closeLocally() { var once bool s.dieOnce.Do(func() { s.dieErr = net.ErrClosed s.pipeR.Close() once = true }) if once { if s.dieHook != nil { s.dieHook() s.dieHook = nil } } } func (s *Stream) closeWithError(err error) error { var once bool s.dieOnce.Do(func() { s.dieErr = err s.pipeR.Close() once = true }) if once { if s.dieHook != nil { s.dieHook() s.dieHook = nil } return s.sess.streamClosed(s.id) } else { return s.dieErr } } func (s *Stream) SetReadDeadline(t time.Time) error { return s.pipeR.SetReadDeadline(t) } func (s *Stream) SetWriteDeadline(t time.Time) error { s.writeDeadline.Set(t) return nil } func (s *Stream) SetDeadline(t time.Time) error { s.SetWriteDeadline(t) return s.SetReadDeadline(t) } // LocalAddr satisfies net.Conn interface func (s *Stream) LocalAddr() net.Addr { if ts, ok := s.sess.conn.(interface { LocalAddr() net.Addr }); ok { return ts.LocalAddr() } return nil } // RemoteAddr satisfies net.Conn interface func (s *Stream) RemoteAddr() net.Addr { if ts, ok := s.sess.conn.(interface { RemoteAddr() net.Addr }); ok { return ts.RemoteAddr() } return nil } // HandshakeFailure should be called when Server fail to create outbound proxy func (s *Stream) HandshakeFailure(err error) error { var once bool s.reportOnce.Do(func() { once = true }) if once && err != nil && s.sess.peerVersion >= 2 { f := newFrame(cmdSYNACK, s.id) f.data = []byte(err.Error()) if _, err := s.sess.writeControlFrame(f); err != nil { return err } } return nil } // HandshakeSuccess should be called when Server success to create outbound proxy func (s *Stream) HandshakeSuccess() error { var once bool s.reportOnce.Do(func() { once = true }) if once && s.sess.peerVersion >= 2 { if _, err := s.sess.writeControlFrame(newFrame(cmdSYNACK, s.id)); err != nil { return err } } return nil } ================================================ FILE: core/Clash.Meta/transport/anytls/skiplist/contianer.go ================================================ package skiplist // Container is a holder object that stores a collection of other objects. type Container interface { IsEmpty() bool // IsEmpty checks if the container has no elements. Len() int // Len returns the number of elements in the container. Clear() // Clear erases all elements from the container. After this call, Len() returns zero. } // Map is a associative container that contains key-value pairs with unique keys. type Map[K any, V any] interface { Container Has(K) bool // Checks whether the container contains element with specific key. Find(K) *V // Finds element with specific key. Insert(K, V) // Inserts a key-value pair in to the container or replace existing value. Remove(K) bool // Remove element with specific key. ForEach(func(K, V)) // Iterate the container. ForEachIf(func(K, V) bool) // Iterate the container, stops when the callback returns false. ForEachMutable(func(K, *V)) // Iterate the container, *V is mutable. ForEachMutableIf(func(K, *V) bool) // Iterate the container, *V is mutable, stops when the callback returns false. } // Set is a containers that store unique elements. type Set[K any] interface { Container Has(K) bool // Checks whether the container contains element with specific key. Insert(K) // Inserts a key-value pair in to the container or replace existing value. InsertN(...K) // Inserts multiple key-value pairs in to the container or replace existing value. Remove(K) bool // Remove element with specific key. RemoveN(...K) // Remove multiple elements with specific keys. ForEach(func(K)) // Iterate the container. ForEachIf(func(K) bool) // Iterate the container, stops when the callback returns false. } // Iterator is the interface for container's iterator. type Iterator[T any] interface { IsNotEnd() bool // Whether it is point to the end of the range. MoveToNext() // Let it point to the next element. Value() T // Return the value of current element. } // MapIterator is the interface for map's iterator. type MapIterator[K any, V any] interface { Iterator[V] Key() K // The key of the element } ================================================ FILE: core/Clash.Meta/transport/anytls/skiplist/skiplist.go ================================================ package skiplist // This implementation is based on https://github.com/liyue201/gostl/tree/master/ds/skiplist // (many thanks), added many optimizations, such as: // // - adaptive level // - lesser search for prevs when key already exists. // - reduce memory allocations // - richer interface. // // etc. import ( "math/bits" "math/rand" "time" ) const ( skipListMaxLevel = 40 ) // SkipList is a probabilistic data structure that seem likely to supplant balanced trees as the // implementation method of choice for many applications. Skip list algorithms have the same // asymptotic expected time bounds as balanced trees and are simpler, faster and use less space. // // See https://en.wikipedia.org/wiki/Skip_list for more details. type SkipList[K any, V any] struct { level int // Current level, may increase dynamically during insertion len int // Total elements numner in the skiplist. head skipListNode[K, V] // head.next[level] is the head of each level. // This cache is used to save the previous nodes when modifying the skip list to avoid // allocating memory each time it is called. prevsCache []*skipListNode[K, V] rander *rand.Rand impl skipListImpl[K, V] } // NewSkipList creates a new SkipList for Ordered key type. func NewSkipList[K Ordered, V any]() *SkipList[K, V] { sl := skipListOrdered[K, V]{} sl.init() sl.impl = (skipListImpl[K, V])(&sl) return &sl.SkipList } // NewSkipListFromMap creates a new SkipList from a map. func NewSkipListFromMap[K Ordered, V any](m map[K]V) *SkipList[K, V] { sl := NewSkipList[K, V]() for k, v := range m { sl.Insert(k, v) } return sl } // NewSkipListFunc creates a new SkipList with specified compare function keyCmp. func NewSkipListFunc[K any, V any](keyCmp CompareFn[K]) *SkipList[K, V] { sl := skipListFunc[K, V]{} sl.init() sl.keyCmp = keyCmp sl.impl = skipListImpl[K, V](&sl) return &sl.SkipList } // IsEmpty implements the Container interface. func (sl *SkipList[K, V]) IsEmpty() bool { return sl.len == 0 } // Len implements the Container interface. func (sl *SkipList[K, V]) Len() int { return sl.len } // Clear implements the Container interface. func (sl *SkipList[K, V]) Clear() { for i := range sl.head.next { sl.head.next[i] = nil } sl.level = 1 sl.len = 0 } // Iterate return an iterator to the skiplist. func (sl *SkipList[K, V]) Iterate() MapIterator[K, V] { return &skipListIterator[K, V]{sl.head.next[0], nil} } // Insert inserts a key-value pair into the skiplist. // If the key is already in the skip list, it's value will be updated. func (sl *SkipList[K, V]) Insert(key K, value V) { node, prevs := sl.impl.findInsertPoint(key) if node != nil { // Already exist, update the value node.value = value return } level := sl.randomLevel() node = newSkipListNode(level, key, value) minLevel := level if sl.level < level { minLevel = sl.level } for i := 0; i < minLevel; i++ { node.next[i] = prevs[i].next[i] prevs[i].next[i] = node } if level > sl.level { for i := sl.level; i < level; i++ { sl.head.next[i] = node } sl.level = level } sl.len++ } // Find returns the value associated with the passed key if the key is in the skiplist, otherwise // returns nil. func (sl *SkipList[K, V]) Find(key K) *V { node := sl.impl.findNode(key) if node != nil { return &node.value } return nil } // Has implement the Map interface. func (sl *SkipList[K, V]) Has(key K) bool { return sl.impl.findNode(key) != nil } // LowerBound returns an iterator to the first element in the skiplist that // does not satisfy element < value (i.e. greater or equal to), // or a end itetator if no such element is found. func (sl *SkipList[K, V]) LowerBound(key K) MapIterator[K, V] { return &skipListIterator[K, V]{sl.impl.lowerBound(key), nil} } // UpperBound returns an iterator to the first element in the skiplist that // does not satisfy value < element (i.e. strictly greater), // or a end itetator if no such element is found. func (sl *SkipList[K, V]) UpperBound(key K) MapIterator[K, V] { return &skipListIterator[K, V]{sl.impl.upperBound(key), nil} } // FindRange returns an iterator in range [first, last) (last is not includeed). func (sl *SkipList[K, V]) FindRange(first, last K) MapIterator[K, V] { return &skipListIterator[K, V]{sl.impl.lowerBound(first), sl.impl.upperBound(last)} } // Remove removes the key-value pair associated with the passed key and returns true if the key is // in the skiplist, otherwise returns false. func (sl *SkipList[K, V]) Remove(key K) bool { node, prevs := sl.impl.findRemovePoint(key) if node == nil { return false } for i, v := range node.next { prevs[i].next[i] = v } for sl.level > 1 && sl.head.next[sl.level-1] == nil { sl.level-- } sl.len-- return true } // ForEach implements the Map interface. func (sl *SkipList[K, V]) ForEach(op func(K, V)) { for e := sl.head.next[0]; e != nil; e = e.next[0] { op(e.key, e.value) } } // ForEachMutable implements the Map interface. func (sl *SkipList[K, V]) ForEachMutable(op func(K, *V)) { for e := sl.head.next[0]; e != nil; e = e.next[0] { op(e.key, &e.value) } } // ForEachIf implements the Map interface. func (sl *SkipList[K, V]) ForEachIf(op func(K, V) bool) { for e := sl.head.next[0]; e != nil; e = e.next[0] { if !op(e.key, e.value) { return } } } // ForEachMutableIf implements the Map interface. func (sl *SkipList[K, V]) ForEachMutableIf(op func(K, *V) bool) { for e := sl.head.next[0]; e != nil; e = e.next[0] { if !op(e.key, &e.value) { return } } } /// SkipList implementation part. type skipListNode[K any, V any] struct { key K value V next []*skipListNode[K, V] } //go:generate bash ./skiplist_newnode_generate.sh skipListMaxLevel skiplist_newnode.go // func newSkipListNode[K Ordered, V any](level int, key K, value V) *skipListNode[K, V] type skipListIterator[K any, V any] struct { node, end *skipListNode[K, V] } func (it *skipListIterator[K, V]) IsNotEnd() bool { return it.node != it.end } func (it *skipListIterator[K, V]) MoveToNext() { it.node = it.node.next[0] } func (it *skipListIterator[K, V]) Key() K { return it.node.key } func (it *skipListIterator[K, V]) Value() V { return it.node.value } // skipListImpl is an interface to provide different implementation for Ordered key or CompareFn. // // We can use CompareFn to cumpare Ordered keys, but a separated implementation is much faster. // We don't make the whole skip list an interface, in order to share the type independented method. // And because these methods are called directly without going through the interface, they are also // much faster. type skipListImpl[K any, V any] interface { findNode(key K) *skipListNode[K, V] lowerBound(key K) *skipListNode[K, V] upperBound(key K) *skipListNode[K, V] findInsertPoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) findRemovePoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) } func (sl *SkipList[K, V]) init() { sl.level = 1 // #nosec G404 -- This is not a security condition sl.rander = rand.New(rand.NewSource(time.Now().Unix())) sl.prevsCache = make([]*skipListNode[K, V], skipListMaxLevel) sl.head.next = make([]*skipListNode[K, V], skipListMaxLevel) } func (sl *SkipList[K, V]) randomLevel() int { total := uint64(1)< 3 && 1<<(level-3) > sl.len { level-- } return level } /// skipListOrdered part // skipListOrdered is the skip list implementation for Ordered types. type skipListOrdered[K Ordered, V any] struct { SkipList[K, V] } func (sl *skipListOrdered[K, V]) findNode(key K) *skipListNode[K, V] { return sl.doFindNode(key, true) } func (sl *skipListOrdered[K, V]) doFindNode(key K, eq bool) *skipListNode[K, V] { // This function execute the job of findNode if eq is true, otherwise lowBound. // Passing the control variable eq is ugly but it's faster than testing node // again outside the function in findNode. prev := &sl.head for i := sl.level - 1; i >= 0; i-- { for cur := prev.next[i]; cur != nil; cur = cur.next[i] { if cur.key == key { return cur } if cur.key > key { // All other node in this level must be greater than the key, // search the next level. break } prev = cur } } if eq { return nil } return prev.next[0] } func (sl *skipListOrdered[K, V]) lowerBound(key K) *skipListNode[K, V] { return sl.doFindNode(key, false) } func (sl *skipListOrdered[K, V]) upperBound(key K) *skipListNode[K, V] { node := sl.lowerBound(key) if node != nil && node.key == key { return node.next[0] } return node } // findInsertPoint returns (*node, nil) to the existed node if the key exists, // or (nil, []*node) to the previous nodes if the key doesn't exist func (sl *skipListOrdered[K, V]) findInsertPoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { prevs := sl.prevsCache[0:sl.level] prev := &sl.head for i := sl.level - 1; i >= 0; i-- { for next := prev.next[i]; next != nil; next = next.next[i] { if next.key == key { // The key is already existed, prevs are useless because no new node insertion. // stop searching. return next, nil } if next.key > key { // All other node in this level must be greater than the key, // search the next level. break } prev = next } prevs[i] = prev } return nil, prevs } // findRemovePoint finds the node which match the key and it's previous nodes. func (sl *skipListOrdered[K, V]) findRemovePoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { prevs := sl.findPrevNodes(key) node := prevs[0].next[0] if node == nil || node.key != key { return nil, nil } return node, prevs } func (sl *skipListOrdered[K, V]) findPrevNodes(key K) []*skipListNode[K, V] { prevs := sl.prevsCache[0:sl.level] prev := &sl.head for i := sl.level - 1; i >= 0; i-- { for next := prev.next[i]; next != nil; next = next.next[i] { if next.key >= key { break } prev = next } prevs[i] = prev } return prevs } /// skipListFunc part // skipListFunc is the skip list implementation which compare keys with func. type skipListFunc[K any, V any] struct { SkipList[K, V] keyCmp CompareFn[K] } func (sl *skipListFunc[K, V]) findNode(key K) *skipListNode[K, V] { node := sl.lowerBound(key) if node != nil && sl.keyCmp(node.key, key) == 0 { return node } return nil } func (sl *skipListFunc[K, V]) lowerBound(key K) *skipListNode[K, V] { var prev = &sl.head for i := sl.level - 1; i >= 0; i-- { cur := prev.next[i] for ; cur != nil; cur = cur.next[i] { cmpRet := sl.keyCmp(cur.key, key) if cmpRet == 0 { return cur } if cmpRet > 0 { break } prev = cur } } return prev.next[0] } func (sl *skipListFunc[K, V]) upperBound(key K) *skipListNode[K, V] { node := sl.lowerBound(key) if node != nil && sl.keyCmp(node.key, key) == 0 { return node.next[0] } return node } // findInsertPoint returns (*node, nil) to the existed node if the key exists, // or (nil, []*node) to the previous nodes if the key doesn't exist func (sl *skipListFunc[K, V]) findInsertPoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { prevs := sl.prevsCache[0:sl.level] prev := &sl.head for i := sl.level - 1; i >= 0; i-- { for cur := prev.next[i]; cur != nil; cur = cur.next[i] { r := sl.keyCmp(cur.key, key) if r == 0 { // The key is already existed, prevs are useless because no new node insertion. // stop searching. return cur, nil } if r > 0 { // All other node in this level must be greater than the key, // search the next level. break } prev = cur } prevs[i] = prev } return nil, prevs } // findRemovePoint finds the node which match the key and it's previous nodes. func (sl *skipListFunc[K, V]) findRemovePoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { prevs := sl.findPrevNodes(key) node := prevs[0].next[0] if node == nil || sl.keyCmp(node.key, key) != 0 { return nil, nil } return node, prevs } func (sl *skipListFunc[K, V]) findPrevNodes(key K) []*skipListNode[K, V] { prevs := sl.prevsCache[0:sl.level] prev := &sl.head for i := sl.level - 1; i >= 0; i-- { for next := prev.next[i]; next != nil; next = next.next[i] { if sl.keyCmp(next.key, key) >= 0 { break } prev = next } prevs[i] = prev } return prevs } ================================================ FILE: core/Clash.Meta/transport/anytls/skiplist/skiplist_newnode.go ================================================ // AUTO GENERATED CODE, DON'T EDIT!!! // EDIT skiplist_newnode_generate.sh accordingly. package skiplist // newSkipListNode creates a new node initialized with specified key, value and next slice. func newSkipListNode[K any, V any](level int, key K, value V) *skipListNode[K, V] { // For nodes with each levels, point their next slice to the nexts array allocated together, // which can reduce 1 memory allocation and improve performance. // // The generics of the golang doesn't support non-type parameters like in C++, // so we have to generate it manually. switch level { case 1: n := struct { head skipListNode[K, V] nexts [1]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 2: n := struct { head skipListNode[K, V] nexts [2]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 3: n := struct { head skipListNode[K, V] nexts [3]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 4: n := struct { head skipListNode[K, V] nexts [4]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 5: n := struct { head skipListNode[K, V] nexts [5]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 6: n := struct { head skipListNode[K, V] nexts [6]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 7: n := struct { head skipListNode[K, V] nexts [7]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 8: n := struct { head skipListNode[K, V] nexts [8]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 9: n := struct { head skipListNode[K, V] nexts [9]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 10: n := struct { head skipListNode[K, V] nexts [10]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 11: n := struct { head skipListNode[K, V] nexts [11]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 12: n := struct { head skipListNode[K, V] nexts [12]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 13: n := struct { head skipListNode[K, V] nexts [13]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 14: n := struct { head skipListNode[K, V] nexts [14]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 15: n := struct { head skipListNode[K, V] nexts [15]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 16: n := struct { head skipListNode[K, V] nexts [16]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 17: n := struct { head skipListNode[K, V] nexts [17]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 18: n := struct { head skipListNode[K, V] nexts [18]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 19: n := struct { head skipListNode[K, V] nexts [19]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 20: n := struct { head skipListNode[K, V] nexts [20]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 21: n := struct { head skipListNode[K, V] nexts [21]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 22: n := struct { head skipListNode[K, V] nexts [22]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 23: n := struct { head skipListNode[K, V] nexts [23]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 24: n := struct { head skipListNode[K, V] nexts [24]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 25: n := struct { head skipListNode[K, V] nexts [25]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 26: n := struct { head skipListNode[K, V] nexts [26]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 27: n := struct { head skipListNode[K, V] nexts [27]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 28: n := struct { head skipListNode[K, V] nexts [28]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 29: n := struct { head skipListNode[K, V] nexts [29]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 30: n := struct { head skipListNode[K, V] nexts [30]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 31: n := struct { head skipListNode[K, V] nexts [31]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 32: n := struct { head skipListNode[K, V] nexts [32]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 33: n := struct { head skipListNode[K, V] nexts [33]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 34: n := struct { head skipListNode[K, V] nexts [34]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 35: n := struct { head skipListNode[K, V] nexts [35]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 36: n := struct { head skipListNode[K, V] nexts [36]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 37: n := struct { head skipListNode[K, V] nexts [37]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 38: n := struct { head skipListNode[K, V] nexts [38]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 39: n := struct { head skipListNode[K, V] nexts [39]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head case 40: n := struct { head skipListNode[K, V] nexts [40]*skipListNode[K, V] }{head: skipListNode[K, V]{key, value, nil}} n.head.next = n.nexts[:] return &n.head } panic("should not reach here") } ================================================ FILE: core/Clash.Meta/transport/anytls/skiplist/types.go ================================================ package skiplist // Signed is a constraint that permits any signed integer type. // If future releases of Go add new predeclared signed integer types, // this constraint will be modified to include them. type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } // Unsigned is a constraint that permits any unsigned integer type. // If future releases of Go add new predeclared unsigned integer types, // this constraint will be modified to include them. type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr } // Integer is a constraint that permits any integer type. // If future releases of Go add new predeclared integer types, // this constraint will be modified to include them. type Integer interface { Signed | Unsigned } // Float is a constraint that permits any floating-point type. // If future releases of Go add new predeclared floating-point types, // this constraint will be modified to include them. type Float interface { ~float32 | ~float64 } // Ordered is a constraint that permits any ordered type: any type // that supports the operators < <= >= >. // If future releases of Go add new ordered types, // this constraint will be modified to include them. type Ordered interface { Integer | Float | ~string } // Numeric is a constraint that permits any numeric type. type Numeric interface { Integer | Float } // LessFn is a function that returns whether 'a' is less than 'b'. type LessFn[T any] func(a, b T) bool // CompareFn is a 3 way compare function that // returns 1 if a > b, // returns 0 if a == b, // returns -1 if a < b. type CompareFn[T any] func(a, b T) int // HashFn is a function that returns the hash of 't'. type HashFn[T any] func(t T) uint64 // Equals wraps the '==' operator for comparable types. func Equals[T comparable](a, b T) bool { return a == b } // Less wraps the '<' operator for ordered types. func Less[T Ordered](a, b T) bool { return a < b } // OrderedCompare provide default CompareFn for ordered types. func OrderedCompare[T Ordered](a, b T) int { if a < b { return -1 } if a > b { return 1 } return 0 } ================================================ FILE: core/Clash.Meta/transport/anytls/util/deadline.go ================================================ package util import ( "sync" "time" ) func NewDeadlineWatcher(ddl time.Duration, timeOut func()) (done func()) { t := time.NewTimer(ddl) closeCh := make(chan struct{}) go func() { defer t.Stop() select { case <-closeCh: case <-t.C: timeOut() } }() var once sync.Once return func() { once.Do(func() { close(closeCh) }) } } ================================================ FILE: core/Clash.Meta/transport/anytls/util/routine.go ================================================ package util import ( "context" "runtime/debug" "time" "github.com/metacubex/mihomo/log" ) func StartRoutine(ctx context.Context, d time.Duration, f func()) { go func() { defer func() { if r := recover(); r != nil { log.Errorln("[BUG] %v %s", r, string(debug.Stack())) } }() for { time.Sleep(d) f() select { case <-ctx.Done(): return default: } } }() } ================================================ FILE: core/Clash.Meta/transport/anytls/util/string_map.go ================================================ package util import ( "strings" ) type StringMap map[string]string func (s StringMap) ToBytes() []byte { var lines []string for k, v := range s { lines = append(lines, k+"="+v) } return []byte(strings.Join(lines, "\n")) } func StringMapFromBytes(b []byte) StringMap { var m = make(StringMap) var lines = strings.Split(string(b), "\n") for _, line := range lines { v := strings.SplitN(line, "=", 2) if len(v) == 2 { m[v[0]] = v[1] } } return m } ================================================ FILE: core/Clash.Meta/transport/anytls/util/type.go ================================================ package util import ( "context" "net" ) type DialOutFunc func(ctx context.Context) (net.Conn, error) ================================================ FILE: core/Clash.Meta/transport/gost-plugin/websocket.go ================================================ package gost import ( "context" "net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" "github.com/metacubex/smux" "github.com/metacubex/tls" ) // Option is options of gost websocket type Option struct { Host string Port string Path string Headers map[string]string TLS bool ECHConfig *ech.Config SkipCertVerify bool Fingerprint string Certificate string PrivateKey string Mux bool } // muxConn is a wrapper around smux.Stream that also closes the session when closed type muxConn struct { net.Conn session *smux.Session } func (m *muxConn) Close() error { streamErr := m.Conn.Close() sessionErr := m.session.Close() // Return stream error if there is one, otherwise return session error if streamErr != nil { return streamErr } return sessionErr } // NewGostWebsocket return a gost websocket func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.Conn, error) { header := http.Header{} for k, v := range option.Headers { header.Add(k, v) } config := &vmess.WebsocketConfig{ Host: option.Host, Port: option.Port, Path: option.Path, ECHConfig: option.ECHConfig, Headers: header, } var err error if option.TLS { config.TLS = true config.TLSConfig, err = ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: option.Host, InsecureSkipVerify: option.SkipCertVerify, NextProtos: []string{"http/1.1"}, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } if host := config.Headers.Get("Host"); host != "" { config.TLSConfig.ServerName = host } } conn, err = vmess.StreamWebsocketConn(ctx, conn, config) if err != nil { return nil, err } if option.Mux { config := smux.DefaultConfig() config.KeepAliveDisabled = true session, err := smux.Client(conn, config) if err != nil { return nil, err } stream, err := session.OpenStream() if err != nil { session.Close() return nil, err } return &muxConn{ Conn: stream, session: session, }, nil } return conn, nil } ================================================ FILE: core/Clash.Meta/transport/gun/gun.go ================================================ // Modified from: https://github.com/Qv2ray/gun-lite // License: MIT package gun import ( "context" "encoding/binary" "errors" "fmt" "io" "net" "net/url" "strings" "sync" "sync/atomic" "time" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/httputils" "github.com/metacubex/mihomo/common/pool" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" ) const ( Http2NextProtoTLS = "h2" ) var ( ErrInvalidLength = errors.New("invalid length") ErrSmallBuffer = errors.New("buffer too small") ) var defaultHeader = http.Header{ "Content-Type": []string{"application/grpc"}, "User-Agent": []string{"grpc-go/1.36.0"}, } type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error) type Conn struct { initFn func(addr *httputils.NetAddr) (io.ReadCloser, error) writer io.Writer // writer must not nil closer io.Closer httputils.NetAddr initOnce sync.Once initErr error reader io.ReadCloser remain int closeMutex sync.Mutex closed bool onClose func() // deadlines deadline *time.Timer } type Config struct { ServiceName string UserAgent string Host string PingInterval int } func (g *Conn) initReader() { reader, err := g.initFn(&g.NetAddr) if err != nil { g.initErr = err if closer, ok := g.writer.(io.Closer); ok { closer.Close() } return } g.closeMutex.Lock() defer g.closeMutex.Unlock() if g.closed { // if g.Close() be called between g.initFn(), direct close the initFn returned reader _ = reader.Close() g.initErr = net.ErrClosed return } g.reader = reader } func (g *Conn) Init() error { g.initOnce.Do(g.initReader) return g.initErr } func (g *Conn) Read(b []byte) (n int, err error) { if err = g.Init(); err != nil { return } return g.read(b) } func (g *Conn) read(b []byte) (n int, err error) { if g.remain > 0 { size := g.remain if len(b) < size { size = len(b) } n, err = g.reader.Read(b[:size]) g.remain -= n return } // 0x00 grpclength(uint32) 0x0A uleb128 payload var discard [6]byte _, err = io.ReadFull(g.reader, discard[:]) if err != nil { if err == io.ErrUnexpectedEOF { err = io.EOF } return 0, err } protobufPayloadLen, err := ReadUVariant(g.reader) if err != nil { return 0, ErrInvalidLength } g.remain = int(protobufPayloadLen) return g.read(b) } func (g *Conn) Write(b []byte) (n int, err error) { dataLen := len(b) varLen := UVarintLen(uint64(dataLen)) buf := pool.Get(5 + 1 + varLen + dataLen) defer pool.Put(buf) _ = buf[6] // bounds check hint to compiler buf[0] = 0x00 binary.BigEndian.PutUint32(buf[1:5], uint32(1+varLen+dataLen)) buf[5] = 0x0A binary.PutUvarint(buf[6:], uint64(dataLen)) copy(buf[6+varLen:], b) _, err = g.writer.Write(buf) if err == io.ErrClosedPipe { if initErr := g.Init(); initErr != nil { err = initErr } } if flusher, ok := g.writer.(http.Flusher); ok { flusher.Flush() } return len(b), err } func (g *Conn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() dataLen := buffer.Len() varLen := UVarintLen(uint64(dataLen)) header := buffer.ExtendHeader(6 + varLen) _ = header[6] // bounds check hint to compiler header[0] = 0x00 binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen)) header[5] = 0x0A binary.PutUvarint(header[6:], uint64(dataLen)) _, err := g.writer.Write(buffer.Bytes()) if err == io.ErrClosedPipe { if initErr := g.Init(); initErr != nil { err = initErr } } if flusher, ok := g.writer.(http.Flusher); ok { flusher.Flush() } return err } func (g *Conn) FrontHeadroom() int { return 6 + binary.MaxVarintLen64 } func (g *Conn) Close() error { g.closeMutex.Lock() defer g.closeMutex.Unlock() if g.closed { return nil } g.closed = true var errorArr []error if closer, ok := g.writer.(io.Closer); ok { if err := closer.Close(); err != nil { errorArr = append(errorArr, err) } } if reader := g.reader; reader != nil { if err := reader.Close(); err != nil { errorArr = append(errorArr, err) } } if closer := g.closer; closer != nil { if err := closer.Close(); err != nil { errorArr = append(errorArr, err) } } if g.onClose != nil { g.onClose() } return errors.Join(errorArr...) } func (g *Conn) SetReadDeadline(t time.Time) error { return g.SetDeadline(t) } func (g *Conn) SetWriteDeadline(t time.Time) error { return g.SetDeadline(t) } func (g *Conn) SetDeadline(t time.Time) error { if t.IsZero() { if g.deadline != nil { g.deadline.Stop() g.deadline = nil } return nil } d := time.Until(t) if g.deadline != nil { g.deadline.Reset(d) return nil } g.deadline = time.AfterFunc(d, func() { g.Close() }) return nil } type Transport struct { transport *http.Transport cfg *Config ctx context.Context cancel context.CancelFunc closeOnce sync.Once count atomic.Int64 } func (t *Transport) Close() error { t.closeOnce.Do(func() { t.cancel() httputils.CloseTransport(t.transport) }) return nil } func NewTransport(dialFn DialFn, tlsConfig *vmess.TLSConfig, gunCfg *Config) *Transport { dialFunc := func(ctx context.Context, network, addr string) (net.Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) defer cancel() pconn, err := dialFn(ctx, network, addr) if err != nil { return nil, err } if tlsConfig == nil { return pconn, nil } conn, err := vmess.StreamTLSConn(ctx, pconn, tlsConfig) if err != nil { _ = pconn.Close() return nil, err } if tlsConfig.Reality == nil { // reality doesn't return the negotiated ALPN state := tlsC.GetTLSConnectionState(conn) if p := state.NegotiatedProtocol; p != Http2NextProtoTLS { _ = conn.Close() return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, Http2NextProtoTLS) } } return conn, nil } // use h2c mode to disallow the net/http fallback to http1.1 protocols := new(http.Protocols) protocols.SetUnencryptedHTTP2(true) transport := &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { wrapped, err := dialFunc(ctx, network, addr) if err != nil { return nil, err } return wrapped, nil }, Protocols: protocols, DisableCompression: true, HTTP2: &http.HTTP2Config{ SendPingTimeout: time.Duration(gunCfg.PingInterval) * time.Second, // If zero, no health check is performed, PingTimeout: 0, }, } ctx, cancel := context.WithCancel(context.Background()) wrap := &Transport{ transport: transport, cfg: gunCfg, ctx: ctx, cancel: cancel, } return wrap } func ServiceNameToPath(serviceName string) string { if strings.HasPrefix(serviceName, "/") { // custom paths return serviceName } return "/" + serviceName + "/Tun" } func (t *Transport) Dial() (net.Conn, error) { serviceName := "GunService" if t.cfg.ServiceName != "" { serviceName = t.cfg.ServiceName } path := ServiceNameToPath(serviceName) reader, writer := io.Pipe() header := defaultHeader.Clone() if t.cfg.UserAgent != "" { header.Set("User-Agent", t.cfg.UserAgent) } request := &http.Request{ Method: http.MethodPost, Body: reader, URL: &url.URL{ Scheme: "https", Host: t.cfg.Host, Path: path, // for unescape path Opaque: "//" + t.cfg.Host + path, }, Proto: "HTTP/2", ProtoMajor: 2, ProtoMinor: 0, Header: header, } request = request.WithContext(t.ctx) initStarted := make(chan struct{}) conn := &Conn{ initFn: func(addr *httputils.NetAddr) (io.ReadCloser, error) { close(initStarted) request = request.WithContext(httputils.NewAddrContext(addr, request.Context())) response, err := t.transport.RoundTrip(request) if err != nil { return nil, err } return response.Body, nil }, writer: writer, } t.count.Add(1) conn.onClose = func() { t.count.Add(-1) } go conn.Init() // ensure conn.initOnce.Do has been called before return // prevent the race caused by the return side immediately calling conn.Close <-initStarted return conn, nil } type Client struct { mutex sync.Mutex maxConnections int minStreams int maxStreams int transports []*Transport maker func() *Transport } func NewClient(maker func() *Transport, maxConnections, minStreams, maxStreams int) *Client { if maxConnections == 0 && minStreams == 0 && maxStreams == 0 { maxConnections = 1 } return &Client{ maxConnections: maxConnections, minStreams: minStreams, maxStreams: maxStreams, maker: maker, } } func (c *Client) Dial() (net.Conn, error) { return c.getTransport().Dial() } func (c *Client) Close() error { c.mutex.Lock() defer c.mutex.Unlock() var errs []error for _, t := range c.transports { if err := t.Close(); err != nil { errs = append(errs, err) } } c.transports = nil return errors.Join(errs...) } func (c *Client) getTransport() *Transport { c.mutex.Lock() defer c.mutex.Unlock() var transport *Transport for _, t := range c.transports { if transport == nil || t.count.Load() < transport.count.Load() { transport = t } } if transport == nil { return c.newTransportLocked() } numStreams := int(transport.count.Load()) if numStreams == 0 { return transport } if c.maxConnections > 0 { if len(c.transports) >= c.maxConnections || numStreams < c.minStreams { return transport } } else { if c.maxStreams > 0 && numStreams < c.maxStreams { return transport } } return c.newTransportLocked() } func (c *Client) newTransportLocked() *Transport { transport := c.maker() c.transports = append(c.transports, transport) return transport } func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, gunCfg *Config) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil } transport := NewTransport(dialFn, tlsConfig, gunCfg) c, err := transport.Dial() if err != nil { return nil, err } if c, ok := c.(*Conn); ok { // The incoming net.Conn should be closed synchronously with the generated gun.Conn c.closer = conn } return c, nil } ================================================ FILE: core/Clash.Meta/transport/gun/server.go ================================================ package gun import ( "fmt" "io" "net" "strings" "sync" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/httputils" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/http" ) type ServerOption struct { ServiceName string ConnHandler func(conn net.Conn) HttpHandler http.Handler } func NewServerHandler(options ServerOption) http.Handler { path := ServiceNameToPath(options.ServiceName) connHandler := options.ConnHandler httpHandler := options.HttpHandler if httpHandler == nil { httpHandler = http.NewServeMux() } return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.URL.Path == path && request.Method == http.MethodPost && strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") { writer.Header().Set("Content-Type", "application/grpc") writer.Header().Set("TE", "trailers") writer.WriteHeader(http.StatusOK) conn := &Conn{ initFn: func(addr *httputils.NetAddr) (io.ReadCloser, error) { httputils.SetAddrFromRequest(addr, request) return h2RequestBodyWrapper{request.Body}, nil }, writer: writer, } _ = conn.Init() wrapper := &h2ConnWrapper{ // gun.Conn can't correct handle ReadDeadline // so call N.NewDeadlineConn to add a safe wrapper ExtendedConn: N.NewDeadlineConn(conn), } connHandler(wrapper) wrapper.CloseWrapper() return } httpHandler.ServeHTTP(writer, request) }) } // h2RequestBodyWrapper used to conceal the h2-special typed error before return to caller type h2RequestBodyWrapper struct { io.ReadCloser } func (r h2RequestBodyWrapper) Read(p []byte) (n int, err error) { n, err = r.ReadCloser.Read(p) if err != nil && err != io.EOF { err = fmt.Errorf("h2: %s", err.Error()) } return } // h2ConnWrapper used to avoid "panic: Write called after Handler finished" for gun.Conn type h2ConnWrapper struct { N.ExtendedConn access sync.Mutex closed bool } func (w *h2ConnWrapper) 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 *h2ConnWrapper) 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 *h2ConnWrapper) CloseWrapper() { w.access.Lock() defer w.access.Unlock() w.closed = true } func (w *h2ConnWrapper) Upstream() any { return w.ExtendedConn } ================================================ FILE: core/Clash.Meta/transport/gun/utils.go ================================================ package gun import ( "encoding/binary" "io" ) type stubByteReader struct { io.Reader } func (r stubByteReader) ReadByte() (byte, error) { var b [1]byte var n int var err error for n == 0 && err == nil { n, err = r.Read(b[:]) } if n == 1 && err == io.EOF { err = nil } return b[0], err } func ToByteReader(reader io.Reader) io.ByteReader { if byteReader, ok := reader.(io.ByteReader); ok { return byteReader } return &stubByteReader{reader} } func ReadUVariant(reader io.Reader) (uint64, error) { return binary.ReadUvarint(ToByteReader(reader)) } func UVarintLen(x uint64) int { switch { case x < 1<<(7*1): return 1 case x < 1<<(7*2): return 2 case x < 1<<(7*3): return 3 case x < 1<<(7*4): return 4 case x < 1<<(7*5): return 5 case x < 1<<(7*6): return 6 case x < 1<<(7*7): return 7 case x < 1<<(7*8): return 8 case x < 1<<(7*9): return 9 default: return 10 } } ================================================ FILE: core/Clash.Meta/transport/hysteria/congestion/brutal.go ================================================ package congestion import ( "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" "time" ) const ( initMaxDatagramSize = 1252 pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample minSampleCount = 50 minAckRate = 0.8 ) var _ congestion.CongestionControlEx = &BrutalSender{} type BrutalSender struct { rttStats congestion.RTTStatsProvider bps congestion.ByteCount maxDatagramSize congestion.ByteCount pacer *pacer pktInfoSlots [pktInfoSlotCount]pktInfo ackRate float64 } type pktInfo struct { Timestamp int64 AckCount uint64 LossCount uint64 } func NewBrutalSender(bps congestion.ByteCount) *BrutalSender { bs := &BrutalSender{ bps: bps, maxDatagramSize: initMaxDatagramSize, ackRate: 1, } bs.pacer = newPacer(func() congestion.ByteCount { return congestion.ByteCount(float64(bs.bps) / bs.ackRate) }) return bs } func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) { b.rttStats = rttStats } func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time { return b.pacer.TimeUntilSend() } func (b *BrutalSender) HasPacingBudget(now monotime.Time) bool { return b.pacer.Budget(now) >= b.maxDatagramSize } func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { return bytesInFlight < b.GetCongestionWindow() } func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { rtt := maxDuration(b.rttStats.LatestRTT(), b.rttStats.SmoothedRTT()) if rtt <= 0 { return 10240 } return congestion.ByteCount(float64(b.bps) * rtt.Seconds() * 1.5 / b.ackRate) } func (b *BrutalSender) OnPacketSent(sentTime monotime.Time, bytesInFlight congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool) { b.pacer.SentPacket(sentTime, bytes) } func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, priorInFlight congestion.ByteCount, eventTime monotime.Time) { // Stub } func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, priorInFlight congestion.ByteCount) { // Stub } func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { currentTimestamp := int64(time.Duration(eventTime) / time.Second) slot := currentTimestamp % pktInfoSlotCount if b.pktInfoSlots[slot].Timestamp == currentTimestamp { b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets)) b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets)) } else { // uninitialized slot or too old, reset b.pktInfoSlots[slot].Timestamp = currentTimestamp b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets)) b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets)) } b.updateAckRate(currentTimestamp) } func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { b.maxDatagramSize = size b.pacer.SetMaxDatagramSize(size) } func (b *BrutalSender) updateAckRate(currentTimestamp int64) { minTimestamp := currentTimestamp - pktInfoSlotCount var ackCount, lossCount uint64 for _, info := range b.pktInfoSlots { if info.Timestamp < minTimestamp { continue } ackCount += info.AckCount lossCount += info.LossCount } if ackCount+lossCount < minSampleCount { b.ackRate = 1 return } rate := float64(ackCount) / float64(ackCount+lossCount) if rate < minAckRate { b.ackRate = minAckRate return } b.ackRate = rate } func (b *BrutalSender) InSlowStart() bool { return false } func (b *BrutalSender) InRecovery() bool { return false } func (b *BrutalSender) MaybeExitSlowStart() {} func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} func maxDuration(a, b time.Duration) time.Duration { if a > b { return a } return b } ================================================ FILE: core/Clash.Meta/transport/hysteria/congestion/pacer.go ================================================ package congestion import ( "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" "math" "time" ) const ( maxBurstPackets = 10 minPacingDelay = time.Millisecond ) // The pacer implements a token bucket pacing algorithm. type pacer struct { budgetAtLastSent congestion.ByteCount maxDatagramSize congestion.ByteCount lastSentTime monotime.Time getBandwidth func() congestion.ByteCount // in bytes/s } func newPacer(getBandwidth func() congestion.ByteCount) *pacer { p := &pacer{ budgetAtLastSent: maxBurstPackets * initMaxDatagramSize, maxDatagramSize: initMaxDatagramSize, getBandwidth: getBandwidth, } return p } func (p *pacer) SentPacket(sendTime monotime.Time, size congestion.ByteCount) { budget := p.Budget(sendTime) if size > budget { p.budgetAtLastSent = 0 } else { p.budgetAtLastSent = budget - size } p.lastSentTime = sendTime } func (p *pacer) Budget(now monotime.Time) congestion.ByteCount { if p.lastSentTime.IsZero() { return p.maxBurstSize() } budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 return minByteCount(p.maxBurstSize(), budget) } func (p *pacer) maxBurstSize() congestion.ByteCount { return maxByteCount( congestion.ByteCount((minPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9, maxBurstPackets*p.maxDatagramSize, ) } // TimeUntilSend returns when the next packet should be sent. // It returns the zero value of monotime.Time if a packet can be sent immediately. func (p *pacer) TimeUntilSend() monotime.Time { if p.budgetAtLastSent >= p.maxDatagramSize { return monotime.Time(0) } return p.lastSentTime.Add(maxDuration( minPacingDelay, time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/ float64(p.getBandwidth())))*time.Nanosecond, )) } func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) { p.maxDatagramSize = s } func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount { if a < b { return b } return a } func minByteCount(a, b congestion.ByteCount) congestion.ByteCount { if a < b { return a } return b } ================================================ FILE: core/Clash.Meta/transport/hysteria/conns/faketcp/LICENSE ================================================ Grabbed from https://github.com/xtaci/tcpraw with modifications ================================================ FILE: core/Clash.Meta/transport/hysteria/conns/faketcp/obfs.go ================================================ package faketcp import ( "github.com/metacubex/mihomo/transport/hysteria/obfs" "net" "sync" "syscall" "time" ) const udpBufferSize = 65535 type ObfsFakeTCPConn struct { orig *TCPConn obfs obfs.Obfuscator readBuf []byte readMutex sync.Mutex writeBuf []byte writeMutex sync.Mutex } func NewObfsFakeTCPConn(orig *TCPConn, obfs obfs.Obfuscator) *ObfsFakeTCPConn { return &ObfsFakeTCPConn{ orig: orig, obfs: obfs, readBuf: make([]byte, udpBufferSize), writeBuf: make([]byte, udpBufferSize), } } func (c *ObfsFakeTCPConn) ReadFrom(p []byte) (int, net.Addr, error) { for { c.readMutex.Lock() n, addr, err := c.orig.ReadFrom(c.readBuf) if n <= 0 { c.readMutex.Unlock() return 0, addr, err } newN := c.obfs.Deobfuscate(c.readBuf[:n], p) c.readMutex.Unlock() if newN > 0 { // Valid packet return newN, addr, err } else if err != nil { // Not valid and orig.ReadFrom had some error return 0, addr, err } } } func (c *ObfsFakeTCPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { c.writeMutex.Lock() bn := c.obfs.Obfuscate(p, c.writeBuf) _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) c.writeMutex.Unlock() if err != nil { return 0, err } else { return len(p), nil } } func (c *ObfsFakeTCPConn) Close() error { return c.orig.Close() } func (c *ObfsFakeTCPConn) LocalAddr() net.Addr { return c.orig.LocalAddr() } func (c *ObfsFakeTCPConn) SetDeadline(t time.Time) error { return c.orig.SetDeadline(t) } func (c *ObfsFakeTCPConn) SetReadDeadline(t time.Time) error { return c.orig.SetReadDeadline(t) } func (c *ObfsFakeTCPConn) SetWriteDeadline(t time.Time) error { return c.orig.SetWriteDeadline(t) } func (c *ObfsFakeTCPConn) SetReadBuffer(bytes int) error { return c.orig.SetReadBuffer(bytes) } func (c *ObfsFakeTCPConn) SetWriteBuffer(bytes int) error { return c.orig.SetWriteBuffer(bytes) } func (c *ObfsFakeTCPConn) SyscallConn() (syscall.RawConn, error) { return c.orig.SyscallConn() } ================================================ FILE: core/Clash.Meta/transport/hysteria/conns/faketcp/tcp_linux.go ================================================ //go:build linux && !no_fake_tcp // +build linux,!no_fake_tcp package faketcp import ( "crypto/rand" "encoding/binary" "errors" "fmt" "io" "io/ioutil" "net" "sync" "sync/atomic" "syscall" "time" "github.com/coreos/go-iptables/iptables" "github.com/metacubex/gopacket" "github.com/metacubex/gopacket/layers" "github.com/metacubex/mihomo/component/dialer" ) var ( errOpNotImplemented = errors.New("operation not implemented") errTimeout = errors.New("timeout") expire = time.Minute ) // a message from NIC type message struct { bts []byte addr net.Addr } // a tcp flow information of a connection pair type tcpFlow struct { conn *net.TCPConn // the related system TCP connection of this flow handle *net.IPConn // the handle to send packets seq uint32 // TCP sequence number ack uint32 // TCP acknowledge number networkLayer gopacket.SerializableLayer // network layer header for tx ts time.Time // last packet incoming time buf gopacket.SerializeBuffer // a buffer for write tcpHeader layers.TCP } // TCPConn defines a TCP-packet oriented connection type TCPConn struct { die chan struct{} dieOnce sync.Once // the main golang sockets tcpconn *net.TCPConn // from net.Dial listener *net.TCPListener // from net.Listen // handles handles []*net.IPConn // packets captured from all related NICs will be delivered to this channel chMessage chan message // all TCP flows flowTable map[string]*tcpFlow flowsLock sync.Mutex // iptables iptables *iptables.IPTables iprule []string ip6tables *iptables.IPTables ip6rule []string // deadlines readDeadline atomic.Value writeDeadline atomic.Value // serialization opts gopacket.SerializeOptions } // lockflow locks the flow table and apply function `f` to the entry, and create one if not exist func (conn *TCPConn) lockflow(addr net.Addr, f func(e *tcpFlow)) { key := addr.String() conn.flowsLock.Lock() e := conn.flowTable[key] if e == nil { // entry first visit e = new(tcpFlow) e.ts = time.Now() e.buf = gopacket.NewSerializeBuffer() } f(e) conn.flowTable[key] = e conn.flowsLock.Unlock() } // clean expired flows func (conn *TCPConn) cleaner() { ticker := time.NewTicker(time.Minute) select { case <-conn.die: return case <-ticker.C: conn.flowsLock.Lock() for k, v := range conn.flowTable { if time.Now().Sub(v.ts) > expire { if v.conn != nil { setTTL(v.conn, 64) v.conn.Close() } delete(conn.flowTable, k) } } conn.flowsLock.Unlock() } } // captureFlow capture every inbound packets based on rules of BPF func (conn *TCPConn) captureFlow(handle *net.IPConn, port int) { buf := make([]byte, 2048) opt := gopacket.DecodeOptions{NoCopy: true, Lazy: true} for { n, addr, err := handle.ReadFromIP(buf) if err != nil { return } // try decoding TCP frame from buf[:n] packet := gopacket.NewPacket(buf[:n], layers.LayerTypeTCP, opt) transport := packet.TransportLayer() tcp, ok := transport.(*layers.TCP) if !ok { continue } // port filtering if int(tcp.DstPort) != port { continue } // address building var src net.TCPAddr src.IP = addr.IP src.Port = int(tcp.SrcPort) var orphan bool // flow maintaince conn.lockflow(&src, func(e *tcpFlow) { if e.conn == nil { // make sure it's related to net.TCPConn orphan = true // mark as orphan if it's not related net.TCPConn } // to keep track of TCP header related to this source e.ts = time.Now() if tcp.ACK { e.seq = tcp.Ack } if tcp.SYN { e.ack = tcp.Seq + 1 } if tcp.PSH { if e.ack == tcp.Seq { e.ack = tcp.Seq + uint32(len(tcp.Payload)) } } e.handle = handle }) // push data if it's not orphan if !orphan && tcp.PSH { payload := make([]byte, len(tcp.Payload)) copy(payload, tcp.Payload) select { case conn.chMessage <- message{payload, &src}: case <-conn.die: return } } } } // ReadFrom implements the PacketConn ReadFrom method. func (conn *TCPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { var timer *time.Timer var deadline <-chan time.Time if d, ok := conn.readDeadline.Load().(time.Time); ok && !d.IsZero() { timer = time.NewTimer(time.Until(d)) defer timer.Stop() deadline = timer.C } select { case <-deadline: return 0, nil, errTimeout case <-conn.die: return 0, nil, io.EOF case packet := <-conn.chMessage: n = copy(p, packet.bts) return n, packet.addr, nil } } // WriteTo implements the PacketConn WriteTo method. func (conn *TCPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { var deadline <-chan time.Time if d, ok := conn.writeDeadline.Load().(time.Time); ok && !d.IsZero() { timer := time.NewTimer(time.Until(d)) defer timer.Stop() deadline = timer.C } select { case <-deadline: return 0, errTimeout case <-conn.die: return 0, io.EOF default: raddr, err := net.ResolveTCPAddr("tcp", addr.String()) if err != nil { return 0, err } var lport int if conn.tcpconn != nil { lport = conn.tcpconn.LocalAddr().(*net.TCPAddr).Port } else { lport = conn.listener.Addr().(*net.TCPAddr).Port } conn.lockflow(addr, func(e *tcpFlow) { // if the flow doesn't have handle , assume this packet has lost, without notification if e.handle == nil { n = len(p) return } // build tcp header with local and remote port e.tcpHeader.SrcPort = layers.TCPPort(lport) e.tcpHeader.DstPort = layers.TCPPort(raddr.Port) binary.Read(rand.Reader, binary.LittleEndian, &e.tcpHeader.Window) e.tcpHeader.Window |= 0x8000 // make sure it's larger than 32768 e.tcpHeader.Ack = e.ack e.tcpHeader.Seq = e.seq e.tcpHeader.PSH = true e.tcpHeader.ACK = true // build IP header with src & dst ip for TCP checksum if raddr.IP.To4() != nil { ip := &layers.IPv4{ Protocol: layers.IPProtocolTCP, SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To4(), DstIP: raddr.IP.To4(), } e.tcpHeader.SetNetworkLayerForChecksum(ip) } else { ip := &layers.IPv6{ NextHeader: layers.IPProtocolTCP, SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To16(), DstIP: raddr.IP.To16(), } e.tcpHeader.SetNetworkLayerForChecksum(ip) } e.buf.Clear() gopacket.SerializeLayers(e.buf, conn.opts, &e.tcpHeader, gopacket.Payload(p)) if conn.tcpconn != nil { _, err = e.handle.Write(e.buf.Bytes()) } else { _, err = e.handle.WriteToIP(e.buf.Bytes(), &net.IPAddr{IP: raddr.IP}) } // increase seq in flow e.seq += uint32(len(p)) n = len(p) }) } return } // Close closes the connection. func (conn *TCPConn) Close() error { var err error conn.dieOnce.Do(func() { // signal closing close(conn.die) // close all established tcp connections if conn.tcpconn != nil { // client setTTL(conn.tcpconn, 64) err = conn.tcpconn.Close() } else if conn.listener != nil { err = conn.listener.Close() // server conn.flowsLock.Lock() for k, v := range conn.flowTable { if v.conn != nil { setTTL(v.conn, 64) v.conn.Close() } delete(conn.flowTable, k) } conn.flowsLock.Unlock() } // close handles for k := range conn.handles { conn.handles[k].Close() } // delete iptable if conn.iptables != nil { conn.iptables.Delete("filter", "OUTPUT", conn.iprule...) } if conn.ip6tables != nil { conn.ip6tables.Delete("filter", "OUTPUT", conn.ip6rule...) } }) return err } // LocalAddr returns the local network address. func (conn *TCPConn) LocalAddr() net.Addr { if conn.tcpconn != nil { return conn.tcpconn.LocalAddr() } else if conn.listener != nil { return conn.listener.Addr() } return nil } // SetDeadline implements the Conn SetDeadline method. func (conn *TCPConn) SetDeadline(t time.Time) error { if err := conn.SetReadDeadline(t); err != nil { return err } if err := conn.SetWriteDeadline(t); err != nil { return err } return nil } // SetReadDeadline implements the Conn SetReadDeadline method. func (conn *TCPConn) SetReadDeadline(t time.Time) error { conn.readDeadline.Store(t) return nil } // SetWriteDeadline implements the Conn SetWriteDeadline method. func (conn *TCPConn) SetWriteDeadline(t time.Time) error { conn.writeDeadline.Store(t) return nil } // SetDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header. func (conn *TCPConn) SetDSCP(dscp int) error { for k := range conn.handles { if err := setDSCP(conn.handles[k], dscp); err != nil { return err } } return nil } // SetReadBuffer sets the size of the operating system's receive buffer associated with the connection. func (conn *TCPConn) SetReadBuffer(bytes int) error { var err error for k := range conn.handles { if err := conn.handles[k].SetReadBuffer(bytes); err != nil { return err } } return err } // SetWriteBuffer sets the size of the operating system's transmit buffer associated with the connection. func (conn *TCPConn) SetWriteBuffer(bytes int) error { var err error for k := range conn.handles { if err := conn.handles[k].SetWriteBuffer(bytes); err != nil { return err } } return err } func (conn *TCPConn) SyscallConn() (syscall.RawConn, error) { if len(conn.handles) == 0 { return nil, errors.New("no handles") // How is it possible? } return conn.handles[0].SyscallConn() } // Dial connects to the remote TCP port, // and returns a single packet-oriented connection func Dial(network, address string) (*TCPConn, error) { // init gopacket.layers layers.Init() // remote address resolve raddr, err := net.ResolveTCPAddr(network, address) if err != nil { return nil, err } var lTcpAddr *net.TCPAddr var lIpAddr *net.IPAddr rAddrPort := raddr.AddrPort() ifaceName := dialer.DefaultInterface.Load() if ifaceName == "" { if finder := dialer.DefaultInterfaceFinder.Load(); finder != nil { ifaceName = finder.FindInterfaceName(rAddrPort.Addr().Unmap()) } } if len(ifaceName) > 0 { addr, err := dialer.LookupLocalAddrFromIfaceName(ifaceName, network, rAddrPort.Addr(), int(rAddrPort.Port())) if err != nil { return nil, err } lTcpAddr = addr.(*net.TCPAddr) lIpAddr = &net.IPAddr{IP: lTcpAddr.IP} } // AF_INET handle, err := net.DialIP("ip:tcp", lIpAddr, &net.IPAddr{IP: raddr.IP}) if err != nil { return nil, err } // create an established tcp connection // will hack this tcp connection for packet transmission tcpconn, err := net.DialTCP(network, lTcpAddr, raddr) if err != nil { return nil, err } // fields conn := new(TCPConn) conn.die = make(chan struct{}) conn.flowTable = make(map[string]*tcpFlow) conn.tcpconn = tcpconn conn.chMessage = make(chan message) conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn }) conn.handles = append(conn.handles, handle) conn.opts = gopacket.SerializeOptions{ FixLengths: true, ComputeChecksums: true, } go conn.captureFlow(handle, tcpconn.LocalAddr().(*net.TCPAddr).Port) go conn.cleaner() // iptables err = setTTL(tcpconn, 1) if err != nil { return nil, err } if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil { rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"} if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { if !exists { if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { conn.iprule = rule conn.iptables = ipt } } } } if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil { rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"} if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { if !exists { if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { conn.ip6rule = rule conn.ip6tables = ipt } } } } // discard everything go io.Copy(ioutil.Discard, tcpconn) return conn, nil } // Listen acts like net.ListenTCP, // and returns a single packet-oriented connection func Listen(network, address string) (*TCPConn, error) { // init gopacket.layers layers.Init() // fields conn := new(TCPConn) conn.flowTable = make(map[string]*tcpFlow) conn.die = make(chan struct{}) conn.chMessage = make(chan message) conn.opts = gopacket.SerializeOptions{ FixLengths: true, ComputeChecksums: true, } // resolve address laddr, err := net.ResolveTCPAddr(network, address) if err != nil { return nil, err } // AF_INET ifaces, err := net.Interfaces() if err != nil { return nil, err } if laddr.IP == nil || laddr.IP.IsUnspecified() { // if address is not specified, capture on all ifaces var lasterr error for _, iface := range ifaces { if addrs, err := iface.Addrs(); err == nil { for _, addr := range addrs { if ipaddr, ok := addr.(*net.IPNet); ok { if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: ipaddr.IP}); err == nil { conn.handles = append(conn.handles, handle) go conn.captureFlow(handle, laddr.Port) } else { lasterr = err } } } } } if len(conn.handles) == 0 { return nil, lasterr } } else { if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: laddr.IP}); err == nil { conn.handles = append(conn.handles, handle) go conn.captureFlow(handle, laddr.Port) } else { return nil, err } } // start listening l, err := net.ListenTCP(network, laddr) if err != nil { return nil, err } conn.listener = l // start cleaner go conn.cleaner() // iptables drop packets marked with TTL = 1 // TODO: what if iptables is not available, the next hop will send back ICMP Time Exceeded, // is this still an acceptable behavior? if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil { rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"} if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { if !exists { if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { conn.iprule = rule conn.iptables = ipt } } } } if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil { rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"} if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { if !exists { if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { conn.ip6rule = rule conn.ip6tables = ipt } } } } // discard everything in original connection go func() { for { tcpconn, err := l.AcceptTCP() if err != nil { return } // if we cannot set TTL = 1, the only thing reasonable is panic if err := setTTL(tcpconn, 1); err != nil { panic(err) } // record net.Conn conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn }) // discard everything go io.Copy(ioutil.Discard, tcpconn) } }() return conn, nil } // setTTL sets the Time-To-Live field on a given connection func setTTL(c *net.TCPConn, ttl int) error { raw, err := c.SyscallConn() if err != nil { return err } addr := c.LocalAddr().(*net.TCPAddr) if addr.IP.To4() == nil { raw.Control(func(fd uintptr) { err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_UNICAST_HOPS, ttl) }) } else { raw.Control(func(fd uintptr) { err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TTL, ttl) }) } return err } // setDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header. func setDSCP(c *net.IPConn, dscp int) error { raw, err := c.SyscallConn() if err != nil { return err } addr := c.LocalAddr().(*net.IPAddr) if addr.IP.To4() == nil { raw.Control(func(fd uintptr) { err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_TCLASS, dscp) }) } else { raw.Control(func(fd uintptr) { err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, dscp<<2) }) } return err } ================================================ FILE: core/Clash.Meta/transport/hysteria/conns/faketcp/tcp_stub.go ================================================ //go:build !linux || no_fake_tcp // +build !linux no_fake_tcp package faketcp import ( "errors" "net" ) type TCPConn struct{ *net.UDPConn } // Dial connects to the remote TCP port, // and returns a single packet-oriented connection func Dial(network, address string) (*TCPConn, error) { return nil, errors.New("faketcp is not supported on this platform") } func Listen(network, address string) (*TCPConn, error) { return nil, errors.New("faketcp is not supported on this platform") } ================================================ FILE: core/Clash.Meta/transport/hysteria/conns/udp/hop.go ================================================ package udp import ( "errors" "net" "strconv" "strings" "sync" "syscall" "time" "github.com/metacubex/mihomo/transport/hysteria/obfs" "github.com/metacubex/mihomo/transport/hysteria/utils" "github.com/metacubex/randv2" ) const ( packetQueueSize = 1024 ) // ObfsUDPHopClientPacketConn is the UDP port-hopping packet connection for client side. // It hops to a different local & server port every once in a while. type ObfsUDPHopClientPacketConn struct { serverAddr net.Addr // Combined udpHopAddr serverAddrs []net.Addr hopInterval time.Duration obfs obfs.Obfuscator connMutex sync.RWMutex prevConn net.PacketConn currentConn net.PacketConn addrIndex int readBufferSize int writeBufferSize int recvQueue chan *udpPacket closeChan chan struct{} closed bool bufPool sync.Pool } type udpHopAddr string func (a *udpHopAddr) Network() string { return "udp-hop" } func (a *udpHopAddr) String() string { return string(*a) } type udpPacket struct { buf []byte n int addr net.Addr } func NewObfsUDPHopClientPacketConn(server string, serverPorts string, hopInterval time.Duration, obfs obfs.Obfuscator, dialer utils.PacketDialer) (net.PacketConn, error) { ports, err := parsePorts(serverPorts) if err != nil { return nil, err } // Resolve the server IP address, then attach the ports to UDP addresses rAddr, err := dialer.RemoteAddr(server) if err != nil { return nil, err } ip, _, err := net.SplitHostPort(rAddr.String()) if err != nil { return nil, err } serverAddrs := make([]net.Addr, len(ports)) for i, port := range ports { serverAddrs[i] = &net.UDPAddr{ IP: net.ParseIP(ip), Port: int(port), } } hopAddr := udpHopAddr(server) conn := &ObfsUDPHopClientPacketConn{ serverAddr: &hopAddr, serverAddrs: serverAddrs, hopInterval: hopInterval, obfs: obfs, addrIndex: randv2.IntN(len(serverAddrs)), recvQueue: make(chan *udpPacket, packetQueueSize), closeChan: make(chan struct{}), bufPool: sync.Pool{ New: func() interface{} { return make([]byte, udpBufferSize) }, }, } curConn, err := dialer.ListenPacket(rAddr) if err != nil { return nil, err } if obfs != nil { conn.currentConn = NewObfsUDPConn(curConn, obfs) } else { conn.currentConn = curConn } go conn.recvRoutine(conn.currentConn) go conn.hopRoutine(dialer, rAddr) if _, ok := conn.currentConn.(syscall.Conn); ok { return &ObfsUDPHopClientPacketConnWithSyscall{conn}, nil } return conn, nil } func (c *ObfsUDPHopClientPacketConn) recvRoutine(conn net.PacketConn) { for { buf := c.bufPool.Get().([]byte) n, addr, err := conn.ReadFrom(buf) if err != nil { return } select { case c.recvQueue <- &udpPacket{buf, n, addr}: default: // Drop the packet if the queue is full c.bufPool.Put(buf) } } } func (c *ObfsUDPHopClientPacketConn) hopRoutine(dialer utils.PacketDialer, rAddr net.Addr) { ticker := time.NewTicker(c.hopInterval) defer ticker.Stop() for { select { case <-ticker.C: c.hop(dialer, rAddr) case <-c.closeChan: return } } } func (c *ObfsUDPHopClientPacketConn) hop(dialer utils.PacketDialer, rAddr net.Addr) { c.connMutex.Lock() defer c.connMutex.Unlock() if c.closed { return } newConn, err := dialer.ListenPacket(rAddr) if err != nil { // Skip this hop if failed to listen return } // Close prevConn, // prevConn <- currentConn // currentConn <- newConn // update addrIndex // // We need to keep receiving packets from the previous connection, // because otherwise there will be packet loss due to the time gap // between we hop to a new port and the server acknowledges this change. if c.prevConn != nil { _ = c.prevConn.Close() // recvRoutine will exit on error } c.prevConn = c.currentConn if c.obfs != nil { c.currentConn = NewObfsUDPConn(newConn, c.obfs) } else { c.currentConn = newConn } // Set buffer sizes if previously set if c.readBufferSize > 0 { _ = trySetPacketConnReadBuffer(c.currentConn, c.readBufferSize) } if c.writeBufferSize > 0 { _ = trySetPacketConnWriteBuffer(c.currentConn, c.writeBufferSize) } go c.recvRoutine(c.currentConn) c.addrIndex = randv2.IntN(len(c.serverAddrs)) } func (c *ObfsUDPHopClientPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { for { select { case p := <-c.recvQueue: /* // Check if the packet is from one of the server addresses for _, addr := range c.serverAddrs { if addr.String() == p.addr.String() { // Copy the packet to the buffer n := copy(b, p.buf[:p.n]) c.bufPool.Put(p.buf) return n, c.serverAddr, nil } } // Drop the packet, continue c.bufPool.Put(p.buf) */ // The above code was causing performance issues when the range is large, // so we skip the check for now. Should probably still check by using a map // or something in the future. n := copy(b, p.buf[:p.n]) c.bufPool.Put(p.buf) return n, c.serverAddr, nil case <-c.closeChan: return 0, nil, net.ErrClosed } // Ignore packets from other addresses } } func (c *ObfsUDPHopClientPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { c.connMutex.RLock() defer c.connMutex.RUnlock() if c.closed { return 0, net.ErrClosed } /* // Check if the address is the server address if addr.String() != c.serverAddr.String() { return 0, net.ErrWriteToConnected } */ // Skip the check for now, always write to the server return c.currentConn.WriteTo(b, c.serverAddrs[c.addrIndex]) } func (c *ObfsUDPHopClientPacketConn) Close() error { c.connMutex.Lock() defer c.connMutex.Unlock() if c.closed { return nil } // Close prevConn and currentConn // Close closeChan to unblock ReadFrom & hopRoutine // Set closed flag to true to prevent double close if c.prevConn != nil { _ = c.prevConn.Close() } err := c.currentConn.Close() close(c.closeChan) c.closed = true c.serverAddrs = nil // For GC return err } func (c *ObfsUDPHopClientPacketConn) LocalAddr() net.Addr { c.connMutex.RLock() defer c.connMutex.RUnlock() return c.currentConn.LocalAddr() } func (c *ObfsUDPHopClientPacketConn) SetReadDeadline(t time.Time) error { // Not supported return nil } func (c *ObfsUDPHopClientPacketConn) SetWriteDeadline(t time.Time) error { // Not supported return nil } func (c *ObfsUDPHopClientPacketConn) SetDeadline(t time.Time) error { err := c.SetReadDeadline(t) if err != nil { return err } return c.SetWriteDeadline(t) } func (c *ObfsUDPHopClientPacketConn) SetReadBuffer(bytes int) error { c.connMutex.Lock() defer c.connMutex.Unlock() c.readBufferSize = bytes if c.prevConn != nil { _ = trySetPacketConnReadBuffer(c.prevConn, bytes) } return trySetPacketConnReadBuffer(c.currentConn, bytes) } func (c *ObfsUDPHopClientPacketConn) SetWriteBuffer(bytes int) error { c.connMutex.Lock() defer c.connMutex.Unlock() c.writeBufferSize = bytes if c.prevConn != nil { _ = trySetPacketConnWriteBuffer(c.prevConn, bytes) } return trySetPacketConnWriteBuffer(c.currentConn, bytes) } func trySetPacketConnReadBuffer(pc net.PacketConn, bytes int) error { sc, ok := pc.(interface { SetReadBuffer(bytes int) error }) if ok { return sc.SetReadBuffer(bytes) } return nil } func trySetPacketConnWriteBuffer(pc net.PacketConn, bytes int) error { sc, ok := pc.(interface { SetWriteBuffer(bytes int) error }) if ok { return sc.SetWriteBuffer(bytes) } return nil } type ObfsUDPHopClientPacketConnWithSyscall struct { *ObfsUDPHopClientPacketConn } func (c *ObfsUDPHopClientPacketConnWithSyscall) SyscallConn() (syscall.RawConn, error) { c.connMutex.RLock() defer c.connMutex.RUnlock() sc, ok := c.currentConn.(syscall.Conn) if !ok { return nil, errors.New("not supported") } return sc.SyscallConn() } // parsePorts parses the multi-port server address and returns the host and ports. // Supports both comma-separated single ports and dash-separated port ranges. // Format: "host:port1,port2-port3,port4" func parsePorts(serverPorts string) (ports []uint16, err error) { portStrs := strings.Split(serverPorts, ",") for _, portStr := range portStrs { if strings.Contains(portStr, "-") { // Port range portRange := strings.Split(portStr, "-") if len(portRange) != 2 { return nil, net.InvalidAddrError("invalid port range") } start, err := strconv.ParseUint(portRange[0], 10, 16) if err != nil { return nil, net.InvalidAddrError("invalid port range") } end, err := strconv.ParseUint(portRange[1], 10, 16) if err != nil { return nil, net.InvalidAddrError("invalid port range") } if start > end { start, end = end, start } for i := start; i <= end; i++ { ports = append(ports, uint16(i)) } } else { // Single port port, err := strconv.ParseUint(portStr, 10, 16) if err != nil { return nil, net.InvalidAddrError("invalid port") } ports = append(ports, uint16(port)) } } if len(ports) == 0 { return nil, net.InvalidAddrError("invalid port") } return ports, nil } ================================================ FILE: core/Clash.Meta/transport/hysteria/conns/udp/obfs.go ================================================ package udp import ( "github.com/metacubex/mihomo/transport/hysteria/obfs" "net" "sync" "time" ) const udpBufferSize = 65535 type ObfsUDPConn struct { orig net.PacketConn obfs obfs.Obfuscator readBuf []byte readMutex sync.Mutex writeBuf []byte writeMutex sync.Mutex } func NewObfsUDPConn(orig net.PacketConn, obfs obfs.Obfuscator) *ObfsUDPConn { return &ObfsUDPConn{ orig: orig, obfs: obfs, readBuf: make([]byte, udpBufferSize), writeBuf: make([]byte, udpBufferSize), } } func (c *ObfsUDPConn) ReadFrom(p []byte) (int, net.Addr, error) { for { c.readMutex.Lock() n, addr, err := c.orig.ReadFrom(c.readBuf) if n <= 0 { c.readMutex.Unlock() return 0, addr, err } newN := c.obfs.Deobfuscate(c.readBuf[:n], p) c.readMutex.Unlock() if newN > 0 { // Valid packet return newN, addr, err } else if err != nil { // Not valid and orig.ReadFrom had some error return 0, addr, err } } } func (c *ObfsUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { c.writeMutex.Lock() bn := c.obfs.Obfuscate(p, c.writeBuf) _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) c.writeMutex.Unlock() if err != nil { return 0, err } else { return len(p), nil } } func (c *ObfsUDPConn) Close() error { return c.orig.Close() } func (c *ObfsUDPConn) LocalAddr() net.Addr { return c.orig.LocalAddr() } func (c *ObfsUDPConn) SetDeadline(t time.Time) error { return c.orig.SetDeadline(t) } func (c *ObfsUDPConn) SetReadDeadline(t time.Time) error { return c.orig.SetReadDeadline(t) } func (c *ObfsUDPConn) SetWriteDeadline(t time.Time) error { return c.orig.SetWriteDeadline(t) } ================================================ FILE: core/Clash.Meta/transport/hysteria/conns/wechat/obfs.go ================================================ package wechat import ( "encoding/binary" "net" "sync" "time" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/hysteria/obfs" "github.com/metacubex/randv2" ) const udpBufferSize = 65535 type ObfsWeChatUDPConn struct { orig net.PacketConn obfs obfs.Obfuscator readBuf []byte readMutex sync.Mutex writeBuf []byte writeMutex sync.Mutex sn uint32 } func NewObfsWeChatUDPConn(orig net.PacketConn, obfs obfs.Obfuscator) *ObfsWeChatUDPConn { log.Infoln("new wechat") return &ObfsWeChatUDPConn{ orig: orig, obfs: obfs, readBuf: make([]byte, udpBufferSize), writeBuf: make([]byte, udpBufferSize), sn: randv2.Uint32() & 0xFFFF, } } func (c *ObfsWeChatUDPConn) ReadFrom(p []byte) (int, net.Addr, error) { for { c.readMutex.Lock() n, addr, err := c.orig.ReadFrom(c.readBuf) if n <= 13 { c.readMutex.Unlock() return 0, addr, err } newN := c.obfs.Deobfuscate(c.readBuf[13:n], p) c.readMutex.Unlock() if newN > 0 { // Valid packet return newN, addr, err } else if err != nil { // Not valid and orig.ReadFrom had some error return 0, addr, err } } } func (c *ObfsWeChatUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { c.writeMutex.Lock() c.writeBuf[0] = 0xa1 c.writeBuf[1] = 0x08 binary.BigEndian.PutUint32(c.writeBuf[2:], c.sn) c.sn++ c.writeBuf[6] = 0x00 c.writeBuf[7] = 0x10 c.writeBuf[8] = 0x11 c.writeBuf[9] = 0x18 c.writeBuf[10] = 0x30 c.writeBuf[11] = 0x22 c.writeBuf[12] = 0x30 bn := c.obfs.Obfuscate(p, c.writeBuf[13:]) _, err = c.orig.WriteTo(c.writeBuf[:13+bn], addr) c.writeMutex.Unlock() if err != nil { return 0, err } else { return len(p), nil } } func (c *ObfsWeChatUDPConn) Close() error { return c.orig.Close() } func (c *ObfsWeChatUDPConn) LocalAddr() net.Addr { return c.orig.LocalAddr() } func (c *ObfsWeChatUDPConn) SetDeadline(t time.Time) error { return c.orig.SetDeadline(t) } func (c *ObfsWeChatUDPConn) SetReadDeadline(t time.Time) error { return c.orig.SetReadDeadline(t) } func (c *ObfsWeChatUDPConn) SetWriteDeadline(t time.Time) error { return c.orig.SetWriteDeadline(t) } ================================================ FILE: core/Clash.Meta/transport/hysteria/core/client.go ================================================ package core import ( "context" "errors" "fmt" "net" "strconv" "sync" "time" "github.com/metacubex/mihomo/transport/hysteria/obfs" "github.com/metacubex/mihomo/transport/hysteria/pmtud_fix" "github.com/metacubex/mihomo/transport/hysteria/transport" "github.com/metacubex/mihomo/transport/hysteria/utils" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/randv2" "github.com/metacubex/tls" ) var ( ErrClosed = errors.New("closed") ) type CongestionFactory func(refBPS uint64) congestion.CongestionControl type Client struct { transport *transport.ClientTransport serverAddr string serverPorts string protocol string sendBPS, recvBPS uint64 auth []byte congestionFactory CongestionFactory obfuscator obfs.Obfuscator tlsConfig *tls.Config quicConfig *quic.Config quicSession *quic.Conn reconnectMutex sync.Mutex closed bool udpSessionMutex sync.RWMutex udpSessionMap map[uint32]chan *udpMessage udpDefragger defragger hopInterval time.Duration fastOpen bool } func NewClient(serverAddr string, serverPorts string, protocol string, auth []byte, tlsConfig *tls.Config, quicConfig *quic.Config, transport *transport.ClientTransport, sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory, obfuscator obfs.Obfuscator, hopInterval time.Duration, fastOpen bool) (*Client, error) { quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud_fix.DisablePathMTUDiscovery c := &Client{ transport: transport, serverAddr: serverAddr, serverPorts: serverPorts, protocol: protocol, sendBPS: sendBPS, recvBPS: recvBPS, auth: auth, congestionFactory: congestionFactory, obfuscator: obfuscator, tlsConfig: tlsConfig, quicConfig: quicConfig, hopInterval: hopInterval, fastOpen: fastOpen, } return c, nil } func (c *Client) connectToServer(dialer utils.PacketDialer) error { qs, err := c.transport.QUICDial(c.protocol, c.serverAddr, c.serverPorts, c.tlsConfig, c.quicConfig, c.obfuscator, c.hopInterval, dialer) if err != nil { return err } // Control stream ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout) stream, err := qs.OpenStreamSync(ctx) ctxCancel() if err != nil { _ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error") return err } ok, msg, err := c.handleControlStream(qs, stream) if err != nil { _ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error") return err } if !ok { _ = qs.CloseWithError(closeErrorCodeAuth, "auth error") return fmt.Errorf("auth error: %s", msg) } // All good c.udpSessionMap = make(map[uint32]chan *udpMessage) go c.handleMessage(qs) c.quicSession = qs return nil } func (c *Client) handleControlStream(qs *quic.Conn, stream *quic.Stream) (bool, string, error) { // Send client hello err := WriteClientHello(stream, ClientHello{ SendBPS: c.sendBPS, RecvBPS: c.recvBPS, Auth: c.auth, }) if err != nil { return false, "", err } // Receive server hello sh, err := ReadServerHello(stream) if err != nil { return false, "", err } // Set the congestion accordingly if sh.OK && c.congestionFactory != nil { qs.SetCongestionControl(c.congestionFactory(sh.RecvBPS)) } return sh.OK, sh.Message, nil } func (c *Client) handleMessage(qs *quic.Conn) { for { msg, err := qs.ReceiveDatagram(context.Background()) if err != nil { break } var udpMsg udpMessage err = udpMsg.Unpack(msg) if err != nil { continue } dfMsg := c.udpDefragger.Feed(udpMsg) if dfMsg == nil { continue } c.udpSessionMutex.RLock() ch, ok := c.udpSessionMap[dfMsg.SessionID] if ok { select { case ch <- dfMsg: // OK default: // Silently drop the message when the channel is full } } c.udpSessionMutex.RUnlock() } } func (c *Client) openStreamWithReconnect(dialer utils.PacketDialer) (*quic.Conn, *wrappedQUICStream, error) { c.reconnectMutex.Lock() defer c.reconnectMutex.Unlock() if c.closed { return nil, nil, ErrClosed } if c.quicSession == nil { if err := c.connectToServer(dialer); err != nil { // Still error, oops return nil, nil, err } } stream, err := c.quicSession.OpenStream() if err == nil { // All good return c.quicSession, &wrappedQUICStream{stream}, nil } // Something is wrong if nErr, ok := err.(net.Error); ok && nErr.Temporary() { // Temporary error, just return return nil, nil, err } // Permanent error, need to reconnect if err := c.connectToServer(dialer); err != nil { // Still error, oops return nil, nil, err } // We are not going to try again even if it still fails the second time stream, err = c.quicSession.OpenStream() return c.quicSession, &wrappedQUICStream{stream}, err } func (c *Client) DialTCP(host string, port uint16, dialer utils.PacketDialer) (net.Conn, error) { session, stream, err := c.openStreamWithReconnect(dialer) if err != nil { return nil, err } // Send request err = WriteClientRequest(stream, ClientRequest{ UDP: false, Host: host, Port: port, }) if err != nil { _ = stream.Close() return nil, err } // If fast open is enabled, we return the stream immediately // and defer the response handling to the first Read() call if !c.fastOpen { // Read response var sr *ServerResponse sr, err = ReadServerResponse(stream) if err != nil { _ = stream.Close() return nil, err } if !sr.OK { _ = stream.Close() return nil, fmt.Errorf("connection rejected: %s", sr.Message) } } return &quicConn{ Orig: stream, PseudoLocalAddr: session.LocalAddr(), PseudoRemoteAddr: session.RemoteAddr(), Established: !c.fastOpen, }, nil } func (c *Client) DialUDP(dialer utils.PacketDialer) (UDPConn, error) { session, stream, err := c.openStreamWithReconnect(dialer) if err != nil { return nil, err } // Send request err = WriteClientRequest(stream, ClientRequest{ UDP: false, }) if err != nil { _ = stream.Close() return nil, err } // Read response var sr *ServerResponse sr, err = ReadServerResponse(stream) if err != nil { _ = stream.Close() return nil, err } if !sr.OK { _ = stream.Close() return nil, fmt.Errorf("connection rejected: %s", sr.Message) } // Create a session in the map c.udpSessionMutex.Lock() nCh := make(chan *udpMessage, 1024) // Store the current session map for CloseFunc below // to ensures that we are adding and removing sessions on the same map, // as reconnecting will reassign the map sessionMap := c.udpSessionMap sessionMap[sr.UDPSessionID] = nCh c.udpSessionMutex.Unlock() pktConn := &quicPktConn{ Session: session, Stream: stream, CloseFunc: func() { c.udpSessionMutex.Lock() if ch, ok := sessionMap[sr.UDPSessionID]; ok { close(ch) delete(sessionMap, sr.UDPSessionID) } c.udpSessionMutex.Unlock() }, UDPSessionID: sr.UDPSessionID, MsgCh: nCh, } go pktConn.Hold() return pktConn, nil } func (c *Client) Close() error { c.reconnectMutex.Lock() defer c.reconnectMutex.Unlock() var err error if c.quicSession != nil { err = c.quicSession.CloseWithError(closeErrorCodeGeneric, "") } c.closed = true return err } type quicConn struct { Orig *wrappedQUICStream PseudoLocalAddr net.Addr PseudoRemoteAddr net.Addr Established bool } func (w *quicConn) Read(b []byte) (n int, err error) { if !w.Established { var sr *ServerResponse sr, err = ReadServerResponse(w.Orig) if err != nil { _ = w.Close() return 0, err } if !sr.OK { _ = w.Close() return 0, fmt.Errorf("connection rejected: %s", sr.Message) } w.Established = true } return w.Orig.Read(b) } func (w *quicConn) Write(b []byte) (n int, err error) { return w.Orig.Write(b) } func (w *quicConn) Close() error { return w.Orig.Close() } func (w *quicConn) LocalAddr() net.Addr { return w.PseudoLocalAddr } func (w *quicConn) RemoteAddr() net.Addr { return w.PseudoRemoteAddr } func (w *quicConn) SetDeadline(t time.Time) error { return w.Orig.SetDeadline(t) } func (w *quicConn) SetReadDeadline(t time.Time) error { return w.Orig.SetReadDeadline(t) } func (w *quicConn) SetWriteDeadline(t time.Time) error { return w.Orig.SetWriteDeadline(t) } type UDPConn interface { ReadFrom() ([]byte, string, error) WriteTo([]byte, string) error Close() error LocalAddr() net.Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } type quicPktConn struct { Session *quic.Conn Stream *wrappedQUICStream CloseFunc func() UDPSessionID uint32 MsgCh <-chan *udpMessage } func (c *quicPktConn) Hold() { // Hold the stream until it's closed buf := make([]byte, 1024) for { _, err := c.Stream.Read(buf) if err != nil { break } } _ = c.Close() } func (c *quicPktConn) ReadFrom() ([]byte, string, error) { msg := <-c.MsgCh if msg == nil { // Closed return nil, "", ErrClosed } return msg.Data, net.JoinHostPort(msg.Host, strconv.Itoa(int(msg.Port))), nil } func (c *quicPktConn) WriteTo(p []byte, addr string) error { host, port, err := utils.SplitHostPort(addr) if err != nil { return err } msg := udpMessage{ SessionID: c.UDPSessionID, Host: host, Port: port, FragCount: 1, Data: p, } // try no frag first err = c.Session.SendDatagram(msg.Pack()) if err != nil { var errSize *quic.DatagramTooLargeError if errors.As(err, &errSize) { // need to frag msg.MsgID = uint16(randv2.IntN(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 fragMsgs := fragUDPMessage(msg, int(errSize.MaxDatagramPayloadSize)) for _, fragMsg := range fragMsgs { err = c.Session.SendDatagram(fragMsg.Pack()) if err != nil { return err } } return nil } else { // some other error return err } } else { return nil } } func (c *quicPktConn) Close() error { c.CloseFunc() return c.Stream.Close() } func (c *quicPktConn) LocalAddr() net.Addr { return c.Session.LocalAddr() } func (c *quicPktConn) SetDeadline(t time.Time) error { return c.Stream.SetDeadline(t) } func (c *quicPktConn) SetReadDeadline(t time.Time) error { return c.Stream.SetReadDeadline(t) } func (c *quicPktConn) SetWriteDeadline(t time.Time) error { return c.Stream.SetWriteDeadline(t) } ================================================ FILE: core/Clash.Meta/transport/hysteria/core/frag.go ================================================ package core func fragUDPMessage(m udpMessage, maxSize int) []udpMessage { if m.Size() <= maxSize { return []udpMessage{m} } fullPayload := m.Data maxPayloadSize := maxSize - m.HeaderSize() off := 0 fragID := uint8(0) fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up var frags []udpMessage for off < len(fullPayload) { payloadSize := len(fullPayload) - off if payloadSize > maxPayloadSize { payloadSize = maxPayloadSize } frag := m frag.FragID = fragID frag.FragCount = fragCount frag.Data = fullPayload[off : off+payloadSize] frags = append(frags, frag) off += payloadSize fragID++ } return frags } type defragger struct { msgID uint16 frags []*udpMessage count uint8 } func (d *defragger) Feed(m udpMessage) *udpMessage { if m.FragCount <= 1 { return &m } if m.FragID >= m.FragCount { // wtf is this? return nil } if m.MsgID != d.msgID { // new message, clear previous state d.msgID = m.MsgID d.frags = make([]*udpMessage, m.FragCount) d.count = 1 d.frags[m.FragID] = &m } else if d.frags[m.FragID] == nil { d.frags[m.FragID] = &m d.count++ if int(d.count) == len(d.frags) { // all fragments received, assemble var data []byte for _, frag := range d.frags { data = append(data, frag.Data...) } m.Data = data m.FragID = 0 m.FragCount = 1 return &m } } return nil } ================================================ FILE: core/Clash.Meta/transport/hysteria/core/frag_test.go ================================================ package core import ( "reflect" "testing" ) func Test_fragUDPMessage(t *testing.T) { type args struct { m udpMessage maxSize int } tests := []struct { name string args args want []udpMessage }{ { "no frag", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 1, Data: []byte("hello"), }, 100, }, []udpMessage{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 1, Data: []byte("hello"), }, }, }, { "2 frags", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 1, Data: []byte("hello"), }, 22, }, []udpMessage{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 2, Data: []byte("hell"), }, udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 1, FragCount: 2, Data: []byte("o"), }, }, }, { "4 frags", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 1, Data: []byte("wow wow wow lol lmao"), }, 23, }, []udpMessage{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 4, Data: []byte("wow w"), }, udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 1, FragCount: 4, Data: []byte("ow wo"), }, udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 2, FragCount: 4, Data: []byte("w lol"), }, udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 3, FragCount: 4, Data: []byte(" lmao"), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := fragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) { t.Errorf("fragUDPMessage() = %v, want %v", got, tt.want) } }) } } func Test_defragger_Feed(t *testing.T) { d := &defragger{} type args struct { m udpMessage } tests := []struct { name string args args want *udpMessage }{ { "no frag", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 1, Data: []byte("hello"), }, }, &udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 123, FragID: 0, FragCount: 1, Data: []byte("hello"), }, }, { "frag 1 - 1/3", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 666, FragID: 0, FragCount: 3, Data: []byte("hello"), }, }, nil, }, { "frag 1 - 2/3", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 666, FragID: 1, FragCount: 3, Data: []byte(" shitty "), }, }, nil, }, { "frag 1 - 3/3", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 666, FragID: 2, FragCount: 3, Data: []byte("world!!"), }, }, &udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 666, FragID: 0, FragCount: 1, Data: []byte("hello shitty world!!"), }, }, { "frag 2 - 1/2", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 777, FragID: 0, FragCount: 2, Data: []byte("hello"), }, }, nil, }, { "frag 3 - 2/2", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 778, FragID: 1, FragCount: 2, Data: []byte(" moto"), }, }, nil, }, { "frag 2 - 2/2", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 777, FragID: 1, FragCount: 2, Data: []byte(" moto"), }, }, nil, }, { "frag 2 - 1/2 re", args{ udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 777, FragID: 0, FragCount: 2, Data: []byte("hello"), }, }, &udpMessage{ SessionID: 123, Host: "test", Port: 123, MsgID: 777, FragID: 0, FragCount: 1, Data: []byte("hello moto"), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) { t.Errorf("Feed() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: core/Clash.Meta/transport/hysteria/core/protocol.go ================================================ package core import ( "bytes" "encoding/binary" "errors" "io" "time" ) const ( protocolVersion = uint8(3) protocolTimeout = 10 * time.Second closeErrorCodeGeneric = 0 closeErrorCodeProtocol = 1 closeErrorCodeAuth = 2 ) type ClientHello struct { SendBPS uint64 RecvBPS uint64 Auth []byte } func WriteClientHello(stream io.Writer, hello ClientHello) error { var requestLen int requestLen += 1 // version requestLen += 8 // sendBPS requestLen += 8 // recvBPS requestLen += 2 // auth len requestLen += len(hello.Auth) request := make([]byte, requestLen) request[0] = protocolVersion binary.BigEndian.PutUint64(request[1:9], hello.SendBPS) binary.BigEndian.PutUint64(request[9:17], hello.RecvBPS) binary.BigEndian.PutUint16(request[17:19], uint16(len(hello.Auth))) copy(request[19:], hello.Auth) _, err := stream.Write(request) return err } func ReadClientHello(stream io.Reader) (*ClientHello, error) { var responseLen int responseLen += 1 // ok responseLen += 8 // sendBPS responseLen += 8 // recvBPS responseLen += 2 // auth len response := make([]byte, responseLen) _, err := io.ReadFull(stream, response) if err != nil { return nil, err } if response[0] != protocolVersion { return nil, errors.New("unsupported client version") } var clientHello ClientHello clientHello.SendBPS = binary.BigEndian.Uint64(response[1:9]) clientHello.RecvBPS = binary.BigEndian.Uint64(response[9:17]) authLen := binary.BigEndian.Uint16(response[17:19]) if clientHello.SendBPS == 0 || clientHello.RecvBPS == 0 { return nil, errors.New("invalid rate from client") } authBytes := make([]byte, authLen) _, err = io.ReadFull(stream, authBytes) if err != nil { return nil, err } clientHello.Auth = authBytes return &clientHello, nil } type ServerHello struct { OK bool SendBPS uint64 RecvBPS uint64 Message string } func ReadServerHello(stream io.Reader) (*ServerHello, error) { var responseLen int responseLen += 1 // ok responseLen += 8 // sendBPS responseLen += 8 // recvBPS responseLen += 2 // message len response := make([]byte, responseLen) _, err := io.ReadFull(stream, response) if err != nil { return nil, err } var serverHello ServerHello serverHello.OK = response[0] == 1 serverHello.SendBPS = binary.BigEndian.Uint64(response[1:9]) serverHello.RecvBPS = binary.BigEndian.Uint64(response[9:17]) messageLen := binary.BigEndian.Uint16(response[17:19]) if messageLen == 0 { return &serverHello, nil } message := make([]byte, messageLen) _, err = io.ReadFull(stream, message) if err != nil { return nil, err } serverHello.Message = string(message) return &serverHello, nil } func WriteServerHello(stream io.Writer, hello ServerHello) error { var responseLen int responseLen += 1 // ok responseLen += 8 // sendBPS responseLen += 8 // recvBPS responseLen += 2 // message len responseLen += len(hello.Message) response := make([]byte, responseLen) if hello.OK { response[0] = 1 } else { response[0] = 0 } binary.BigEndian.PutUint64(response[1:9], hello.SendBPS) binary.BigEndian.PutUint64(response[9:17], hello.RecvBPS) binary.BigEndian.PutUint16(response[17:19], uint16(len(hello.Message))) copy(response[19:], hello.Message) _, err := stream.Write(response) return err } type ClientRequest struct { UDP bool Host string Port uint16 } func ReadClientRequest(stream io.Reader) (*ClientRequest, error) { var clientRequest ClientRequest err := binary.Read(stream, binary.BigEndian, &clientRequest.UDP) if err != nil { return nil, err } var hostLen uint16 err = binary.Read(stream, binary.BigEndian, &hostLen) if err != nil { return nil, err } host := make([]byte, hostLen) _, err = io.ReadFull(stream, host) if err != nil { return nil, err } clientRequest.Host = string(host) err = binary.Read(stream, binary.BigEndian, &clientRequest.Port) if err != nil { return nil, err } return &clientRequest, nil } func WriteClientRequest(stream io.Writer, request ClientRequest) error { var requestLen int requestLen += 1 // udp requestLen += 2 // host len requestLen += len(request.Host) requestLen += 2 // port buffer := make([]byte, requestLen) if request.UDP { buffer[0] = 1 } else { buffer[0] = 0 } binary.BigEndian.PutUint16(buffer[1:3], uint16(len(request.Host))) n := copy(buffer[3:], request.Host) binary.BigEndian.PutUint16(buffer[3+n:3+n+2], request.Port) _, err := stream.Write(buffer) return err } type ServerResponse struct { OK bool UDPSessionID uint32 Message string } func ReadServerResponse(stream io.Reader) (*ServerResponse, error) { var responseLen int responseLen += 1 // ok responseLen += 4 // udp session id responseLen += 2 // message len response := make([]byte, responseLen) _, err := io.ReadFull(stream, response) if err != nil { return nil, err } var serverResponse ServerResponse serverResponse.OK = response[0] == 1 serverResponse.UDPSessionID = binary.BigEndian.Uint32(response[1:5]) messageLen := binary.BigEndian.Uint16(response[5:7]) if messageLen == 0 { return &serverResponse, nil } message := make([]byte, messageLen) _, err = io.ReadFull(stream, message) if err != nil { return nil, err } serverResponse.Message = string(message) return &serverResponse, nil } func WriteServerResponse(stream io.Writer, response ServerResponse) error { var responseLen int responseLen += 1 // ok responseLen += 4 // udp session id responseLen += 2 // message len responseLen += len(response.Message) buffer := make([]byte, responseLen) if response.OK { buffer[0] = 1 } else { buffer[0] = 0 } binary.BigEndian.PutUint32(buffer[1:5], response.UDPSessionID) binary.BigEndian.PutUint16(buffer[5:7], uint16(len(response.Message))) copy(buffer[7:], response.Message) _, err := stream.Write(buffer) return err } type udpMessage struct { SessionID uint32 Host string Port uint16 MsgID uint16 // doesn't matter when not fragmented, but must not be 0 when fragmented FragID uint8 // doesn't matter when not fragmented, starts at 0 when fragmented FragCount uint8 // must be 1 when not fragmented Data []byte } func (m udpMessage) HeaderSize() int { return 4 + 2 + len(m.Host) + 2 + 2 + 1 + 1 + 2 } func (m udpMessage) Size() int { return m.HeaderSize() + len(m.Data) } func (m udpMessage) Pack() []byte { data := make([]byte, m.Size()) buffer := bytes.NewBuffer(data) _ = binary.Write(buffer, binary.BigEndian, m.SessionID) _ = binary.Write(buffer, binary.BigEndian, uint16(len(m.Host))) buffer.WriteString(m.Host) _ = binary.Write(buffer, binary.BigEndian, m.Port) _ = binary.Write(buffer, binary.BigEndian, m.MsgID) _ = binary.Write(buffer, binary.BigEndian, m.FragID) _ = binary.Write(buffer, binary.BigEndian, m.FragCount) _ = binary.Write(buffer, binary.BigEndian, uint16(len(m.Data))) buffer.Write(m.Data) return buffer.Bytes() } func (m *udpMessage) Unpack(data []byte) error { reader := bytes.NewReader(data) err := binary.Read(reader, binary.BigEndian, &m.SessionID) if err != nil { return err } var hostLen uint16 err = binary.Read(reader, binary.BigEndian, &hostLen) if err != nil { return err } hostBytes := make([]byte, hostLen) _, err = io.ReadFull(reader, hostBytes) if err != nil { return err } m.Host = string(hostBytes) err = binary.Read(reader, binary.BigEndian, &m.Port) if err != nil { return err } err = binary.Read(reader, binary.BigEndian, &m.MsgID) if err != nil { return err } err = binary.Read(reader, binary.BigEndian, &m.FragID) if err != nil { return err } err = binary.Read(reader, binary.BigEndian, &m.FragCount) if err != nil { return err } var dataLen uint16 err = binary.Read(reader, binary.BigEndian, &dataLen) if err != nil { return err } if reader.Len() != int(dataLen) { return errors.New("invalid data length") } m.Data = data[len(data)-reader.Len():] return nil } ================================================ FILE: core/Clash.Meta/transport/hysteria/core/stream.go ================================================ package core import ( "context" "github.com/metacubex/quic-go" "time" ) // Handle stream close properly // Ref: https://github.com/libp2p/go-libp2p-quic-transport/blob/master/stream.go type wrappedQUICStream struct { Stream *quic.Stream } func (s *wrappedQUICStream) StreamID() quic.StreamID { return s.Stream.StreamID() } func (s *wrappedQUICStream) Read(p []byte) (n int, err error) { return s.Stream.Read(p) } func (s *wrappedQUICStream) CancelRead(code quic.StreamErrorCode) { s.Stream.CancelRead(code) } func (s *wrappedQUICStream) SetReadDeadline(t time.Time) error { return s.Stream.SetReadDeadline(t) } func (s *wrappedQUICStream) Write(p []byte) (n int, err error) { return s.Stream.Write(p) } func (s *wrappedQUICStream) Close() error { s.Stream.CancelRead(0) return s.Stream.Close() } func (s *wrappedQUICStream) CancelWrite(code quic.StreamErrorCode) { s.Stream.CancelWrite(code) } func (s *wrappedQUICStream) Context() context.Context { return s.Stream.Context() } func (s *wrappedQUICStream) SetWriteDeadline(t time.Time) error { return s.Stream.SetWriteDeadline(t) } func (s *wrappedQUICStream) SetDeadline(t time.Time) error { return s.Stream.SetDeadline(t) } ================================================ FILE: core/Clash.Meta/transport/hysteria/obfs/dummy.go ================================================ package obfs type DummyObfuscator struct{} func NewDummyObfuscator() *DummyObfuscator { return &DummyObfuscator{} } func (x *DummyObfuscator) Deobfuscate(in []byte, out []byte) int { if len(out) < len(in) { return 0 } return copy(out, in) } func (x *DummyObfuscator) Obfuscate(in []byte, out []byte) int { return copy(out, in) } ================================================ FILE: core/Clash.Meta/transport/hysteria/obfs/obfs.go ================================================ package obfs type Obfuscator interface { Deobfuscate(in []byte, out []byte) int Obfuscate(in []byte, out []byte) int } ================================================ FILE: core/Clash.Meta/transport/hysteria/obfs/xplus.go ================================================ package obfs import ( "crypto/rand" "crypto/sha256" ) // [salt][obfuscated payload] const saltLen = 16 type XPlusObfuscator struct { Key []byte } func NewXPlusObfuscator(key []byte) *XPlusObfuscator { return &XPlusObfuscator{ Key: key, } } func (x *XPlusObfuscator) Deobfuscate(in []byte, out []byte) int { pLen := len(in) - saltLen if pLen <= 0 || len(out) < pLen { // Invalid return 0 } key := sha256.Sum256(append(x.Key, in[:saltLen]...)) // Deobfuscate the payload for i, c := range in[saltLen:] { out[i] = c ^ key[i%sha256.Size] } return pLen } func (x *XPlusObfuscator) Obfuscate(in []byte, out []byte) int { _, _ = rand.Read(out[:saltLen]) // salt // Obfuscate the payload key := sha256.Sum256(append(x.Key, out[:saltLen]...)) for i, c := range in { out[i+saltLen] = c ^ key[i%sha256.Size] } return len(in) + saltLen } ================================================ FILE: core/Clash.Meta/transport/hysteria/obfs/xplus_test.go ================================================ package obfs import ( "bytes" "testing" ) func TestXPlusObfuscator(t *testing.T) { x := NewXPlusObfuscator([]byte("Vaundy")) tests := []struct { name string p []byte }{ {name: "1", p: []byte("HelloWorld")}, {name: "2", p: []byte("Regret is just a horrible attempt at time travel that ends with you feeling like crap")}, {name: "3", p: []byte("To be, or not to be, that is the question:\nWhether 'tis nobler in the mind to suffer\n" + "The slings and arrows of outrageous fortune,\nOr to take arms against a sea of troubles\n" + "And by opposing end them. To die—to sleep,\nNo more; and by a sleep to say we end")}, {name: "empty", p: []byte("")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := make([]byte, 10240) n := x.Obfuscate(tt.p, buf) n2 := x.Deobfuscate(buf[:n], buf[n:]) if !bytes.Equal(tt.p, buf[n:n+n2]) { t.Errorf("Inconsistent deobfuscate result: got %v, want %v", buf[n:n+n2], tt.p) } }) } } ================================================ FILE: core/Clash.Meta/transport/hysteria/pmtud_fix/avail.go ================================================ //go:build linux || windows || darwin package pmtud_fix const ( DisablePathMTUDiscovery = false ) ================================================ FILE: core/Clash.Meta/transport/hysteria/pmtud_fix/unavail.go ================================================ //go:build !linux && !windows && !darwin package pmtud_fix const ( DisablePathMTUDiscovery = true ) ================================================ FILE: core/Clash.Meta/transport/hysteria/transport/client.go ================================================ package transport import ( "fmt" "net" "time" "github.com/metacubex/mihomo/transport/hysteria/conns/faketcp" "github.com/metacubex/mihomo/transport/hysteria/conns/udp" "github.com/metacubex/mihomo/transport/hysteria/conns/wechat" obfsPkg "github.com/metacubex/mihomo/transport/hysteria/obfs" "github.com/metacubex/mihomo/transport/hysteria/utils" "github.com/metacubex/quic-go" "github.com/metacubex/tls" ) type ClientTransport struct{} func (ct *ClientTransport) quicPacketConn(proto string, rAddr net.Addr, serverPorts string, obfs obfsPkg.Obfuscator, hopInterval time.Duration, dialer utils.PacketDialer) (net.PacketConn, error) { server := rAddr.String() if len(proto) == 0 || proto == "udp" { conn, err := dialer.ListenPacket(rAddr) if err != nil { return nil, err } if obfs != nil { if serverPorts != "" { return udp.NewObfsUDPHopClientPacketConn(server, serverPorts, hopInterval, obfs, dialer) } oc := udp.NewObfsUDPConn(conn, obfs) return oc, nil } else { if serverPorts != "" { return udp.NewObfsUDPHopClientPacketConn(server, serverPorts, hopInterval, nil, dialer) } return conn, nil } } else if proto == "wechat-video" { conn, err := dialer.ListenPacket(rAddr) if err != nil { return nil, err } if obfs == nil { obfs = obfsPkg.NewDummyObfuscator() } return wechat.NewObfsWeChatUDPConn(conn, obfs), nil } else if proto == "faketcp" { var conn *faketcp.TCPConn conn, err := faketcp.Dial("tcp", server) if err != nil { return nil, err } if obfs != nil { oc := faketcp.NewObfsFakeTCPConn(conn, obfs) return oc, nil } else { return conn, nil } } else { return nil, fmt.Errorf("unsupported protocol: %s", proto) } } func (ct *ClientTransport) QUICDial(proto string, server string, serverPorts string, tlsConfig *tls.Config, quicConfig *quic.Config, obfs obfsPkg.Obfuscator, hopInterval time.Duration, dialer utils.PacketDialer) (*quic.Conn, error) { serverUDPAddr, err := dialer.RemoteAddr(server) if err != nil { return nil, err } pktConn, err := ct.quicPacketConn(proto, serverUDPAddr, serverPorts, obfs, hopInterval, dialer) if err != nil { return nil, err } transport := quic.Transport{Conn: pktConn} transport.SetCreatedConn(true) // auto close conn transport.SetSingleUse(true) // auto close transport qs, err := transport.Dial(dialer.Context(), serverUDPAddr, tlsConfig, quicConfig) if err != nil { _ = pktConn.Close() return nil, err } return qs, nil } ================================================ FILE: core/Clash.Meta/transport/hysteria/utils/misc.go ================================================ package utils import ( "context" "net" "strconv" ) func SplitHostPort(hostport string) (string, uint16, error) { host, port, err := net.SplitHostPort(hostport) if err != nil { return "", 0, err } portUint, err := strconv.ParseUint(port, 10, 16) if err != nil { return "", 0, err } return host, uint16(portUint), err } func ParseIPZone(s string) (net.IP, string) { s, zone := splitHostZone(s) return net.ParseIP(s), zone } func splitHostZone(s string) (host, zone string) { if i := last(s, '%'); i > 0 { host, zone = s[:i], s[i+1:] } else { host = s } return } func last(s string, b byte) int { i := len(s) for i--; i >= 0; i-- { if s[i] == b { break } } return i } type PacketDialer interface { ListenPacket(rAddr net.Addr) (net.PacketConn, error) Context() context.Context RemoteAddr(host string) (net.Addr, error) } ================================================ FILE: core/Clash.Meta/transport/kcptun/client.go ================================================ package kcptun import ( "context" "net" "sync" "time" "github.com/metacubex/mihomo/log" "github.com/metacubex/kcp-go" "github.com/metacubex/randv2" "github.com/metacubex/smux" ) const Mode = "kcptun" type DialFn func(ctx context.Context) (net.PacketConn, net.Addr, error) type Client struct { once sync.Once config Config block kcp.BlockCrypt ctx context.Context cancel context.CancelFunc numconn uint16 muxes []timedSession rr uint16 connMu sync.Mutex chScavenger chan timedSession } func NewClient(config Config) *Client { config.FillDefaults() block := config.NewBlock() ctx, cancel := context.WithCancel(context.Background()) return &Client{ config: config, block: block, ctx: ctx, cancel: cancel, } } func (c *Client) Close() error { c.cancel() return nil } func (c *Client) createConn(ctx context.Context, dial DialFn) (*smux.Session, error) { conn, addr, err := dial(ctx) if err != nil { return nil, err } config := c.config convid := randv2.Uint32() kcpconn, err := kcp.NewConn4(convid, addr, c.block, config.DataShard, config.ParityShard, true, conn) if err != nil { return nil, err } kcpconn.SetStreamMode(true) kcpconn.SetWriteDelay(false) kcpconn.SetNoDelay(config.NoDelay, config.Interval, config.Resend, config.NoCongestion) kcpconn.SetWindowSize(config.SndWnd, config.RcvWnd) kcpconn.SetMtu(config.MTU) kcpconn.SetACKNoDelay(config.AckNodelay) kcpconn.SetRateLimit(uint32(config.RateLimit)) _ = kcpconn.SetDSCP(config.DSCP) _ = kcpconn.SetReadBuffer(config.SockBuf) _ = kcpconn.SetWriteBuffer(config.SockBuf) smuxConfig := smux.DefaultConfig() smuxConfig.Version = config.SmuxVer smuxConfig.MaxReceiveBuffer = config.SmuxBuf smuxConfig.MaxStreamBuffer = config.StreamBuf smuxConfig.MaxFrameSize = config.FrameSize smuxConfig.KeepAliveInterval = time.Duration(config.KeepAlive) * time.Second if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout { smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval } if err := smux.VerifyConfig(smuxConfig); err != nil { return nil, err } var netConn net.Conn = kcpconn if !config.NoComp { netConn = NewCompStream(netConn) } // stream multiplex return smux.Client(netConn, smuxConfig) } func (c *Client) OpenStream(ctx context.Context, dial DialFn) (*smux.Stream, error) { c.once.Do(func() { // start scavenger if autoexpire is set c.chScavenger = make(chan timedSession, 128) if c.config.AutoExpire > 0 { go scavenger(c.ctx, c.chScavenger, &c.config) } c.numconn = uint16(c.config.Conn) c.muxes = make([]timedSession, c.config.Conn) c.rr = uint16(0) }) c.connMu.Lock() idx := c.rr % c.numconn // do auto expiration && reconnection if c.muxes[idx].session == nil || c.muxes[idx].session.IsClosed() || (c.config.AutoExpire > 0 && time.Now().After(c.muxes[idx].expiryDate)) { var err error c.muxes[idx].session, err = c.createConn(ctx, dial) if err != nil { c.connMu.Unlock() return nil, err } c.muxes[idx].expiryDate = time.Now().Add(time.Duration(c.config.AutoExpire) * time.Second) if c.config.AutoExpire > 0 { // only when autoexpire set c.chScavenger <- c.muxes[idx] } } c.rr++ session := c.muxes[idx].session c.connMu.Unlock() return session.OpenStream() } // timedSession is a wrapper for smux.Session with expiry date type timedSession struct { session *smux.Session expiryDate time.Time } // scavenger goroutine is used to close expired sessions func scavenger(ctx context.Context, ch chan timedSession, config *Config) { ticker := time.NewTicker(scavengePeriod * time.Second) defer ticker.Stop() var sessionList []timedSession for { select { case item := <-ch: sessionList = append(sessionList, timedSession{ item.session, item.expiryDate.Add(time.Duration(config.ScavengeTTL) * time.Second)}) case <-ticker.C: var newList []timedSession for k := range sessionList { s := sessionList[k] if s.session.IsClosed() { log.Debugln("scavenger: session normally closed: %s", s.session.LocalAddr()) } else if time.Now().After(s.expiryDate) { s.session.Close() log.Debugln("scavenger: session closed due to ttl: %s", s.session.LocalAddr()) } else { newList = append(newList, sessionList[k]) } } sessionList = newList case <-ctx.Done(): return } } } ================================================ FILE: core/Clash.Meta/transport/kcptun/common.go ================================================ package kcptun import ( "crypto/sha1" "github.com/metacubex/mihomo/log" "github.com/metacubex/kcp-go" "golang.org/x/crypto/pbkdf2" ) const ( // SALT is use for pbkdf2 key expansion SALT = "kcp-go" // maximum supported smux version maxSmuxVer = 2 // scavenger check period scavengePeriod = 5 ) type Config struct { Key string `json:"key"` Crypt string `json:"crypt"` Mode string `json:"mode"` Conn int `json:"conn"` AutoExpire int `json:"autoexpire"` ScavengeTTL int `json:"scavengettl"` MTU int `json:"mtu"` RateLimit int `json:"ratelimit"` SndWnd int `json:"sndwnd"` RcvWnd int `json:"rcvwnd"` DataShard int `json:"datashard"` ParityShard int `json:"parityshard"` DSCP int `json:"dscp"` NoComp bool `json:"nocomp"` AckNodelay bool `json:"acknodelay"` NoDelay int `json:"nodelay"` Interval int `json:"interval"` Resend int `json:"resend"` NoCongestion int `json:"nc"` SockBuf int `json:"sockbuf"` SmuxVer int `json:"smuxver"` SmuxBuf int `json:"smuxbuf"` FrameSize int `json:"framesize"` StreamBuf int `json:"streambuf"` KeepAlive int `json:"keepalive"` } func (config *Config) FillDefaults() { if config.Key == "" { config.Key = "it's a secrect" } if config.Crypt == "" { config.Crypt = "aes" } if config.Mode == "" { config.Mode = "fast" } if config.Conn == 0 { config.Conn = 1 } if config.ScavengeTTL == 0 { config.ScavengeTTL = 600 } if config.MTU == 0 { config.MTU = 1350 } if config.SndWnd == 0 { config.SndWnd = 128 } if config.RcvWnd == 0 { config.RcvWnd = 512 } if config.DataShard == 0 { config.DataShard = 10 } if config.ParityShard == 0 { config.ParityShard = 3 } if config.Interval == 0 { config.Interval = 50 } if config.SockBuf == 0 { config.SockBuf = 4194304 } if config.SmuxVer == 0 { config.SmuxVer = 1 } if config.SmuxBuf == 0 { config.SmuxBuf = 4194304 } if config.FrameSize == 0 { config.FrameSize = 8192 } if config.StreamBuf == 0 { config.StreamBuf = 2097152 } if config.KeepAlive == 0 { config.KeepAlive = 10 } switch config.Mode { case "normal": config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 40, 2, 1 case "fast": config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 30, 2, 1 case "fast2": config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 1, 20, 2, 1 case "fast3": config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 1, 10, 2, 1 } // SMUX Version check if config.SmuxVer > maxSmuxVer { log.Warnln("unsupported smux version: %d", config.SmuxVer) config.SmuxVer = maxSmuxVer } // Scavenge parameters check if config.AutoExpire != 0 && config.ScavengeTTL > config.AutoExpire { log.Warnln("WARNING: scavengettl is bigger than autoexpire, connections may race hard to use bandwidth.") log.Warnln("Try limiting scavengettl to a smaller value.") } } func (config *Config) NewBlock() (block kcp.BlockCrypt) { pass := pbkdf2.Key([]byte(config.Key), []byte(SALT), 4096, 32, sha1.New) switch config.Crypt { case "null": block = nil case "tea": block, _ = kcp.NewTEABlockCrypt(pass[:16]) case "xor": block, _ = kcp.NewSimpleXORBlockCrypt(pass) case "none": block, _ = kcp.NewNoneBlockCrypt(pass) case "aes-128": block, _ = kcp.NewAESBlockCrypt(pass[:16]) case "aes-192": block, _ = kcp.NewAESBlockCrypt(pass[:24]) case "blowfish": block, _ = kcp.NewBlowfishBlockCrypt(pass) case "twofish": block, _ = kcp.NewTwofishBlockCrypt(pass) case "cast5": block, _ = kcp.NewCast5BlockCrypt(pass[:16]) case "3des": block, _ = kcp.NewTripleDESBlockCrypt(pass[:24]) case "xtea": block, _ = kcp.NewXTEABlockCrypt(pass[:16]) case "salsa20": block, _ = kcp.NewSalsa20BlockCrypt(pass) case "aes-128-gcm": block, _ = kcp.NewAESGCMCrypt(pass[:16]) default: config.Crypt = "aes" block, _ = kcp.NewAESBlockCrypt(pass) } return } ================================================ FILE: core/Clash.Meta/transport/kcptun/comp.go ================================================ package kcptun import ( "net" "time" "github.com/golang/snappy" ) // CompStream is a net.Conn wrapper that compresses data using snappy type CompStream struct { conn net.Conn w *snappy.Writer r *snappy.Reader } func (c *CompStream) Read(p []byte) (n int, err error) { return c.r.Read(p) } func (c *CompStream) Write(p []byte) (n int, err error) { if _, err := c.w.Write(p); err != nil { return 0, err } if err := c.w.Flush(); err != nil { return 0, err } return len(p), err } func (c *CompStream) Close() error { return c.conn.Close() } func (c *CompStream) LocalAddr() net.Addr { return c.conn.LocalAddr() } func (c *CompStream) RemoteAddr() net.Addr { return c.conn.RemoteAddr() } func (c *CompStream) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) } func (c *CompStream) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) } func (c *CompStream) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) } // NewCompStream creates a new stream that compresses data using snappy func NewCompStream(conn net.Conn) *CompStream { c := new(CompStream) c.conn = conn c.w = snappy.NewBufferedWriter(conn) c.r = snappy.NewReader(conn) return c } ================================================ FILE: core/Clash.Meta/transport/kcptun/doc.go ================================================ // Package kcptun copy and modify from: // https://github.com/xtaci/kcptun/tree/f54f35175bed6ddda4e47aa35c9d7ae8b7e7eb85 // adopt for mihomo // without SM4,QPP,tcpraw support package kcptun ================================================ FILE: core/Clash.Meta/transport/kcptun/server.go ================================================ package kcptun import ( "net" "time" "github.com/metacubex/kcp-go" "github.com/metacubex/smux" ) type Server struct { config Config block kcp.BlockCrypt } func NewServer(config Config) *Server { config.FillDefaults() block := config.NewBlock() return &Server{ config: config, block: block, } } func (s *Server) Serve(pc net.PacketConn, handler func(net.Conn)) error { lis, err := kcp.ServeConn(s.block, s.config.DataShard, s.config.ParityShard, pc) if err != nil { return err } defer lis.Close() _ = lis.SetDSCP(s.config.DSCP) _ = lis.SetReadBuffer(s.config.SockBuf) _ = lis.SetWriteBuffer(s.config.SockBuf) for { conn, err := lis.AcceptKCP() if err != nil { return err } conn.SetStreamMode(true) conn.SetWriteDelay(false) conn.SetNoDelay(s.config.NoDelay, s.config.Interval, s.config.Resend, s.config.NoCongestion) conn.SetMtu(s.config.MTU) conn.SetWindowSize(s.config.SndWnd, s.config.RcvWnd) conn.SetACKNoDelay(s.config.AckNodelay) conn.SetRateLimit(uint32(s.config.RateLimit)) var netConn net.Conn = conn if !s.config.NoComp { netConn = NewCompStream(netConn) } go func() { // stream multiplex smuxConfig := smux.DefaultConfig() smuxConfig.Version = s.config.SmuxVer smuxConfig.MaxReceiveBuffer = s.config.SmuxBuf smuxConfig.MaxStreamBuffer = s.config.StreamBuf smuxConfig.MaxFrameSize = s.config.FrameSize smuxConfig.KeepAliveInterval = time.Duration(s.config.KeepAlive) * time.Second if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout { smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval } mux, err := smux.Server(netConn, smuxConfig) if err != nil { return } defer mux.Close() for { stream, err := mux.AcceptStream() if err != nil { return } go handler(stream) } }() } } ================================================ FILE: core/Clash.Meta/transport/masque/client_h2.go ================================================ package masque // copy and modify from: https://github.com/Diniboy1123/connect-ip-go/blob/8d7bb0a858a2674046a7cb5538749e4c826c3538/client_h2.go import ( "context" "encoding/binary" "errors" "fmt" "io" "net" "net/url" "strings" "sync" "github.com/metacubex/mihomo/common/contextutils" "github.com/metacubex/mihomo/log" "github.com/metacubex/http" "github.com/metacubex/quic-go/quicvarint" "github.com/yosida95/uritemplate/v3" ) const h2DatagramCapsuleType uint64 = 0 const ( ipv4HeaderLen = 20 ipv6HeaderLen = 40 ) func ConnectTunnelH2(ctx context.Context, h2Transport *http.Transport, connectUri string) (*http.ClientConn, IpConn, error) { additionalHeaders := http.Header{ "User-Agent": []string{""}, } template := uritemplate.MustNew(connectUri) h2Headers := additionalHeaders.Clone() h2Headers.Set("cf-connect-proto", "cf-connect-ip") // TODO: support PQC h2Headers.Set("pq-enabled", "false") cc, err := h2Transport.NewClientConn(ctx, "https", ":0") if err != nil { return nil, nil, fmt.Errorf("connect-ip: failed to create client connection: %w", err) } if err = cc.Reserve(); err != nil { _ = cc.Close() return nil, nil, fmt.Errorf("connect-ip: failed to reserve client connection: %w", err) } ipConn, rsp, err := dialH2(ctx, cc, template, h2Headers) if err != nil { _ = cc.Close() if strings.Contains(err.Error(), "tls: access denied") { return nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service") } return nil, nil, fmt.Errorf("failed to dial connect-ip over HTTP/2: %w", err) } if rsp.StatusCode != http.StatusOK { _ = ipConn.Close() _ = cc.Close() return nil, nil, fmt.Errorf("failed to dial connect-ip: %v", rsp.Status) } return cc, ipConn, nil } // dialH2 dials a proxied connection over HTTP/2 CONNECT-IP. // // This transport carries proxied packets inside HTTP capsule DATAGRAM frames. func dialH2(ctx context.Context, rt http.RoundTripper, template *uritemplate.Template, additionalHeaders http.Header) (*h2IpConn, *http.Response, error) { if len(template.Varnames()) > 0 { return nil, nil, errors.New("connect-ip: IP flow forwarding not supported") } u, err := url.Parse(template.Raw()) if err != nil { return nil, nil, fmt.Errorf("connect-ip: failed to parse URI: %w", err) } reqCtx, cancel := context.WithCancel(context.Background()) // reqCtx must disconnect from ctx, otherwise ctx would close the entire HTTP/2 connection. pr, pw := io.Pipe() req, err := http.NewRequestWithContext(reqCtx, http.MethodConnect, u.String(), pr) if err != nil { cancel() _ = pr.Close() _ = pw.Close() return nil, nil, fmt.Errorf("connect-ip: failed to create request: %w", err) } req.Host = authorityFromURL(u) req.ContentLength = -1 req.Header = make(http.Header) for k, v := range additionalHeaders { req.Header[k] = v } stop := contextutils.AfterFunc(ctx, cancel) // temporarily connect ctx with reqCtx when client.Do rsp, err := rt.RoundTrip(req) stop() // disconnect ctx with reqCtx after client.Do if err != nil { cancel() _ = pr.Close() _ = pw.Close() return nil, nil, fmt.Errorf("connect-ip: failed to send request: %w", err) } if rsp.StatusCode < 200 || rsp.StatusCode > 299 { cancel() _ = pr.Close() _ = pw.Close() _ = rsp.Body.Close() return nil, rsp, fmt.Errorf("connect-ip: server responded with %d", rsp.StatusCode) } stream := &h2DatagramStream{ requestBody: pw, responseBody: rsp.Body, cancel: cancel, } return &h2IpConn{ str: stream, closeChan: make(chan struct{}), }, rsp, nil } func authorityFromURL(u *url.URL) string { if u.Port() != "" { return u.Host } host := u.Hostname() if host == "" { return u.Host } return host + ":443" } type h2IpConn struct { str *h2DatagramStream mu sync.Mutex closeChan chan struct{} closeErr error } func (c *h2IpConn) ReadPacket() (b []byte, err error) { start: data, err := c.str.ReceiveDatagram(context.Background()) if err != nil { defer func() { // There are no errors that can be recovered in h2 mode, // so calling Close allows the outer read loop to exit in the next iteration by returning net.ErrClosed. _ = c.Close() }() select { case <-c.closeChan: return nil, c.closeErr default: return nil, err } } if err := c.handleIncomingProxiedPacket(data); err != nil { log.Debugln("dropping proxied packet: %s", err) goto start } return data, nil } func (c *h2IpConn) handleIncomingProxiedPacket(data []byte) error { if len(data) == 0 { return errors.New("connect-ip: empty packet") } switch v := ipVersion(data); v { default: return fmt.Errorf("connect-ip: unknown IP versions: %d", v) case 4: if len(data) < ipv4HeaderLen { return fmt.Errorf("connect-ip: malformed datagram: too short") } case 6: if len(data) < ipv6HeaderLen { return fmt.Errorf("connect-ip: malformed datagram: too short") } } return nil } // WritePacket writes an IP packet to the stream. // If sending the packet fails, it might return an ICMP packet. // It is the caller's responsibility to send the ICMP packet to the sender. func (c *h2IpConn) WritePacket(b []byte) (icmp []byte, err error) { data, err := c.composeDatagram(b) if err != nil { log.Debugln("dropping proxied packet (%d bytes) that can't be proxied: %s", len(b), err) return nil, nil } if err := c.str.SendDatagram(data); err != nil { select { case <-c.closeChan: return nil, c.closeErr default: return nil, err } } return nil, nil } func (c *h2IpConn) composeDatagram(b []byte) ([]byte, error) { // TODO: implement src, dst and ipproto checks if len(b) == 0 { return nil, nil } switch v := ipVersion(b); v { default: return nil, fmt.Errorf("connect-ip: unknown IP versions: %d", v) case 4: if len(b) < ipv4HeaderLen { return nil, fmt.Errorf("connect-ip: IPv4 packet too short") } ttl := b[8] if ttl <= 1 { return nil, fmt.Errorf("connect-ip: datagram TTL too small: %d", ttl) } b[8]-- // decrement TTL // recalculate the checksum binary.BigEndian.PutUint16(b[10:12], calculateIPv4Checksum(([ipv4HeaderLen]byte)(b[:ipv4HeaderLen]))) case 6: if len(b) < ipv6HeaderLen { return nil, fmt.Errorf("connect-ip: IPv6 packet too short") } hopLimit := b[7] if hopLimit <= 1 { return nil, fmt.Errorf("connect-ip: datagram Hop Limit too small: %d", hopLimit) } b[7]-- // Decrement Hop Limit } return b, nil } func (c *h2IpConn) Close() error { c.mu.Lock() if c.closeErr == nil { c.closeErr = net.ErrClosed close(c.closeChan) } c.mu.Unlock() err := c.str.Close() return err } func ipVersion(b []byte) uint8 { return b[0] >> 4 } func calculateIPv4Checksum(header [ipv4HeaderLen]byte) uint16 { // add every 16-bit word in the header, skipping the checksum field (bytes 10 and 11) var sum uint32 for i := 0; i < len(header); i += 2 { if i == 10 { continue // skip checksum field } sum += uint32(binary.BigEndian.Uint16(header[i : i+2])) } for (sum >> 16) > 0 { sum = (sum & 0xffff) + (sum >> 16) } return ^uint16(sum) } type h2DatagramStream struct { requestBody *io.PipeWriter responseBody io.ReadCloser cancel context.CancelFunc readMu sync.Mutex writeMu sync.Mutex } func (s *h2DatagramStream) ReceiveDatagram(_ context.Context) ([]byte, error) { s.readMu.Lock() defer s.readMu.Unlock() reader := quicvarint.NewReader(s.responseBody) for { capsuleType, err := quicvarint.Read(reader) if err != nil { return nil, err } payloadLen, err := quicvarint.Read(reader) if err != nil { return nil, err } payload := make([]byte, payloadLen) _, err = io.ReadFull(reader, payload) if err != nil { return nil, err } if capsuleType != h2DatagramCapsuleType { continue } return payload, nil } } func (s *h2DatagramStream) SendDatagram(data []byte) error { frame := make([]byte, 0, quicvarint.Len(h2DatagramCapsuleType)+quicvarint.Len(uint64(len(data)))+len(data)) frame = quicvarint.Append(frame, h2DatagramCapsuleType) frame = quicvarint.Append(frame, uint64(len(data))) frame = append(frame, data...) s.writeMu.Lock() defer s.writeMu.Unlock() _, err := s.requestBody.Write(frame) if err != nil { return fmt.Errorf("connect-ip: failed to send datagram capsule: %w", err) } return nil } func (s *h2DatagramStream) Close() error { _ = s.requestBody.Close() err := s.responseBody.Close() s.cancel() return err } ================================================ FILE: core/Clash.Meta/transport/masque/masque.go ================================================ // Package masque // copy and modify from https://github.com/Diniboy1123/usque/blob/d0eb96e7e5c56cce6cf34a7f8d75abbedba58fef/api/masque.go package masque import ( "context" "crypto/ecdsa" "crypto/rand" "crypto/x509" "errors" "fmt" "math/big" "net/netip" "net/url" "time" connectip "github.com/metacubex/connect-ip-go" "github.com/metacubex/http" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" "github.com/metacubex/tls" "github.com/yosida95/uritemplate/v3" ) const ( ConnectSNI = "consumer-masque.cloudflareclient.com" ConnectURI = "https://cloudflareaccess.com" ) type IpConn interface { ReadPacket() (b []byte, err error) WritePacket(b []byte) (icmp []byte, err error) Close() error } // PrepareTlsConfig creates a TLS configuration using the provided certificate and SNI (Server Name Indication). // It also verifies the peer's public key against the provided public key. func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, sni string, insecure bool) (*tls.Config, error) { verfiyCert := func(cert *x509.Certificate) error { if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok { // we only support ECDSA // TODO: don't hardcode cert type in the future // as backend can start using different cert types return x509.ErrUnsupportedAlgorithm } if !cert.PublicKey.(*ecdsa.PublicKey).Equal(peerPubKey) { // reason is incorrect, but the best I could figure // detail explains the actual reason //10 is NoValidChains, but we support go1.22 where it's not defined return x509.CertificateInvalidError{Cert: cert, Reason: 10, Detail: "remote endpoint has a different public key than what we trust"} } return nil } cert, err := GenerateCert(privKey) if err != nil { return nil, fmt.Errorf("failed to generate cert: %v", err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{ { Certificate: cert, PrivateKey: privKey, }, }, ServerName: sni, NextProtos: []string{http3.NextProtoH3}, // WARN: SNI is usually not for the endpoint, so we must skip verification InsecureSkipVerify: true, // we pin to the endpoint public key VerifyConnection: func(cs tls.ConnectionState) error { var err error for _, cert := range cs.PeerCertificates { if er := verfiyCert(cert); er != nil { err = errors.Join(err, er) continue } } return err }, } if insecure { tlsConfig.VerifyConnection = nil } return tlsConfig, nil } func GenerateCert(privKey *ecdsa.PrivateKey) ([][]byte, error) { cert, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ SerialNumber: big.NewInt(0), NotBefore: time.Now(), NotAfter: time.Now().Add(1 * 24 * time.Hour), }, &x509.Certificate{}, &privKey.PublicKey, privKey) if err != nil { return nil, err } return [][]byte{cert}, nil } // ConnectTunnel establishes a QUIC connection and sets up a Connect-IP tunnel with the provided endpoint. // Endpoint address is used to check whether the authentication/connection is successful or not. // Requires modified connect-ip-go for now to support Cloudflare's non RFC compliant implementation. func ConnectTunnel(ctx context.Context, quicConn *quic.Conn, connectUri string) (*http3.Transport, *connectip.Conn, error) { tr := &http3.Transport{ EnableDatagrams: true, AdditionalSettings: map[uint64]uint64{ // official client still sends this out as well, even though // it's deprecated, see https://datatracker.ietf.org/doc/draft-ietf-masque-h3-datagram/00/ // SETTINGS_H3_DATAGRAM_00 = 0x0000000000000276 // https://github.com/cloudflare/quiche/blob/7c66757dbc55b8d0c3653d4b345c6785a181f0b7/quiche/src/h3/frame.rs#L46 0x276: 1, }, DisableCompression: true, } hconn := tr.NewClientConn(quicConn) additionalHeaders := http.Header{ "User-Agent": []string{""}, } template := uritemplate.MustNew(connectUri) ipConn, rsp, err := dialEx(ctx, hconn, template, "cf-connect-ip", additionalHeaders, true) if err != nil { _ = tr.Close() if err.Error() == "CRYPTO_ERROR 0x131 (remote): tls: access denied" { return nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service") } return nil, nil, fmt.Errorf("failed to dial connect-ip: %v", err) } err = ipConn.AdvertiseRoute(ctx, []connectip.IPRoute{ { IPProtocol: 0, StartIP: netip.AddrFrom4([4]byte{}), EndIP: netip.AddrFrom4([4]byte{255, 255, 255, 255}), }, { IPProtocol: 0, StartIP: netip.AddrFrom16([16]byte{}), EndIP: netip.AddrFrom16([16]byte{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, }), }, }) if err != nil { _ = ipConn.Close() _ = tr.Close() return nil, nil, err } if rsp.StatusCode != http.StatusOK { _ = ipConn.Close() _ = tr.Close() return nil, nil, fmt.Errorf("failed to dial connect-ip: %v", rsp.Status) } return tr, ipConn, nil } // dialEx dials a proxied connection to a target server. func dialEx(ctx context.Context, conn *http3.ClientConn, template *uritemplate.Template, requestProtocol string, additionalHeaders http.Header, ignoreExtendedConnect bool) (*connectip.Conn, *http.Response, error) { if len(template.Varnames()) > 0 { return nil, nil, errors.New("connect-ip: IP flow forwarding not supported") } u, err := url.Parse(template.Raw()) if err != nil { return nil, nil, fmt.Errorf("connect-ip: failed to parse URI: %w", err) } select { case <-ctx.Done(): return nil, nil, context.Cause(ctx) case <-conn.Context().Done(): return nil, nil, context.Cause(conn.Context()) case <-conn.ReceivedSettings(): } settings := conn.Settings() if !ignoreExtendedConnect && !settings.EnableExtendedConnect { return nil, nil, errors.New("connect-ip: server didn't enable Extended CONNECT") } if !settings.EnableDatagrams { return nil, nil, errors.New("connect-ip: server didn't enable datagrams") } const capsuleProtocolHeaderValue = "?1" headers := http.Header{http3.CapsuleProtocolHeader: []string{capsuleProtocolHeaderValue}} for k, v := range additionalHeaders { headers[k] = v } rstr, err := conn.OpenRequestStream(ctx) if err != nil { return nil, nil, fmt.Errorf("connect-ip: failed to open request stream: %w", err) } if err := rstr.SendRequestHeader(&http.Request{ Method: http.MethodConnect, Proto: requestProtocol, Host: u.Host, Header: headers, URL: u, }); err != nil { return nil, nil, fmt.Errorf("connect-ip: failed to send request: %w", err) } // TODO: optimistically return the connection rsp, err := rstr.ReadResponse() if err != nil { return nil, nil, fmt.Errorf("connect-ip: failed to read response: %w", err) } if rsp.StatusCode < 200 || rsp.StatusCode > 299 { return nil, rsp, fmt.Errorf("connect-ip: server responded with %d", rsp.StatusCode) } return connectip.NewProxiedConn(rstr), rsp, nil } ================================================ FILE: core/Clash.Meta/transport/restls/restls.go ================================================ package restls import ( "context" "net" tls "github.com/metacubex/restls-client-go" ) const ( Mode string = "restls" ) type Restls struct { *tls.UConn } func (r *Restls) Upstream() any { return r.UConn.NetConn() } type Config = tls.Config var NewRestlsConfig = tls.NewRestlsConfig // NewRestls return a Restls Connection func NewRestls(ctx context.Context, conn net.Conn, config *Config) (net.Conn, error) { clientHellowID := tls.HelloChrome_Auto if config != nil { clientIDPtr := config.ClientID.Load() if clientIDPtr != nil { clientHellowID = *clientIDPtr } } restls := &Restls{ UConn: tls.UClient(conn, config, clientHellowID), } if err := restls.HandshakeContext(ctx); err != nil { return nil, err } return restls, nil } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/core/cipher.go ================================================ package core import ( "crypto/md5" "errors" "net" "sort" "strings" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" "github.com/metacubex/mihomo/transport/shadowsocks/shadowstream" ) type Cipher interface { StreamConnCipher PacketConnCipher } type StreamConnCipher interface { StreamConn(net.Conn) net.Conn } type PacketConnCipher interface { PacketConn(N.EnhancePacketConn) N.EnhancePacketConn } // ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns). var ErrCipherNotSupported = errors.New("cipher not supported") const ( aeadAes128Gcm = "AEAD_AES_128_GCM" aeadAes192Gcm = "AEAD_AES_192_GCM" aeadAes256Gcm = "AEAD_AES_256_GCM" aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305" aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305" aeadChacha8Poly1305 = "AEAD_CHACHA8_POLY1305" aeadXChacha8Poly1305 = "AEAD_XCHACHA8_POLY1305" aeadAes128Ccm = "AEAD_AES_128_CCM" aeadAes192Ccm = "AEAD_AES_192_CCM" aeadAes256Ccm = "AEAD_AES_256_CCM" ) // List of AEAD ciphers: key size in bytes and constructor var aeadList = map[string]struct { KeySize int New func([]byte) (shadowaead.Cipher, error) }{ aeadAes128Gcm: {16, shadowaead.AESGCM}, aeadAes192Gcm: {24, shadowaead.AESGCM}, aeadAes256Gcm: {32, shadowaead.AESGCM}, aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305}, aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305}, aeadChacha8Poly1305: {32, shadowaead.Chacha8Poly1305}, aeadXChacha8Poly1305: {32, shadowaead.XChacha8Poly1305}, aeadAes128Ccm: {16, shadowaead.AESCCM}, aeadAes192Ccm: {24, shadowaead.AESCCM}, aeadAes256Ccm: {32, shadowaead.AESCCM}, } // List of stream ciphers: key size in bytes and constructor var streamList = map[string]struct { KeySize int New func(key []byte) (shadowstream.Cipher, error) }{ "RC4-MD5": {16, shadowstream.RC4MD5}, "AES-128-CTR": {16, shadowstream.AESCTR}, "AES-192-CTR": {24, shadowstream.AESCTR}, "AES-256-CTR": {32, shadowstream.AESCTR}, "AES-128-CFB": {16, shadowstream.AESCFB}, "AES-192-CFB": {24, shadowstream.AESCFB}, "AES-256-CFB": {32, shadowstream.AESCFB}, "CHACHA20": {32, shadowstream.ChaCha20}, "CHACHA20-IETF": {32, shadowstream.Chacha20IETF}, "XCHACHA20": {32, shadowstream.Xchacha20}, } // ListCipher returns a list of available cipher names sorted alphabetically. func ListCipher() []string { var l []string for k := range aeadList { l = append(l, k) } for k := range streamList { l = append(l, k) } sort.Strings(l) return l } // PickCipher returns a Cipher of the given name. Derive key from password if given key is empty. func PickCipher(name string, key []byte, password string) (Cipher, error) { name = strings.ToUpper(name) switch name { case "DUMMY": return &dummy{}, nil case "CHACHA20-IETF-POLY1305": name = aeadChacha20Poly1305 case "XCHACHA20-IETF-POLY1305": name = aeadXChacha20Poly1305 case "AES-128-GCM": name = aeadAes128Gcm case "AES-192-GCM": name = aeadAes192Gcm case "AES-256-GCM": name = aeadAes256Gcm case "CHACHA8-IETF-POLY1305": name = aeadChacha8Poly1305 case "XCHACHA8-IETF-POLY1305": name = aeadXChacha8Poly1305 case "AES-128-CCM": name = aeadAes128Ccm case "AES-192-CCM": name = aeadAes192Ccm case "AES-256-CCM": name = aeadAes256Ccm } if choice, ok := aeadList[name]; ok { if len(key) == 0 { key = Kdf(password, choice.KeySize) } if len(key) != choice.KeySize { return nil, shadowaead.KeySizeError(choice.KeySize) } aead, err := choice.New(key) return &AeadCipher{Cipher: aead, Key: key}, err } if choice, ok := streamList[name]; ok { if len(key) == 0 { key = Kdf(password, choice.KeySize) } if len(key) != choice.KeySize { return nil, shadowstream.KeySizeError(choice.KeySize) } ciph, err := choice.New(key) return &StreamCipher{Cipher: ciph, Key: key}, err } return nil, ErrCipherNotSupported } type AeadCipher struct { shadowaead.Cipher Key []byte } func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) } func (aead *AeadCipher) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return shadowaead.NewPacketConn(c, aead) } type StreamCipher struct { shadowstream.Cipher Key []byte } func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) } func (ciph *StreamCipher) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return shadowstream.NewPacketConn(c, ciph) } // dummy cipher does not encrypt type dummy struct{} func (dummy) StreamConn(c net.Conn) net.Conn { return c } func (dummy) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return c } // key-derivation function from original Shadowsocks func Kdf(password string, keyLen int) []byte { var b, prev []byte h := md5.New() for len(b) < keyLen { h.Write(prev) h.Write([]byte(password)) b = h.Sum(b) prev = b[len(b)-h.Size():] h.Reset() } return b[:keyLen] } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/shadowaead/cipher.go ================================================ package shadowaead import ( "crypto/aes" "crypto/cipher" "crypto/sha1" "io" "strconv" "github.com/metacubex/chacha" "gitlab.com/go-extension/aes-ccm" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) type Cipher interface { KeySize() int SaltSize() int Encrypter(salt []byte) (cipher.AEAD, error) Decrypter(salt []byte) (cipher.AEAD, error) } type KeySizeError int func (e KeySizeError) Error() string { return "key size error: need " + strconv.Itoa(int(e)) + " bytes" } func hkdfSHA1(secret, salt, info, outkey []byte) { r := hkdf.New(sha1.New, secret, salt, info) if _, err := io.ReadFull(r, outkey); err != nil { panic(err) // should never happen } } type metaCipher struct { psk []byte makeAEAD func(key []byte) (cipher.AEAD, error) } func (a *metaCipher) KeySize() int { return len(a.psk) } func (a *metaCipher) SaltSize() int { if ks := a.KeySize(); ks > 16 { return ks } return 16 } func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) { subkey := make([]byte, a.KeySize()) hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) return a.makeAEAD(subkey) } func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) { subkey := make([]byte, a.KeySize()) hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) return a.makeAEAD(subkey) } func aesGCM(key []byte) (cipher.AEAD, error) { blk, err := aes.NewCipher(key) if err != nil { return nil, err } return cipher.NewGCM(blk) } // AESGCM creates a new Cipher with a pre-shared key. len(psk) must be // one of 16, 24, or 32 to select AES-128/196/256-GCM. func AESGCM(psk []byte) (Cipher, error) { switch l := len(psk); l { case 16, 24, 32: // AES 128/196/256 default: return nil, aes.KeySizeError(l) } return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil } func aesCCM(key []byte) (cipher.AEAD, error) { blk, err := aes.NewCipher(key) if err != nil { return nil, err } return ccm.NewCCM(blk) } // AESCCM creates a new Cipher with a pre-shared key. len(psk) must be // one of 16, 24, or 32 to select AES-128/196/256-GCM. func AESCCM(psk []byte) (Cipher, error) { switch l := len(psk); l { case 16, 24, 32: // AES 128/196/256 default: return nil, aes.KeySizeError(l) } return &metaCipher{psk: psk, makeAEAD: aesCCM}, nil } // Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) // must be 32. func Chacha20Poly1305(psk []byte) (Cipher, error) { if len(psk) != chacha20poly1305.KeySize { return nil, KeySizeError(chacha20poly1305.KeySize) } return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil } // XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) // must be 32. func XChacha20Poly1305(psk []byte) (Cipher, error) { if len(psk) != chacha20poly1305.KeySize { return nil, KeySizeError(chacha20poly1305.KeySize) } return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil } // Chacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) // must be 32. func Chacha8Poly1305(psk []byte) (Cipher, error) { if len(psk) != chacha.KeySize { return nil, KeySizeError(chacha.KeySize) } return &metaCipher{psk: psk, makeAEAD: chacha.NewChaCha8IETFPoly1305}, nil } // XChacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) // must be 32. func XChacha8Poly1305(psk []byte) (Cipher, error) { if len(psk) != chacha.KeySize { return nil, KeySizeError(chacha.KeySize) } return &metaCipher{psk: psk, makeAEAD: chacha.NewXChaCha20IETFPoly1305}, nil } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/shadowaead/packet.go ================================================ package shadowaead import ( "crypto/rand" "errors" "io" "net" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" ) // ErrShortPacket means that the packet is too short for a valid encrypted packet. var ErrShortPacket = errors.New("short packet") var _zerononce [128]byte // read-only. 128 bytes is more than enough. // Pack encrypts plaintext using Cipher with a randomly generated salt and // returns a slice of dst containing the encrypted packet and any error occurred. // Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead(). func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) { saltSize := ciph.SaltSize() salt := dst[:saltSize] if _, err := rand.Read(salt); err != nil { return nil, err } aead, err := ciph.Encrypter(salt) if err != nil { return nil, err } if len(dst) < saltSize+len(plaintext)+aead.Overhead() { return nil, io.ErrShortBuffer } b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil) return dst[:saltSize+len(b)], nil } // Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred. // Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead(). func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) { saltSize := ciph.SaltSize() if len(pkt) < saltSize { return nil, ErrShortPacket } salt := pkt[:saltSize] aead, err := ciph.Decrypter(salt) if err != nil { return nil, err } if len(pkt) < saltSize+aead.Overhead() { return nil, ErrShortPacket } if saltSize+len(dst)+aead.Overhead() < len(pkt) { return nil, io.ErrShortBuffer } b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil) return b, err } type PacketConn struct { N.EnhancePacketConn Cipher } const maxPacketSize = 64 * 1024 // NewPacketConn wraps an N.EnhancePacketConn with cipher func NewPacketConn(c N.EnhancePacketConn, ciph Cipher) *PacketConn { return &PacketConn{EnhancePacketConn: c, Cipher: ciph} } // WriteTo encrypts b and write to addr using the embedded PacketConn. func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { buf := pool.Get(maxPacketSize) defer pool.Put(buf) buf, err := Pack(buf, b, c) if err != nil { return 0, err } _, err = c.EnhancePacketConn.WriteTo(buf, addr) return len(b), err } // ReadFrom reads from the embedded PacketConn and decrypts into b. func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { n, addr, err := c.EnhancePacketConn.ReadFrom(b) if err != nil { return n, addr, err } bb, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c) if err != nil { return n, addr, err } copy(b, bb) return len(bb), addr, err } func (c *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { data, put, addr, err = c.EnhancePacketConn.WaitReadFrom() if err != nil { return } data, err = Unpack(data[c.Cipher.SaltSize():], data, c) if err != nil { if put != nil { put() } data = nil put = nil return } return } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/shadowaead/stream.go ================================================ package shadowaead import ( "crypto/cipher" "crypto/rand" "errors" "io" "net" "github.com/metacubex/mihomo/common/pool" ) const ( // payloadSizeMask is the maximum size of payload in bytes. payloadSizeMask = 0x3FFF // 16*1024 - 1 bufSize = 17 * 1024 // >= 2+aead.Overhead()+payloadSizeMask+aead.Overhead() ) var ErrZeroChunk = errors.New("zero chunk") type Writer struct { io.Writer cipher.AEAD nonce [32]byte // should be sufficient for most nonce sizes } // NewWriter wraps an io.Writer with authenticated encryption. func NewWriter(w io.Writer, aead cipher.AEAD) *Writer { return &Writer{Writer: w, AEAD: aead} } // Write encrypts p and writes to the embedded io.Writer. func (w *Writer) Write(p []byte) (n int, err error) { buf := pool.Get(bufSize) defer pool.Put(buf) nonce := w.nonce[:w.NonceSize()] tag := w.Overhead() off := 2 + tag // compatible with snell if len(p) == 0 { buf = buf[:off] buf[0], buf[1] = byte(0), byte(0) w.Seal(buf[:0], nonce, buf[:2], nil) increment(nonce) _, err = w.Writer.Write(buf) return } for nr := 0; n < len(p) && err == nil; n += nr { nr = payloadSizeMask if n+nr > len(p) { nr = len(p) - n } buf = buf[:off+nr+tag] buf[0], buf[1] = byte(nr>>8), byte(nr) // big-endian payload size w.Seal(buf[:0], nonce, buf[:2], nil) increment(nonce) w.Seal(buf[:off], nonce, p[n:n+nr], nil) increment(nonce) _, err = w.Writer.Write(buf) } return } // ReadFrom reads from the given io.Reader until EOF or error, encrypts and // writes to the embedded io.Writer. Returns number of bytes read from r and // any error encountered. func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { buf := pool.Get(bufSize) defer pool.Put(buf) nonce := w.nonce[:w.NonceSize()] tag := w.Overhead() off := 2 + tag for { nr, er := r.Read(buf[off : off+payloadSizeMask]) n += int64(nr) buf[0], buf[1] = byte(nr>>8), byte(nr) w.Seal(buf[:0], nonce, buf[:2], nil) increment(nonce) w.Seal(buf[:off], nonce, buf[off:off+nr], nil) increment(nonce) if _, ew := w.Writer.Write(buf[:off+nr+tag]); ew != nil { err = ew return } if er != nil { if er != io.EOF { // ignore EOF as per io.ReaderFrom contract err = er } return } } } type Reader struct { io.Reader cipher.AEAD nonce [32]byte // should be sufficient for most nonce sizes buf []byte // to be put back into bufPool off int // offset to unconsumed part of buf } // NewReader wraps an io.Reader with authenticated decryption. func NewReader(r io.Reader, aead cipher.AEAD) *Reader { return &Reader{Reader: r, AEAD: aead} } // Read and decrypt a record into p. len(p) >= max payload size + AEAD overhead. func (r *Reader) read(p []byte) (int, error) { nonce := r.nonce[:r.NonceSize()] tag := r.Overhead() // decrypt payload size p = p[:2+tag] if _, err := io.ReadFull(r.Reader, p); err != nil { return 0, err } _, err := r.Open(p[:0], nonce, p, nil) increment(nonce) if err != nil { return 0, err } // decrypt payload size := (int(p[0])<<8 + int(p[1])) & payloadSizeMask if size == 0 { return 0, ErrZeroChunk } p = p[:size+tag] if _, err := io.ReadFull(r.Reader, p); err != nil { return 0, err } _, err = r.Open(p[:0], nonce, p, nil) increment(nonce) if err != nil { return 0, err } return size, nil } // Read reads from the embedded io.Reader, decrypts and writes to p. func (r *Reader) Read(p []byte) (int, error) { if r.buf == nil { if len(p) >= payloadSizeMask+r.Overhead() { return r.read(p) } b := pool.Get(bufSize) n, err := r.read(b) if err != nil { return 0, err } r.buf = b[:n] r.off = 0 } n := copy(p, r.buf[r.off:]) r.off += n if r.off == len(r.buf) { pool.Put(r.buf[:cap(r.buf)]) r.buf = nil } return n, nil } // WriteTo reads from the embedded io.Reader, decrypts and writes to w until // there's no more data to write or when an error occurs. Return number of // bytes written to w and any error encountered. func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { if r.buf == nil { r.buf = pool.Get(bufSize) r.off = len(r.buf) } for { for r.off < len(r.buf) { nw, ew := w.Write(r.buf[r.off:]) r.off += nw n += int64(nw) if ew != nil { if r.off == len(r.buf) { pool.Put(r.buf[:cap(r.buf)]) r.buf = nil } err = ew return } } nr, er := r.read(r.buf) if er != nil { if er != io.EOF { err = er } return } r.buf = r.buf[:nr] r.off = 0 } } // increment little-endian encoded unsigned integer b. Wrap around on overflow. func increment(b []byte) { for i := range b { b[i]++ if b[i] != 0 { return } } } type Conn struct { net.Conn Cipher r *Reader w *Writer } // NewConn wraps a stream-oriented net.Conn with cipher. func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } func (c *Conn) initReader() error { salt := make([]byte, c.SaltSize()) if _, err := io.ReadFull(c.Conn, salt); err != nil { return err } aead, err := c.Decrypter(salt) if err != nil { return err } c.r = NewReader(c.Conn, aead) return nil } func (c *Conn) Read(b []byte) (int, error) { if c.r == nil { if err := c.initReader(); err != nil { return 0, err } } return c.r.Read(b) } func (c *Conn) WriteTo(w io.Writer) (int64, error) { if c.r == nil { if err := c.initReader(); err != nil { return 0, err } } return c.r.WriteTo(w) } func (c *Conn) initWriter() error { salt := make([]byte, c.SaltSize()) if _, err := rand.Read(salt); err != nil { return err } aead, err := c.Encrypter(salt) if err != nil { return err } _, err = c.Conn.Write(salt) if err != nil { return err } c.w = NewWriter(c.Conn, aead) return nil } func (c *Conn) Write(b []byte) (int, error) { if c.w == nil { if err := c.initWriter(); err != nil { return 0, err } } return c.w.Write(b) } func (c *Conn) ReadFrom(r io.Reader) (int64, error) { if c.w == nil { if err := c.initWriter(); err != nil { return 0, err } } return c.w.ReadFrom(r) } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/shadowstream/chacha20.go ================================================ package shadowstream import ( "crypto/cipher" "github.com/metacubex/chacha" ) func newChaCha20(nonce, key []byte) cipher.Stream { c, err := chacha.NewChaCha20IgnoreCounterOverflow(nonce, key) if err != nil { panic(err) // should never happen } return c } type chacha20key []byte func (k chacha20key) IVSize() int { return chacha.NonceSize } func (k chacha20key) Encrypter(iv []byte) cipher.Stream { return newChaCha20(iv, k) } func (k chacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } func ChaCha20(key []byte) (Cipher, error) { if len(key) != chacha.KeySize { return nil, KeySizeError(chacha.KeySize) } return chacha20key(key), nil } // IETF-variant of chacha20 type chacha20ietfkey []byte func (k chacha20ietfkey) IVSize() int { return chacha.INonceSize } func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream { return newChaCha20(iv, k) } func Chacha20IETF(key []byte) (Cipher, error) { if len(key) != chacha.KeySize { return nil, KeySizeError(chacha.KeySize) } return chacha20ietfkey(key), nil } type xchacha20key []byte func (k xchacha20key) IVSize() int { return chacha.XNonceSize } func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } func (k xchacha20key) Encrypter(iv []byte) cipher.Stream { return newChaCha20(iv, k) } func Xchacha20(key []byte) (Cipher, error) { if len(key) != chacha.KeySize { return nil, KeySizeError(chacha.KeySize) } return xchacha20key(key), nil } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/shadowstream/cipher.go ================================================ package shadowstream import ( "crypto/aes" "crypto/cipher" "crypto/md5" "crypto/rc4" "strconv" ) // Cipher generates a pair of stream ciphers for encryption and decryption. type Cipher interface { IVSize() int Encrypter(iv []byte) cipher.Stream Decrypter(iv []byte) cipher.Stream } type KeySizeError int func (e KeySizeError) Error() string { return "key size error: need " + strconv.Itoa(int(e)) + " bytes" } // CTR mode type ctrStream struct{ cipher.Block } func (b *ctrStream) IVSize() int { return b.BlockSize() } func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) } func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) } func AESCTR(key []byte) (Cipher, error) { blk, err := aes.NewCipher(key) if err != nil { return nil, err } return &ctrStream{blk}, nil } // CFB mode type cfbStream struct{ cipher.Block } func (b *cfbStream) IVSize() int { return b.BlockSize() } func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) } func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) } func AESCFB(key []byte) (Cipher, error) { blk, err := aes.NewCipher(key) if err != nil { return nil, err } return &cfbStream{blk}, nil } type rc4Md5Key []byte func (k rc4Md5Key) IVSize() int { return 16 } func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream { h := md5.New() h.Write([]byte(k)) h.Write(iv) rc4key := h.Sum(nil) c, _ := rc4.NewCipher(rc4key) return c } func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } func RC4MD5(key []byte) (Cipher, error) { return rc4Md5Key(key), nil } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/shadowstream/packet.go ================================================ package shadowstream import ( "crypto/rand" "errors" "io" "net" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" ) // ErrShortPacket means the packet is too short to be a valid encrypted packet. var ErrShortPacket = errors.New("short packet") // Pack encrypts plaintext using stream cipher s and a random IV. // Returns a slice of dst containing random IV and ciphertext. // Ensure len(dst) >= s.IVSize() + len(plaintext). func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) { if len(dst) < s.IVSize()+len(plaintext) { return nil, io.ErrShortBuffer } iv := dst[:s.IVSize()] _, err := rand.Read(iv) if err != nil { return nil, err } s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext) return dst[:len(iv)+len(plaintext)], nil } // UnpackInplace decrypts pkt using stream cipher s. // Returns a slice of pkt containing decrypted plaintext. // Note: The data in the input dst will be changed func UnpackInplace(pkt []byte, s Cipher) ([]byte, error) { if len(pkt) < s.IVSize() { return nil, ErrShortPacket } iv, dst := pkt[:s.IVSize()], pkt[s.IVSize():] s.Decrypter(iv).XORKeyStream(dst, dst) return dst, nil } // Unpack decrypts pkt using stream cipher s. // Returns a slice of dst containing decrypted plaintext. func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) { if len(pkt) < s.IVSize() { return nil, ErrShortPacket } if len(dst) < len(pkt)-s.IVSize() { return nil, io.ErrShortBuffer } iv := pkt[:s.IVSize()] s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):]) return dst[:len(pkt)-len(iv)], nil } type PacketConn struct { N.EnhancePacketConn Cipher } // NewPacketConn wraps an N.EnhancePacketConn with stream cipher encryption/decryption. func NewPacketConn(c N.EnhancePacketConn, ciph Cipher) *PacketConn { return &PacketConn{EnhancePacketConn: c, Cipher: ciph} } const maxPacketSize = 64 * 1024 func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { buf := pool.Get(maxPacketSize) defer pool.Put(buf) buf, err := Pack(buf, b, c.Cipher) if err != nil { return 0, err } _, err = c.EnhancePacketConn.WriteTo(buf, addr) return len(b), err } func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { n, addr, err := c.EnhancePacketConn.ReadFrom(b) if err != nil { return n, addr, err } bb, err := UnpackInplace(b[:n], c.Cipher) if err != nil { return n, addr, err } copy(b, bb) return len(bb), addr, err } func (c *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { data, put, addr, err = c.EnhancePacketConn.WaitReadFrom() if err != nil { return } data, err = UnpackInplace(data, c.Cipher) if err != nil { if put != nil { put() } data = nil put = nil return } return } ================================================ FILE: core/Clash.Meta/transport/shadowsocks/shadowstream/stream.go ================================================ package shadowstream import ( "crypto/cipher" "crypto/rand" "io" "net" ) const bufSize = 2048 type Writer struct { io.Writer cipher.Stream buf [bufSize]byte } // NewWriter wraps an io.Writer with stream cipher encryption. func NewWriter(w io.Writer, s cipher.Stream) *Writer { return &Writer{Writer: w, Stream: s} } func (w *Writer) Write(p []byte) (n int, err error) { buf := w.buf[:] for nw := 0; n < len(p) && err == nil; n += nw { end := n + len(buf) if end > len(p) { end = len(p) } w.XORKeyStream(buf, p[n:end]) nw, err = w.Writer.Write(buf[:end-n]) } return } func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { buf := w.buf[:] for { nr, er := r.Read(buf) n += int64(nr) b := buf[:nr] w.XORKeyStream(b, b) if _, err = w.Writer.Write(b); err != nil { return } if er != nil { if er != io.EOF { // ignore EOF as per io.ReaderFrom contract err = er } return } } } type Reader struct { io.Reader cipher.Stream buf [bufSize]byte } // NewReader wraps an io.Reader with stream cipher decryption. func NewReader(r io.Reader, s cipher.Stream) *Reader { return &Reader{Reader: r, Stream: s} } func (r *Reader) Read(p []byte) (n int, err error) { n, err = r.Reader.Read(p) if err != nil { return 0, err } r.XORKeyStream(p, p[:n]) return } func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { buf := r.buf[:] for { nr, er := r.Reader.Read(buf) if nr > 0 { r.XORKeyStream(buf, buf[:nr]) nw, ew := w.Write(buf[:nr]) n += int64(nw) if ew != nil { err = ew return } } if er != nil { if er != io.EOF { // ignore EOF as per io.Copy contract (using src.WriteTo shortcut) err = er } return } } } // A Conn represents a Shadowsocks connection. It implements the net.Conn interface. type Conn struct { net.Conn Cipher r *Reader w *Writer readIV []byte writeIV []byte } // NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption. func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } func (c *Conn) initReader() error { if c.r == nil { iv, err := c.ObtainReadIV() if err != nil { return err } c.r = NewReader(c.Conn, c.Decrypter(iv)) } return nil } func (c *Conn) Read(b []byte) (int, error) { if c.r == nil { if err := c.initReader(); err != nil { return 0, err } } return c.r.Read(b) } func (c *Conn) WriteTo(w io.Writer) (int64, error) { if c.r == nil { if err := c.initReader(); err != nil { return 0, err } } return c.r.WriteTo(w) } func (c *Conn) initWriter() error { if c.w == nil { iv, err := c.ObtainWriteIV() if err != nil { return err } if _, err := c.Conn.Write(iv); err != nil { return err } c.w = NewWriter(c.Conn, c.Encrypter(iv)) } return nil } func (c *Conn) Write(b []byte) (int, error) { if c.w == nil { if err := c.initWriter(); err != nil { return 0, err } } return c.w.Write(b) } func (c *Conn) ReadFrom(r io.Reader) (int64, error) { if c.w == nil { if err := c.initWriter(); err != nil { return 0, err } } return c.w.ReadFrom(r) } func (c *Conn) ObtainWriteIV() ([]byte, error) { if len(c.writeIV) == c.IVSize() { return c.writeIV, nil } iv := make([]byte, c.IVSize()) if _, err := rand.Read(iv); err != nil { return nil, err } c.writeIV = iv return iv, nil } func (c *Conn) ObtainReadIV() ([]byte, error) { if len(c.readIV) == c.IVSize() { return c.readIV, nil } iv := make([]byte, c.IVSize()) if _, err := io.ReadFull(c.Conn, iv); err != nil { return nil, err } c.readIV = iv return iv, nil } ================================================ FILE: core/Clash.Meta/transport/shadowtls/shadowtls.go ================================================ package shadowtls import ( "context" "crypto/hmac" "crypto/sha1" "encoding/binary" "fmt" "hash" "io" "net" "github.com/metacubex/mihomo/common/pool" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/tls" ) const ( chunkSize = 1 << 13 Mode string = "shadow-tls" hashLen int = 8 tlsHeaderLen int = 5 ) var ( DefaultALPN = []string{"h2", "http/1.1"} ) // ShadowTLS is shadow-tls implementation type ShadowTLS struct { net.Conn password []byte remain int firstRequest bool tlsConfig *tls.Config } type HashedConn struct { net.Conn hasher hash.Hash } func newHashedStream(conn net.Conn, password []byte) HashedConn { return HashedConn{ Conn: conn, hasher: hmac.New(sha1.New, password), } } func (h HashedConn) Read(b []byte) (n int, err error) { n, err = h.Conn.Read(b) h.hasher.Write(b[:n]) return } func (s *ShadowTLS) read(b []byte) (int, error) { var buf [tlsHeaderLen]byte _, err := io.ReadFull(s.Conn, buf[:]) if err != nil { return 0, fmt.Errorf("shadowtls read failed %w", err) } if buf[0] != 0x17 || buf[1] != 0x3 || buf[2] != 0x3 { return 0, fmt.Errorf("invalid shadowtls header %v", buf) } length := int(binary.BigEndian.Uint16(buf[3:])) if length > len(b) { n, err := s.Conn.Read(b) if err != nil { return n, err } s.remain = length - n return n, nil } return io.ReadFull(s.Conn, b[:length]) } func (s *ShadowTLS) Read(b []byte) (int, error) { if s.remain > 0 { length := s.remain if length > len(b) { length = len(b) } n, err := io.ReadFull(s.Conn, b[:length]) if err != nil { return n, fmt.Errorf("shadowtls Read failed with %w", err) } s.remain -= n return n, nil } return s.read(b) } func (s *ShadowTLS) 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 := s.write(b[i:end]) if err != nil { return n, fmt.Errorf("shadowtls Write failed with %w, i=%d, end=%d, n=%d", err, i, end, n) } } return length, nil } func (s *ShadowTLS) write(b []byte) (int, error) { var hashVal []byte if s.firstRequest { hashedConn := newHashedStream(s.Conn, s.password) tlsConn := tls.Client(hashedConn, s.tlsConfig) // fix tls handshake not timeout ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel() if err := tlsConn.HandshakeContext(ctx); err != nil { return 0, fmt.Errorf("tls connect failed with %w", err) } hashVal = hashedConn.hasher.Sum(nil)[:hashLen] s.firstRequest = false } buf := pool.GetBuffer() defer pool.PutBuffer(buf) buf.Write([]byte{0x17, 0x03, 0x03}) binary.Write(buf, binary.BigEndian, uint16(len(b)+len(hashVal))) buf.Write(hashVal) buf.Write(b) _, err := s.Conn.Write(buf.Bytes()) if err != nil { // return 0 because errors occur here make the // whole situation irrecoverable return 0, err } return len(b), nil } // NewShadowTLS return a ShadowTLS func NewShadowTLS(conn net.Conn, password string, tlsConfig *tls.Config) net.Conn { return &ShadowTLS{ Conn: conn, password: []byte(password), firstRequest: true, tlsConfig: tlsConfig, } } ================================================ FILE: core/Clash.Meta/transport/simple-obfs/http.go ================================================ package obfs import ( "bytes" "crypto/rand" "encoding/base64" "fmt" "io" "net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/http" "github.com/metacubex/randv2" ) // 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) { pool.Put(ho.buf) ho.buf = nil } return n, nil } if ho.firstResponse { buf := pool.Get(pool.RelayBufferSize) n, err := ho.Conn.Read(buf) if err != nil { pool.Put(buf) return 0, err } idx := bytes.Index(buf[:n], []byte("\r\n\r\n")) if idx == -1 { pool.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 { pool.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, err := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) if err != nil { return 0, err } req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", randv2.Int()%54, randv2.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) } // 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: core/Clash.Meta/transport/simple-obfs/tls.go ================================================ package obfs import ( "bytes" "crypto/rand" "encoding/binary" "io" "net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/ntp" ) 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 := pool.Get(discardN) _, err := io.ReadFull(to.Conn, buf) pool.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 := pool.GetBuffer() defer pool.PutBuffer(buf) buf.Write([]byte{0x17, 0x03, 0x03}) binary.Write(buf, binary.BigEndian, uint16(len(b))) buf.Write(b) _, err := to.Conn.Write(buf.Bytes()) if err != nil { // return 0 because errors occur here make the // whole situation irrecoverable return 0, err } return len(b), nil } // 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(ntp.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: core/Clash.Meta/transport/sing-shadowtls/shadowtls.go ================================================ package sing_shadowtls import ( "context" "net" "github.com/metacubex/mihomo/component/ca" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/log" "github.com/metacubex/sing-shadowtls" "github.com/metacubex/tls" "golang.org/x/exp/slices" ) const ( Mode string = "shadow-tls" ) var ( DefaultALPN = []string{"h2", "http/1.1"} WsALPN = []string{"http/1.1"} ) type ShadowTLSOption struct { Password string Host string Fingerprint string Certificate string PrivateKey string ClientFingerprint string SkipCertVerify bool Version int ALPN []string } func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) (net.Conn, error) { tlsConfig, err := ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ NextProtos: option.ALPN, MinVersion: tls.VersionTLS12, InsecureSkipVerify: option.SkipCertVerify, ServerName: option.Host, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } if option.Version == 1 { tlsConfig.MaxVersion = tls.VersionTLS12 // ShadowTLS v1 only support TLS 1.2 } tlsHandshake := uTLSHandshakeFunc(tlsConfig, option.ClientFingerprint, option.Version) client, err := shadowtls.NewClient(shadowtls.ClientConfig{ Version: option.Version, Password: option.Password, TLSHandshake: tlsHandshake, Logger: log.SingLogger, }) if err != nil { return nil, err } return client.DialContextConn(ctx, conn) } func uTLSHandshakeFunc(config *tls.Config, clientFingerprint string, version int) shadowtls.TLSHandshakeFunc { return func(ctx context.Context, conn net.Conn, sessionIDGenerator shadowtls.TLSSessionIDGeneratorFunc) error { tlsConfig := tlsC.UConfig(config) tlsConfig.SessionIDGenerator = sessionIDGenerator if version == 1 { tlsConfig.MaxVersion = tlsC.VersionTLS12 // ShadowTLS v1 only support TLS 1.2 tlsConn := tlsC.Client(conn, tlsConfig) return tlsConn.HandshakeContext(ctx) } if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok { tlsConn := tlsC.UClient(conn, tlsConfig, clientFingerprint) if slices.Equal(tlsConfig.NextProtos, WsALPN) { err := tlsC.BuildWebsocketHandshakeState(tlsConn) if err != nil { return err } } if version == 2 { // ShadowTLS v2 not work with X25519MLKEM768 err := tlsC.BuildRemovedX25519MLKEM768HandshakeState(tlsConn) if err != nil { return err } } return tlsConn.HandshakeContext(ctx) } tlsConn := tlsC.Client(conn, tlsConfig) return tlsConn.HandshakeContext(ctx) } } ================================================ FILE: core/Clash.Meta/transport/snell/cipher.go ================================================ package snell import ( "crypto/aes" "crypto/cipher" "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" "golang.org/x/crypto/argon2" "golang.org/x/crypto/chacha20poly1305" ) type snellCipher struct { psk []byte keySize int makeAEAD func(key []byte) (cipher.AEAD, error) } func (sc *snellCipher) KeySize() int { return sc.keySize } func (sc *snellCipher) SaltSize() int { return 16 } func (sc *snellCipher) Encrypter(salt []byte) (cipher.AEAD, error) { return sc.makeAEAD(snellKDF(sc.psk, salt, sc.KeySize())) } func (sc *snellCipher) Decrypter(salt []byte) (cipher.AEAD, error) { return sc.makeAEAD(snellKDF(sc.psk, salt, sc.KeySize())) } func snellKDF(psk, salt []byte, keySize int) []byte { // snell use a special kdf function return argon2.IDKey(psk, salt, 3, 8, 1, 32)[:keySize] } func aesGCM(key []byte) (cipher.AEAD, error) { blk, err := aes.NewCipher(key) if err != nil { return nil, err } return cipher.NewGCM(blk) } func NewAES128GCM(psk []byte) shadowaead.Cipher { return &snellCipher{ psk: psk, keySize: 16, makeAEAD: aesGCM, } } func NewChacha20Poly1305(psk []byte) shadowaead.Cipher { return &snellCipher{ psk: psk, keySize: 32, makeAEAD: chacha20poly1305.New, } } ================================================ FILE: core/Clash.Meta/transport/snell/pool.go ================================================ package snell import ( "context" "net" "time" "github.com/metacubex/mihomo/component/pool" "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" ) type Pool struct { pool *pool.Pool[*Snell] } func (p *Pool) Get() (net.Conn, error) { return p.GetContext(context.Background()) } func (p *Pool) GetContext(ctx context.Context) (net.Conn, error) { elm, err := p.pool.GetContext(ctx) if err != nil { return nil, err } return &PoolConn{elm, p}, nil } func (p *Pool) Put(conn *Snell) { if err := HalfClose(conn); err != nil { _ = conn.Close() return } p.pool.Put(conn) } type PoolConn struct { *Snell pool *Pool } func (pc *PoolConn) Read(b []byte) (int, error) { // save old status of reply (it mutable by Read) reply := pc.Snell.reply n, err := pc.Snell.Read(b) if err == shadowaead.ErrZeroChunk { // if reply is false, it should be client halfclose. // ignore error and read data again. if !reply { pc.Snell.reply = false return pc.Snell.Read(b) } } return n, err } func (pc *PoolConn) Write(b []byte) (int, error) { return pc.Snell.Write(b) } func (pc *PoolConn) Close() error { // mihomo use SetReadDeadline to break bidirectional copy between client and server. // reset it before reuse connection to avoid io timeout error. _ = pc.Snell.Conn.SetReadDeadline(time.Time{}) pc.pool.Put(pc.Snell) return nil } func NewPool(factory func(context.Context) (*Snell, error)) *Pool { p := pool.New[*Snell]( func(ctx context.Context) (*Snell, error) { return factory(ctx) }, pool.WithAge[*Snell](15000), pool.WithSize[*Snell](10), pool.WithEvict[*Snell](func(item *Snell) { _ = item.Close() }), ) return &Pool{pool: p} } ================================================ FILE: core/Clash.Meta/transport/snell/snell.go ================================================ package snell import ( "encoding/binary" "errors" "fmt" "io" "net" "sync" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" "github.com/metacubex/mihomo/transport/socks5" ) const ( Version1 = 1 Version2 = 2 Version3 = 3 DefaultSnellVersion = Version1 // max packet length maxLength = 0x3FFF ) const ( CommandPing byte = 0 CommandConnect byte = 1 CommandConnectV2 byte = 5 CommandUDP byte = 6 CommondUDPForward byte = 1 CommandTunnel byte = 0 CommandPong byte = 1 CommandError byte = 2 Version byte = 1 ) var endSignal = []byte{} type Snell struct { net.Conn buffer [1]byte reply bool } func (s *Snell) Read(b []byte) (int, error) { if s.reply { return s.Conn.Read(b) } s.reply = true if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { return 0, err } if s.buffer[0] == CommandTunnel { return s.Conn.Read(b) } else if s.buffer[0] != CommandError { return 0, errors.New("command not support") } // CommandError // 1 byte error code if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { return 0, err } errcode := int(s.buffer[0]) // 1 byte error message length if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { return 0, err } length := int(s.buffer[0]) msg := make([]byte, length) if _, err := io.ReadFull(s.Conn, msg); err != nil { return 0, err } return 0, fmt.Errorf("server reported code: %d, message: %s", errcode, string(msg)) } func WriteHeader(conn net.Conn, host string, port uint, version int) error { buf := pool.GetBuffer() defer pool.PutBuffer(buf) buf.WriteByte(Version) if version == Version2 { buf.WriteByte(CommandConnectV2) } else { buf.WriteByte(CommandConnect) } // clientID length & id buf.WriteByte(0) // host & port buf.WriteByte(uint8(len(host))) buf.WriteString(host) binary.Write(buf, binary.BigEndian, uint16(port)) if _, err := conn.Write(buf.Bytes()); err != nil { return err } return nil } func WriteUDPHeader(conn net.Conn, version int) error { if version < Version3 { return errors.New("unsupport UDP version") } // version, command, clientID length _, err := conn.Write([]byte{Version, CommandUDP, 0x00}) return err } // HalfClose works only on version2 func HalfClose(conn net.Conn) error { if _, err := conn.Write(endSignal); err != nil { return err } if s, ok := conn.(*Snell); ok { s.reply = false } return nil } func StreamConn(conn net.Conn, psk []byte, version int) *Snell { var cipher shadowaead.Cipher if version != Version1 { cipher = NewAES128GCM(psk) } else { cipher = NewChacha20Poly1305(psk) } return &Snell{Conn: shadowaead.NewConn(conn, cipher)} } func PacketConn(conn net.Conn) net.PacketConn { return &packetConn{ Conn: conn, } } func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { buf := pool.GetBuffer() defer pool.PutBuffer(buf) // compose snell UDP address format (refer: icpz/snell-server-reversed) // a brand new wheel to replace socks5 address format, well done Yachen buf.WriteByte(CommondUDPForward) switch socks5Addr[0] { case socks5.AtypDomainName: hostLen := socks5Addr[1] buf.Write(socks5Addr[1 : 1+1+hostLen+2]) case socks5.AtypIPv4: buf.Write([]byte{0x00, 0x04}) buf.Write(socks5Addr[1 : 1+net.IPv4len+2]) case socks5.AtypIPv6: buf.Write([]byte{0x00, 0x06}) buf.Write(socks5Addr[1 : 1+net.IPv6len+2]) } buf.Write(payload) _, err := w.Write(buf.Bytes()) if err != nil { return 0, err } return len(payload), nil } func WritePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { if len(payload) <= maxLength { return writePacket(w, socks5Addr, payload) } offset := 0 total := len(payload) for { cursor := offset + maxLength if cursor > total { cursor = total } n, err := writePacket(w, socks5Addr, payload[offset:cursor]) if err != nil { return offset + n, err } offset = cursor if offset == total { break } } return total, nil } func ReadPacket(r io.Reader, payload []byte) (net.Addr, int, error) { buf := pool.Get(pool.UDPBufferSize) defer pool.Put(buf) n, err := r.Read(buf) headLen := 1 if err != nil { return nil, 0, err } if n < headLen { return nil, 0, errors.New("insufficient UDP length") } // parse snell UDP response address format switch buf[0] { case 0x04: headLen += net.IPv4len + 2 if n < headLen { err = errors.New("insufficient UDP length") break } buf[0] = socks5.AtypIPv4 case 0x06: headLen += net.IPv6len + 2 if n < headLen { err = errors.New("insufficient UDP length") break } buf[0] = socks5.AtypIPv6 default: err = errors.New("ip version invalid") } if err != nil { return nil, 0, err } addr := socks5.SplitAddr(buf[0:]) if addr == nil { return nil, 0, errors.New("remote address invalid") } uAddr := addr.UDPAddr() if uAddr == nil { return nil, 0, errors.New("parse addr error") } length := len(payload) if n-headLen < length { length = n - headLen } copy(payload[:], buf[headLen:headLen+length]) return uAddr, length, nil } type packetConn struct { net.Conn rMux sync.Mutex wMux sync.Mutex } func (pc *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { pc.wMux.Lock() defer pc.wMux.Unlock() return WritePacket(pc, socks5.ParseAddrToSocksAddr(addr), b) } func (pc *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { pc.rMux.Lock() defer pc.rMux.Unlock() addr, n, err := ReadPacket(pc.Conn, b) if err != nil { return 0, nil, err } return n, addr, nil } ================================================ FILE: core/Clash.Meta/transport/socks4/socks4.go ================================================ package socks4 import ( "bytes" "encoding/binary" "errors" "io" "net" "net/netip" "strconv" "github.com/metacubex/mihomo/component/auth" ) const Version = 0x04 type Command = uint8 const ( CmdConnect Command = 0x01 CmdBind Command = 0x02 ) type Code = uint8 const ( RequestGranted Code = 90 RequestRejected Code = 91 RequestIdentdFailed Code = 92 RequestIdentdMismatched Code = 93 ) var ( errVersionMismatched = errors.New("version code mismatched") errCommandNotSupported = errors.New("command not supported") errIPv6NotSupported = errors.New("IPv6 not supported") ErrRequestRejected = errors.New("request rejected or failed") ErrRequestIdentdFailed = errors.New("request rejected because SOCKS server cannot connect to identd on the client") ErrRequestIdentdMismatched = errors.New("request rejected because the client program and identd report different user-ids") ErrRequestUnknownCode = errors.New("request failed with unknown code") ) var subnet = netip.PrefixFrom(netip.IPv4Unspecified(), 24) func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr string, command Command, user string, err error) { var req [8]byte if _, err = io.ReadFull(rw, req[:]); err != nil { return } if req[0] != Version { err = errVersionMismatched return } if command = req[1]; command != CmdConnect { err = errCommandNotSupported return } var ( dstIP = netip.AddrFrom4(*(*[4]byte)(req[4:8])) // [4]byte dstPort = req[2:4] // [2]byte ) var ( host string port string code uint8 userID []byte ) if userID, err = readUntilNull(rw); err != nil { return } user = string(userID) if isReservedIP(dstIP) { var target []byte if target, err = readUntilNull(rw); err != nil { return } host = string(target) } port = strconv.Itoa(int(binary.BigEndian.Uint16(dstPort))) if host != "" { addr = net.JoinHostPort(host, port) } else { addr = net.JoinHostPort(dstIP.String(), port) } // SOCKS4 only support USERID auth. if authenticator == nil || authenticator.Verify(user, "") { code = RequestGranted } else { code = RequestIdentdMismatched err = ErrRequestIdentdMismatched } var reply [8]byte reply[0] = 0x00 // reply code reply[1] = code // result code copy(reply[4:8], dstIP.AsSlice()) copy(reply[2:4], dstPort) _, wErr := rw.Write(reply[:]) if err == nil { err = wErr } return } func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID string) (err error) { host, portStr, err := net.SplitHostPort(addr) if err != nil { return err } port, err := strconv.ParseUint(portStr, 10, 16) if err != nil { return err } dstIP, err := netip.ParseAddr(host) if err != nil /* HOST */ { dstIP = netip.AddrFrom4([4]byte{0, 0, 0, 1}) } else if dstIP.Is6() /* IPv6 */ { return errIPv6NotSupported } req := &bytes.Buffer{} req.WriteByte(Version) req.WriteByte(command) _ = binary.Write(req, binary.BigEndian, uint16(port)) req.Write(dstIP.AsSlice()) req.WriteString(userID) req.WriteByte(0) /* NULL */ if isReservedIP(dstIP) /* SOCKS4A */ { req.WriteString(host) req.WriteByte(0) /* NULL */ } if _, err = rw.Write(req.Bytes()); err != nil { return err } var resp [8]byte if _, err = io.ReadFull(rw, resp[:]); err != nil { return err } if resp[0] != 0x00 { return errVersionMismatched } switch resp[1] { case RequestGranted: return nil case RequestRejected: return ErrRequestRejected case RequestIdentdFailed: return ErrRequestIdentdFailed case RequestIdentdMismatched: return ErrRequestIdentdMismatched default: return ErrRequestUnknownCode } } // For version 4A, if the client cannot resolve the destination host's // domain name to find its IP address, it should set the first three bytes // of DSTIP to NULL and the last byte to a non-zero value. (This corresponds // to IP address 0.0.0.x, with x nonzero. As decreed by IANA -- The // Internet Assigned Numbers Authority -- such an address is inadmissible // as a destination IP address and thus should never occur if the client // can resolve the domain name.) func isReservedIP(ip netip.Addr) bool { return !ip.IsUnspecified() && subnet.Contains(ip) } func readUntilNull(r io.Reader) ([]byte, error) { buf := &bytes.Buffer{} var data [1]byte for { if _, err := r.Read(data[:]); err != nil { return nil, err } if data[0] == 0 { return buf.Bytes(), nil } buf.WriteByte(data[0]) } } ================================================ FILE: core/Clash.Meta/transport/socks5/socks5.go ================================================ package socks5 import ( "bytes" "encoding/binary" "errors" "io" "net" "net/netip" "strconv" "github.com/metacubex/mihomo/component/auth" ) // Error represents a SOCKS error type Error byte func (err Error) Error() string { return "SOCKS error: " + strconv.Itoa(int(err)) } // Command is request commands as defined in RFC 1928 section 4. type Command = uint8 const Version = 5 // SOCKS request commands as defined in RFC 1928 section 4. const ( CmdConnect Command = 1 CmdBind Command = 2 CmdUDPAssociate Command = 3 ) // SOCKS address types as defined in RFC 1928 section 5. const ( AtypIPv4 = 1 AtypDomainName = 3 AtypIPv6 = 4 ) // MaxAddrLen is the maximum size of SOCKS address in bytes. const MaxAddrLen = 1 + 1 + 255 + 2 // MaxAuthLen is the maximum size of user/password field in SOCKS5 Auth const MaxAuthLen = 255 // Addr represents a SOCKS address as defined in RFC 1928 section 5. type Addr []byte func (a Addr) String() string { var host, port string switch a[0] { case AtypDomainName: hostLen := uint16(a[1]) host = string(a[2 : 2+hostLen]) port = strconv.Itoa((int(a[2+hostLen]) << 8) | int(a[2+hostLen+1])) case AtypIPv4: host = net.IP(a[1 : 1+net.IPv4len]).String() port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1])) case AtypIPv6: host = net.IP(a[1 : 1+net.IPv6len]).String() port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1])) } return net.JoinHostPort(host, port) } // UDPAddr converts a socks5.Addr to *net.UDPAddr func (a Addr) UDPAddr() *net.UDPAddr { if len(a) == 0 { return nil } switch a[0] { case AtypIPv4: var ip [net.IPv4len]byte copy(ip[0:], a[1:1+net.IPv4len]) return &net.UDPAddr{IP: net.IP(ip[:]), Port: int(binary.BigEndian.Uint16(a[1+net.IPv4len : 1+net.IPv4len+2]))} case AtypIPv6: var ip [net.IPv6len]byte copy(ip[0:], a[1:1+net.IPv6len]) return &net.UDPAddr{IP: net.IP(ip[:]), Port: int(binary.BigEndian.Uint16(a[1+net.IPv6len : 1+net.IPv6len+2]))} } // Other Atyp return nil } // SOCKS errors as defined in RFC 1928 section 6. const ( ErrGeneralFailure = Error(1) ErrConnectionNotAllowed = Error(2) ErrNetworkUnreachable = Error(3) ErrHostUnreachable = Error(4) ErrConnectionRefused = Error(5) ErrTTLExpired = Error(6) ErrCommandNotSupported = Error(7) ErrAddressNotSupported = Error(8) ) // Auth errors used to return a specific "Auth failed" error var ErrAuth = errors.New("auth failed") type User struct { Username string Password string } // ServerHandshake fast-tracks SOCKS initialization to get target address to connect on server side. func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, user string, err error) { // Read RFC 1928 for request and reply structure and sizes. buf := make([]byte, MaxAddrLen) // read VER, NMETHODS, METHODS if _, err = io.ReadFull(rw, buf[:2]); err != nil { return } nmethods := buf[1] if _, err = io.ReadFull(rw, buf[:nmethods]); err != nil { return } if nmethods == 1 && buf[0] == 0x02 /* will use password */ && authenticator == nil { authenticator = auth.AlwaysValid } // write VER METHOD if authenticator != nil { if _, err = rw.Write([]byte{5, 2}); err != nil { return } // Get header header := make([]byte, 2) if _, err = io.ReadFull(rw, header); err != nil { return } authBuf := make([]byte, MaxAuthLen) // Get username userLen := int(header[1]) if userLen <= 0 { rw.Write([]byte{1, 1}) err = ErrAuth return } if _, err = io.ReadFull(rw, authBuf[:userLen]); err != nil { return } user = string(authBuf[:userLen]) // Get password if _, err = rw.Read(header[:1]); err != nil { return } passLen := int(header[0]) if passLen <= 0 { rw.Write([]byte{1, 1}) err = ErrAuth return } if _, err = io.ReadFull(rw, authBuf[:passLen]); err != nil { return } pass := string(authBuf[:passLen]) // Verify if ok := authenticator.Verify(string(user), string(pass)); !ok { rw.Write([]byte{1, 1}) err = ErrAuth return } // Response auth state if _, err = rw.Write([]byte{1, 0}); err != nil { return } } else { if _, err = rw.Write([]byte{5, 0}); err != nil { return } } // read VER CMD RSV ATYP DST.ADDR DST.PORT if _, err = io.ReadFull(rw, buf[:3]); err != nil { return } command = buf[1] addr, err = ReadAddr(rw, buf) if err != nil { return } switch command { case CmdConnect, CmdUDPAssociate: // Acquire server listened address info localAddr := ParseAddrToSocksAddr(rw.LocalAddr()) if localAddr == nil { err = ErrAddressNotSupported } else { // write VER REP RSV ATYP BND.ADDR BND.PORT _, err = rw.Write(bytes.Join([][]byte{{5, 0, 0}, localAddr}, []byte{})) } case CmdBind: fallthrough default: err = ErrCommandNotSupported } return } // ClientHandshake fast-tracks SOCKS initialization to get target address to connect on client side. func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (Addr, error) { buf := make([]byte, MaxAddrLen) var err error // VER, NMETHODS, METHODS if user != nil { _, err = rw.Write([]byte{5, 1, 2}) } else { _, err = rw.Write([]byte{5, 1, 0}) } if err != nil { return nil, err } // VER, METHOD if _, err := io.ReadFull(rw, buf[:2]); err != nil { return nil, err } if buf[0] != 5 { return nil, errors.New("SOCKS version error") } if buf[1] == 2 { if user == nil { return nil, ErrAuth } // password protocol version authMsg := &bytes.Buffer{} authMsg.WriteByte(1) authMsg.WriteByte(uint8(len(user.Username))) authMsg.WriteString(user.Username) authMsg.WriteByte(uint8(len(user.Password))) authMsg.WriteString(user.Password) if _, err := rw.Write(authMsg.Bytes()); err != nil { return nil, err } if _, err := io.ReadFull(rw, buf[:2]); err != nil { return nil, err } if buf[1] != 0 { return nil, errors.New("rejected username/password") } } else if buf[1] != 0 { return nil, errors.New("SOCKS need auth") } // VER, CMD, RSV, ADDR if _, err := rw.Write(bytes.Join([][]byte{{5, command, 0}, addr}, []byte{})); err != nil { return nil, err } // VER, REP, RSV if _, err := io.ReadFull(rw, buf[:3]); err != nil { return nil, err } return ReadAddr(rw, buf) } func ReadAddr(r io.Reader, b []byte) (Addr, error) { if len(b) < MaxAddrLen { return nil, io.ErrShortBuffer } _, err := io.ReadFull(r, b[:1]) // read 1st byte for address type if err != nil { return nil, err } switch b[0] { case AtypDomainName: _, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length if err != nil { return nil, err } domainLength := uint16(b[1]) _, err = io.ReadFull(r, b[2:2+domainLength+2]) return b[:1+1+domainLength+2], err case AtypIPv4: _, err = io.ReadFull(r, b[1:1+net.IPv4len+2]) return b[:1+net.IPv4len+2], err case AtypIPv6: _, err = io.ReadFull(r, b[1:1+net.IPv6len+2]) return b[:1+net.IPv6len+2], err } return nil, ErrAddressNotSupported } func ReadAddr0(r io.Reader) (Addr, error) { aType, err := ReadByte(r) // read 1st byte for address type if err != nil { return nil, err } switch aType { case AtypDomainName: var domainLength byte domainLength, err = ReadByte(r) // read 2nd byte for domain length if err != nil { return nil, err } b := make([]byte, 1+1+uint16(domainLength)+2) _, err = io.ReadFull(r, b[2:]) b[0] = aType b[1] = domainLength return b, err case AtypIPv4: var b [1 + net.IPv4len + 2]byte _, err = io.ReadFull(r, b[1:]) b[0] = aType return b[:], err case AtypIPv6: var b [1 + net.IPv6len + 2]byte _, err = io.ReadFull(r, b[1:]) b[0] = aType return b[:], err } return nil, ErrAddressNotSupported } func ReadByte(reader io.Reader) (byte, error) { if br, isBr := reader.(io.ByteReader); isBr { return br.ReadByte() } var b [1]byte if _, err := io.ReadFull(reader, b[:]); err != nil { return 0, err } return b[0], nil } // SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed. func SplitAddr(b []byte) Addr { addrLen := 1 if len(b) < addrLen { return nil } switch b[0] { case AtypDomainName: if len(b) < 2 { return nil } addrLen = 1 + 1 + int(b[1]) + 2 case AtypIPv4: addrLen = 1 + net.IPv4len + 2 case AtypIPv6: addrLen = 1 + net.IPv6len + 2 default: return nil } if len(b) < addrLen { return nil } return b[:addrLen] } // ParseAddr parses the address in string s. Returns nil if failed. func ParseAddr(s string) Addr { var addr Addr host, port, err := net.SplitHostPort(s) if err != nil { return nil } if ip := net.ParseIP(host); ip != nil { if ip4 := ip.To4(); ip4 != nil { addr = make([]byte, 1+net.IPv4len+2) addr[0] = AtypIPv4 copy(addr[1:], ip4) } else { addr = make([]byte, 1+net.IPv6len+2) addr[0] = AtypIPv6 copy(addr[1:], ip) } } else { if len(host) > 255 { return nil } addr = make([]byte, 1+1+len(host)+2) addr[0] = AtypDomainName addr[1] = byte(len(host)) copy(addr[2:], host) } portnum, err := strconv.ParseUint(port, 10, 16) if err != nil { return nil } addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum) return addr } // ParseAddrToSocksAddr parse a socks addr from net.addr // This is a fast path of ParseAddr(addr.String()) func ParseAddrToSocksAddr(addr net.Addr) Addr { var hostip net.IP var port int switch addr := addr.(type) { case *net.UDPAddr: hostip = addr.IP port = addr.Port case *net.TCPAddr: hostip = addr.IP port = addr.Port case nil: return nil } // fallback parse if hostip == nil { return ParseAddr(addr.String()) } var parsed Addr if ip4 := hostip.To4(); ip4.DefaultMask() != nil { parsed = make([]byte, 1+net.IPv4len+2) parsed[0] = AtypIPv4 copy(parsed[1:], ip4) binary.BigEndian.PutUint16(parsed[1+net.IPv4len:], uint16(port)) } else { parsed = make([]byte, 1+net.IPv6len+2) parsed[0] = AtypIPv6 copy(parsed[1:], hostip) binary.BigEndian.PutUint16(parsed[1+net.IPv6len:], uint16(port)) } return parsed } func AddrFromStdAddrPort(addrPort netip.AddrPort) Addr { addr := addrPort.Addr() if addr.Is4() { ip4 := addr.As4() return []byte{AtypIPv4, ip4[0], ip4[1], ip4[2], ip4[3], byte(addrPort.Port() >> 8), byte(addrPort.Port())} } buf := make([]byte, 1+net.IPv6len+2) buf[0] = AtypIPv6 copy(buf[1:], addr.AsSlice()) buf[1+net.IPv6len] = byte(addrPort.Port() >> 8) buf[1+net.IPv6len+1] = byte(addrPort.Port()) return buf } // DecodeUDPPacket split `packet` to addr payload, and this function is mutable with `packet` func DecodeUDPPacket(packet []byte) (addr Addr, payload []byte, err error) { if len(packet) < 5 { err = errors.New("insufficient length of packet") return } // packet[0] and packet[1] are reserved if !bytes.Equal(packet[:2], []byte{0, 0}) { err = errors.New("reserved fields should be zero") return } if packet[2] != 0 /* fragments */ { err = errors.New("discarding fragmented payload") return } addr = SplitAddr(packet[3:]) if addr == nil { err = errors.New("failed to read UDP header") } payload = packet[3+len(addr):] return } func EncodeUDPPacket(addr Addr, payload []byte) (packet []byte, err error) { if addr == nil { err = errors.New("address is invalid") return } packet = bytes.Join([][]byte{{0, 0, 0}, addr, payload}, []byte{}) return } ================================================ FILE: core/Clash.Meta/transport/ssr/obfs/base.go ================================================ package obfs type Base struct { Host string Port int Key []byte IVSize int Param string } ================================================ FILE: core/Clash.Meta/transport/ssr/obfs/http_post.go ================================================ package obfs func init() { register("http_post", newHTTPPost, 0) } func newHTTPPost(b *Base) Obfs { return &httpObfs{Base: b, post: true} } ================================================ FILE: core/Clash.Meta/transport/ssr/obfs/http_simple.go ================================================ package obfs import ( "bytes" "encoding/hex" "io" "net" "strconv" "strings" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/randv2" ) func init() { register("http_simple", newHTTPSimple, 0) } type httpObfs struct { *Base post bool } func newHTTPSimple(b *Base) Obfs { return &httpObfs{Base: b} } type httpConn struct { net.Conn *httpObfs hasSentHeader bool hasRecvHeader bool buf []byte } func (h *httpObfs) StreamConn(c net.Conn) net.Conn { return &httpConn{Conn: c, httpObfs: h} } func (c *httpConn) Read(b []byte) (int, error) { if c.buf != nil { n := copy(b, c.buf) if n == len(c.buf) { c.buf = nil } else { c.buf = c.buf[n:] } return n, nil } if c.hasRecvHeader { return c.Conn.Read(b) } buf := pool.Get(pool.RelayBufferSize) defer pool.Put(buf) n, err := c.Conn.Read(buf) if err != nil { return 0, err } pos := bytes.Index(buf[:n], []byte("\r\n\r\n")) if pos == -1 { return 0, io.EOF } c.hasRecvHeader = true dataLength := n - pos - 4 n = copy(b, buf[4+pos:n]) if dataLength > n { c.buf = append(c.buf, buf[4+pos+n:4+pos+dataLength]...) } return n, nil } func (c *httpConn) Write(b []byte) (int, error) { if c.hasSentHeader { return c.Conn.Write(b) } // 30: head length headLength := c.IVSize + 30 bLength := len(b) headDataLength := bLength if bLength-headLength > 64 { headDataLength = headLength + randv2.IntN(65) } headData := b[:headDataLength] b = b[headDataLength:] var body string host := c.Host if len(c.Param) > 0 { pos := strings.Index(c.Param, "#") if pos != -1 { body = strings.ReplaceAll(c.Param[pos+1:], "\n", "\r\n") body = strings.ReplaceAll(body, "\\n", "\r\n") host = c.Param[:pos] } else { host = c.Param } } hosts := strings.Split(host, ",") host = hosts[randv2.IntN(len(hosts))] buf := pool.GetBuffer() defer pool.PutBuffer(buf) if c.post { buf.WriteString("POST /") } else { buf.WriteString("GET /") } packURLEncodedHeadData(buf, headData) buf.WriteString(" HTTP/1.1\r\nHost: " + host) if c.Port != 80 { buf.WriteString(":" + strconv.Itoa(c.Port)) } buf.WriteString("\r\n") if len(body) > 0 { buf.WriteString(body + "\r\n\r\n") } else { buf.WriteString("User-Agent: ") buf.WriteString(userAgent[randv2.IntN(len(userAgent))]) buf.WriteString("\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n") if c.post { packBoundary(buf) } buf.WriteString("DNT: 1\r\nConnection: keep-alive\r\n\r\n") } buf.Write(b) _, err := c.Conn.Write(buf.Bytes()) if err != nil { return 0, err } c.hasSentHeader = true return bLength, nil } func packURLEncodedHeadData(buf *bytes.Buffer, data []byte) { dataLength := len(data) for i := 0; i < dataLength; i++ { buf.WriteRune('%') buf.WriteString(hex.EncodeToString(data[i : i+1])) } } func packBoundary(buf *bytes.Buffer) { buf.WriteString("Content-Type: multipart/form-data; boundary=") set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" for i := 0; i < 32; i++ { buf.WriteByte(set[randv2.IntN(62)]) } buf.WriteString("\r\n") } var userAgent = []string{ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36", "Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0", "Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36", "Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", "Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", "Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", "Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", "Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", "Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1", "Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", "Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", "Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36", "Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", } ================================================ FILE: core/Clash.Meta/transport/ssr/obfs/obfs.go ================================================ package obfs import ( "errors" "fmt" "net" ) var ( errTLS12TicketAuthIncorrectMagicNumber = errors.New("tls1.2_ticket_auth incorrect magic number") errTLS12TicketAuthTooShortData = errors.New("tls1.2_ticket_auth too short data") errTLS12TicketAuthHMACError = errors.New("tls1.2_ticket_auth hmac verifying failed") ) type authData struct { clientID [32]byte } type Obfs interface { StreamConn(net.Conn) net.Conn } type obfsCreator func(b *Base) Obfs var obfsList = make(map[string]struct { overhead int new obfsCreator }) func register(name string, c obfsCreator, o int) { obfsList[name] = struct { overhead int new obfsCreator }{overhead: o, new: c} } func PickObfs(name string, b *Base) (Obfs, int, error) { if choice, ok := obfsList[name]; ok { return choice.new(b), choice.overhead, nil } return nil, 0, fmt.Errorf("Obfs %s not supported", name) } ================================================ FILE: core/Clash.Meta/transport/ssr/obfs/plain.go ================================================ package obfs import "net" type plain struct{} func init() { register("plain", newPlain, 0) } func newPlain(b *Base) Obfs { return &plain{} } func (p *plain) StreamConn(c net.Conn) net.Conn { return c } ================================================ FILE: core/Clash.Meta/transport/ssr/obfs/random_head.go ================================================ package obfs import ( "crypto/rand" "encoding/binary" "hash/crc32" "net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/randv2" ) func init() { register("random_head", newRandomHead, 0) } type randomHead struct { *Base } func newRandomHead(b *Base) Obfs { return &randomHead{Base: b} } type randomHeadConn struct { net.Conn *randomHead hasSentHeader bool rawTransSent bool rawTransRecv bool buf []byte } func (r *randomHead) StreamConn(c net.Conn) net.Conn { return &randomHeadConn{Conn: c, randomHead: r} } func (c *randomHeadConn) Read(b []byte) (int, error) { if c.rawTransRecv { return c.Conn.Read(b) } buf := pool.Get(pool.RelayBufferSize) defer pool.Put(buf) c.Conn.Read(buf) c.rawTransRecv = true c.Write(nil) return 0, nil } func (c *randomHeadConn) Write(b []byte) (int, error) { if c.rawTransSent { return c.Conn.Write(b) } c.buf = append(c.buf, b...) if !c.hasSentHeader { c.hasSentHeader = true dataLength := randv2.IntN(96) + 4 buf := pool.Get(dataLength + 4) defer pool.Put(buf) rand.Read(buf[:dataLength]) binary.LittleEndian.PutUint32(buf[dataLength:], 0xffffffff-crc32.ChecksumIEEE(buf[:dataLength])) _, err := c.Conn.Write(buf) return len(b), err } if c.rawTransRecv { _, err := c.Conn.Write(c.buf) c.buf = nil c.rawTransSent = true return len(b), err } return len(b), nil } ================================================ FILE: core/Clash.Meta/transport/ssr/obfs/tls1.2_ticket_auth.go ================================================ package obfs import ( "bytes" "crypto/hmac" "crypto/rand" "encoding/binary" "net" "strings" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/ssr/tools" "github.com/metacubex/randv2" ) func init() { register("tls1.2_ticket_auth", newTLS12Ticket, 5) register("tls1.2_ticket_fastauth", newTLS12Ticket, 5) } type tls12Ticket struct { *Base *authData } func newTLS12Ticket(b *Base) Obfs { r := &tls12Ticket{Base: b, authData: &authData{}} rand.Read(r.clientID[:]) return r } type tls12TicketConn struct { net.Conn *tls12Ticket handshakeStatus int decoded bytes.Buffer underDecoded bytes.Buffer sendBuf bytes.Buffer } func (t *tls12Ticket) StreamConn(c net.Conn) net.Conn { return &tls12TicketConn{Conn: c, tls12Ticket: t} } func (c *tls12TicketConn) Read(b []byte) (int, error) { if c.decoded.Len() > 0 { return c.decoded.Read(b) } buf := pool.Get(pool.RelayBufferSize) defer pool.Put(buf) n, err := c.Conn.Read(buf) if err != nil { return 0, err } if c.handshakeStatus == 8 { c.underDecoded.Write(buf[:n]) for c.underDecoded.Len() > 5 { if !bytes.Equal(c.underDecoded.Bytes()[:3], []byte{0x17, 3, 3}) { c.underDecoded.Reset() return 0, errTLS12TicketAuthIncorrectMagicNumber } size := int(binary.BigEndian.Uint16(c.underDecoded.Bytes()[3:5])) if c.underDecoded.Len() < 5+size { break } c.underDecoded.Next(5) c.decoded.Write(c.underDecoded.Next(size)) } n, _ = c.decoded.Read(b) return n, nil } if n < 11+32+1+32 { return 0, errTLS12TicketAuthTooShortData } if !hmac.Equal(buf[33:43], c.hmacSHA1(buf[11:33])[:10]) || !hmac.Equal(buf[n-10:n], c.hmacSHA1(buf[:n-10])[:10]) { return 0, errTLS12TicketAuthHMACError } c.Write(nil) return 0, nil } func (c *tls12TicketConn) Write(b []byte) (int, error) { length := len(b) if c.handshakeStatus == 8 { buf := pool.GetBuffer() defer pool.PutBuffer(buf) for len(b) > 2048 { size := randv2.IntN(4096) + 100 if len(b) < size { size = len(b) } packData(buf, b[:size]) b = b[size:] } if len(b) > 0 { packData(buf, b) } _, err := c.Conn.Write(buf.Bytes()) if err != nil { return 0, err } return length, nil } if len(b) > 0 { packData(&c.sendBuf, b) } if c.handshakeStatus == 0 { c.handshakeStatus = 1 data := pool.GetBuffer() defer pool.PutBuffer(data) data.Write([]byte{3, 3}) c.packAuthData(data) data.WriteByte(0x20) data.Write(c.clientID[:]) data.Write([]byte{0x00, 0x1c, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x0a, 0xc0, 0x14, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x9c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x0a}) data.Write([]byte{0x1, 0x0}) ext := pool.GetBuffer() defer pool.PutBuffer(ext) host := c.getHost() ext.Write([]byte{0xff, 0x01, 0x00, 0x01, 0x00}) packSNIData(ext, host) ext.Write([]byte{0, 0x17, 0, 0}) c.packTicketBuf(ext, host) ext.Write([]byte{0x00, 0x0d, 0x00, 0x16, 0x00, 0x14, 0x06, 0x01, 0x06, 0x03, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03}) ext.Write([]byte{0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00}) ext.Write([]byte{0x00, 0x12, 0x00, 0x00}) ext.Write([]byte{0x75, 0x50, 0x00, 0x00}) ext.Write([]byte{0x00, 0x0b, 0x00, 0x02, 0x01, 0x00}) ext.Write([]byte{0x00, 0x0a, 0x00, 0x06, 0x00, 0x04, 0x00, 0x17, 0x00, 0x18}) binary.Write(data, binary.BigEndian, uint16(ext.Len())) data.ReadFrom(ext) ret := pool.GetBuffer() defer pool.PutBuffer(ret) ret.Write([]byte{0x16, 3, 1}) binary.Write(ret, binary.BigEndian, uint16(data.Len()+4)) ret.Write([]byte{1, 0}) binary.Write(ret, binary.BigEndian, uint16(data.Len())) ret.ReadFrom(data) _, err := c.Conn.Write(ret.Bytes()) if err != nil { return 0, err } return length, nil } else if c.handshakeStatus == 1 && len(b) == 0 { buf := pool.GetBuffer() defer pool.PutBuffer(buf) buf.Write([]byte{0x14, 3, 3, 0, 1, 1, 0x16, 3, 3, 0, 0x20}) tools.AppendRandBytes(buf, 22) buf.Write(c.hmacSHA1(buf.Bytes())[:10]) buf.ReadFrom(&c.sendBuf) c.handshakeStatus = 8 _, err := c.Conn.Write(buf.Bytes()) return 0, err } return length, nil } func packData(buf *bytes.Buffer, data []byte) { buf.Write([]byte{0x17, 3, 3}) binary.Write(buf, binary.BigEndian, uint16(len(data))) buf.Write(data) } func (t *tls12Ticket) packAuthData(buf *bytes.Buffer) { binary.Write(buf, binary.BigEndian, uint32(ntp.Now().Unix())) tools.AppendRandBytes(buf, 18) buf.Write(t.hmacSHA1(buf.Bytes()[buf.Len()-22:])[:10]) } func packSNIData(buf *bytes.Buffer, u string) { len := uint16(len(u)) buf.Write([]byte{0, 0}) binary.Write(buf, binary.BigEndian, len+5) binary.Write(buf, binary.BigEndian, len+3) buf.WriteByte(0) binary.Write(buf, binary.BigEndian, len) buf.WriteString(u) } func (c *tls12TicketConn) packTicketBuf(buf *bytes.Buffer, u string) { length := 16 * (randv2.IntN(17) + 8) buf.Write([]byte{0, 0x23}) binary.Write(buf, binary.BigEndian, uint16(length)) tools.AppendRandBytes(buf, length) } func (t *tls12Ticket) hmacSHA1(data []byte) []byte { key := pool.Get(len(t.Key) + 32) defer pool.Put(key) copy(key, t.Key) copy(key[len(t.Key):], t.clientID[:]) sha1Data := tools.HmacSHA1(key, data) return sha1Data[:10] } func (t *tls12Ticket) getHost() string { host := t.Param if len(host) == 0 { host = t.Host } if len(host) > 0 && host[len(host)-1] >= '0' && host[len(host)-1] <= '9' { host = "" } hosts := strings.Split(host, ",") host = hosts[randv2.IntN(len(hosts))] return host } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/auth_aes128_md5.go ================================================ package protocol import "github.com/metacubex/mihomo/transport/ssr/tools" func init() { register("auth_aes128_md5", newAuthAES128MD5, 9) } func newAuthAES128MD5(b *Base) Protocol { a := &authAES128{ Base: b, authData: &authData{}, authAES128Function: &authAES128Function{salt: "auth_aes128_md5", hmac: tools.HmacMD5, hashDigest: tools.MD5Sum}, userData: &userData{}, } a.initUserData() return a } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/auth_aes128_sha1.go ================================================ package protocol import ( "bytes" "crypto/rand" "encoding/binary" "math" "net" "strconv" "strings" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/ssr/tools" "github.com/metacubex/randv2" ) type ( hmacMethod func(key, data []byte) []byte hashDigestMethod func([]byte) []byte ) func init() { register("auth_aes128_sha1", newAuthAES128SHA1, 9) } type authAES128Function struct { salt string hmac hmacMethod hashDigest hashDigestMethod } type authAES128 struct { *Base *authData *authAES128Function *userData iv []byte hasSentHeader bool rawTrans bool packID uint32 recvID uint32 } func newAuthAES128SHA1(b *Base) Protocol { a := &authAES128{ Base: b, authData: &authData{}, authAES128Function: &authAES128Function{salt: "auth_aes128_sha1", hmac: tools.HmacSHA1, hashDigest: tools.SHA1Sum}, userData: &userData{}, } a.initUserData() return a } func (a *authAES128) initUserData() { params := strings.Split(a.Param, ":") if len(params) > 1 { if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil { binary.LittleEndian.PutUint32(a.userID[:], uint32(userID)) a.userKey = a.hashDigest([]byte(params[1])) } else { log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt) } } if len(a.userKey) == 0 { a.userKey = a.Key rand.Read(a.userID[:]) } } func (a *authAES128) StreamConn(c net.Conn, iv []byte) net.Conn { p := &authAES128{ Base: a.Base, authData: a.next(), authAES128Function: a.authAES128Function, userData: a.userData, packID: 1, recvID: 1, } p.iv = iv return &Conn{Conn: c, Protocol: p} } func (a *authAES128) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { p := &authAES128{ Base: a.Base, authAES128Function: a.authAES128Function, userData: a.userData, } return &PacketConn{EnhancePacketConn: c, Protocol: p} } func (a *authAES128) Decode(dst, src *bytes.Buffer) error { if a.rawTrans { dst.ReadFrom(src) return nil } for src.Len() > 4 { macKey := pool.Get(len(a.userKey) + 4) defer pool.Put(macKey) copy(macKey, a.userKey) binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID) if !bytes.Equal(a.hmac(macKey, src.Bytes()[:2])[:2], src.Bytes()[2:4]) { src.Reset() return errAuthAES128MACError } length := int(binary.LittleEndian.Uint16(src.Bytes()[:2])) if length >= 8192 || length < 7 { a.rawTrans = true src.Reset() return errAuthAES128LengthError } if length > src.Len() { break } if !bytes.Equal(a.hmac(macKey, src.Bytes()[:length-4])[:4], src.Bytes()[length-4:length]) { a.rawTrans = true src.Reset() return errAuthAES128ChksumError } a.recvID++ pos := int(src.Bytes()[4]) if pos < 255 { pos += 4 } else { pos = int(binary.LittleEndian.Uint16(src.Bytes()[5:7])) + 4 } dst.Write(src.Bytes()[pos : length-4]) src.Next(length) } return nil } func (a *authAES128) Encode(buf *bytes.Buffer, b []byte) error { fullDataLength := len(b) if !a.hasSentHeader { dataLength := getDataLength(b) a.packAuthData(buf, b[:dataLength]) b = b[dataLength:] a.hasSentHeader = true } for len(b) > 8100 { a.packData(buf, b[:8100], fullDataLength) b = b[8100:] } if len(b) > 0 { a.packData(buf, b, fullDataLength) } return nil } func (a *authAES128) DecodePacket(b []byte) ([]byte, error) { if len(b) < 4 { return nil, errAuthAES128LengthError } if !bytes.Equal(a.hmac(a.Key, b[:len(b)-4])[:4], b[len(b)-4:]) { return nil, errAuthAES128ChksumError } return b[:len(b)-4], nil } func (a *authAES128) EncodePacket(buf *bytes.Buffer, b []byte) error { buf.Write(b) buf.Write(a.userID[:]) buf.Write(a.hmac(a.userKey, buf.Bytes())[:4]) return nil } func (a *authAES128) packData(poolBuf *bytes.Buffer, data []byte, fullDataLength int) { dataLength := len(data) randDataLength := a.getRandDataLengthForPackData(dataLength, fullDataLength) /* 2: uint16 LittleEndian packedDataLength 2: hmac of packedDataLength 3: maxRandDataLengthPrefix (min:1) 4: hmac of packedData except the last 4 bytes */ packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4 if randDataLength < 128 { packedDataLength -= 2 } macKey := pool.Get(len(a.userKey) + 4) defer pool.Put(macKey) copy(macKey, a.userKey) binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID) a.packID++ binary.Write(poolBuf, binary.LittleEndian, uint16(packedDataLength)) poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-2:])[:2]) a.packRandData(poolBuf, randDataLength) poolBuf.Write(data) poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:])[:4]) } func trapezoidRandom(max int, d float64) int { base := randv2.Float64() if d-0 > 1e-6 { a := 1 - d base = (math.Sqrt(a*a+4*d*base) - a) / (2 * d) } return int(base * float64(max)) } func (a *authAES128) getRandDataLengthForPackData(dataLength, fullDataLength int) int { if fullDataLength >= 32*1024-a.Overhead { return 0 } // 1460: tcp_mss revLength := 1460 - dataLength - 9 if revLength == 0 { return 0 } if revLength < 0 { if revLength > -1460 { return trapezoidRandom(revLength+1460, -0.3) } return randv2.IntN(32) } if dataLength > 900 { return randv2.IntN(revLength) } return trapezoidRandom(revLength, -0.3) } func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { if len(data) == 0 { return } dataLength := len(data) randDataLength := a.getRandDataLengthForPackAuthData(dataLength) /* 7: checkHead(1) and hmac of checkHead(6) 4: userID 16: encrypted data of authdata(12), uint16 BigEndian packedDataLength(2) and uint16 BigEndian randDataLength(2) 4: hmac of userID and encrypted data 4: hmac of packedAuthData except the last 4 bytes */ packedAuthDataLength := 7 + 4 + 16 + 4 + randDataLength + dataLength + 4 macKey := pool.Get(len(a.iv) + len(a.Key)) defer pool.Put(macKey) copy(macKey, a.iv) copy(macKey[len(a.iv):], a.Key) poolBuf.WriteByte(byte(randv2.IntN(256))) poolBuf.Write(a.hmac(macKey, poolBuf.Bytes())[:6]) poolBuf.Write(a.userID[:]) err := a.authData.putEncryptedData(poolBuf, a.userKey, [2]int{packedAuthDataLength, randDataLength}, a.salt) if err != nil { poolBuf.Reset() return } poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[7:])[:4]) tools.AppendRandBytes(poolBuf, randDataLength) poolBuf.Write(data) poolBuf.Write(a.hmac(a.userKey, poolBuf.Bytes())[:4]) } func (a *authAES128) getRandDataLengthForPackAuthData(size int) int { if size > 400 { return randv2.IntN(512) } return randv2.IntN(1024) } func (a *authAES128) packRandData(poolBuf *bytes.Buffer, size int) { if size < 128 { poolBuf.WriteByte(byte(size + 1)) tools.AppendRandBytes(poolBuf, size) return } poolBuf.WriteByte(255) binary.Write(poolBuf, binary.LittleEndian, uint16(size+3)) tools.AppendRandBytes(poolBuf, size) } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/auth_chain_a.go ================================================ package protocol import ( "bytes" "crypto/cipher" "crypto/rand" "crypto/rc4" "encoding/base64" "encoding/binary" "errors" "net" "strconv" "strings" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/ssr/tools" ) func init() { register("auth_chain_a", newAuthChainA, 4) } type randDataLengthMethod func(int, []byte, *tools.XorShift128Plus) int type authChainA struct { *Base *authData *userData iv []byte salt string hasSentHeader bool rawTrans bool lastClientHash []byte lastServerHash []byte encrypter cipher.Stream decrypter cipher.Stream randomClient tools.XorShift128Plus randomServer tools.XorShift128Plus randDataLength randDataLengthMethod packID uint32 recvID uint32 } func newAuthChainA(b *Base) Protocol { a := &authChainA{ Base: b, authData: &authData{}, userData: &userData{}, salt: "auth_chain_a", } a.initUserData() return a } func (a *authChainA) initUserData() { params := strings.Split(a.Param, ":") if len(params) > 1 { if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil { binary.LittleEndian.PutUint32(a.userID[:], uint32(userID)) a.userKey = []byte(params[1]) } else { log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt) } } if len(a.userKey) == 0 { a.userKey = a.Key rand.Read(a.userID[:]) } } func (a *authChainA) StreamConn(c net.Conn, iv []byte) net.Conn { p := &authChainA{ Base: a.Base, authData: a.next(), userData: a.userData, salt: a.salt, packID: 1, recvID: 1, } p.iv = iv p.randDataLength = p.getRandLength return &Conn{Conn: c, Protocol: p} } func (a *authChainA) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { p := &authChainA{ Base: a.Base, salt: a.salt, userData: a.userData, } return &PacketConn{EnhancePacketConn: c, Protocol: p} } func (a *authChainA) Decode(dst, src *bytes.Buffer) error { if a.rawTrans { dst.ReadFrom(src) return nil } for src.Len() > 4 { macKey := pool.Get(len(a.userKey) + 4) defer pool.Put(macKey) copy(macKey, a.userKey) binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID) dataLength := int(binary.LittleEndian.Uint16(src.Bytes()[:2]) ^ binary.LittleEndian.Uint16(a.lastServerHash[14:16])) randDataLength := a.randDataLength(dataLength, a.lastServerHash, &a.randomServer) length := dataLength + randDataLength // Temporary workaround for https://github.com/metacubex/mihomo/issues/1352 if dataLength < 0 || randDataLength < 0 || length < 0 { return errors.New("ssr crashing blocked") } if length >= 4096 { a.rawTrans = true src.Reset() return errAuthChainLengthError } if 4+length > src.Len() { break } serverHash := tools.HmacMD5(macKey, src.Bytes()[:length+2]) if !bytes.Equal(serverHash[:2], src.Bytes()[length+2:length+4]) { a.rawTrans = true src.Reset() return errAuthChainChksumError } a.lastServerHash = serverHash pos := 2 if dataLength > 0 && randDataLength > 0 { pos += getRandStartPos(randDataLength, &a.randomServer) } // Temporary workaround for https://github.com/metacubex/mihomo/issues/1352 if pos < 0 || pos+dataLength < 0 || dataLength < 0 { return errors.New("ssr crashing blocked") } wantedData := src.Bytes()[pos : pos+dataLength] a.decrypter.XORKeyStream(wantedData, wantedData) if a.recvID == 1 { dst.Write(wantedData[2:]) } else { dst.Write(wantedData) } a.recvID++ src.Next(length + 4) } return nil } func (a *authChainA) Encode(buf *bytes.Buffer, b []byte) error { if !a.hasSentHeader { dataLength := getDataLength(b) a.packAuthData(buf, b[:dataLength]) b = b[dataLength:] a.hasSentHeader = true } for len(b) > 2800 { a.packData(buf, b[:2800]) b = b[2800:] } if len(b) > 0 { a.packData(buf, b) } return nil } func (a *authChainA) DecodePacket(b []byte) ([]byte, error) { if len(b) < 9 { return nil, errAuthChainLengthError } if !bytes.Equal(tools.HmacMD5(a.userKey, b[:len(b)-1])[:1], b[len(b)-1:]) { return nil, errAuthChainChksumError } md5Data := tools.HmacMD5(a.Key, b[len(b)-8:len(b)-1]) randDataLength := udpGetRandLength(md5Data, &a.randomServer) key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16) rc4Cipher, err := rc4.NewCipher(key) if err != nil { return nil, err } wantedData := b[:len(b)-8-randDataLength] rc4Cipher.XORKeyStream(wantedData, wantedData) return wantedData, nil } func (a *authChainA) EncodePacket(buf *bytes.Buffer, b []byte) error { authData := pool.Get(3) defer pool.Put(authData) rand.Read(authData) md5Data := tools.HmacMD5(a.Key, authData) randDataLength := udpGetRandLength(md5Data, &a.randomClient) key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16) rc4Cipher, err := rc4.NewCipher(key) if err != nil { return err } rc4Cipher.XORKeyStream(b, b) buf.Write(b) tools.AppendRandBytes(buf, randDataLength) buf.Write(authData) binary.Write(buf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(md5Data[:4])) buf.Write(tools.HmacMD5(a.userKey, buf.Bytes())[:1]) return nil } func (a *authChainA) packAuthData(poolBuf *bytes.Buffer, data []byte) { /* dataLength := len(data) 12: checkHead(4) and hmac of checkHead(8) 4: uint32 LittleEndian uid (uid = userID ^ last client hash) 16: encrypted data of authdata(12), uint16 LittleEndian overhead(2) and uint16 LittleEndian number zero(2) 4: last server hash(4) packedAuthDataLength := 12 + 4 + 16 + 4 + dataLength */ macKey := pool.Get(len(a.iv) + len(a.Key)) defer pool.Put(macKey) copy(macKey, a.iv) copy(macKey[len(a.iv):], a.Key) // check head tools.AppendRandBytes(poolBuf, 4) a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes()) a.initRC4Cipher() poolBuf.Write(a.lastClientHash[:8]) // uid binary.Write(poolBuf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(a.lastClientHash[8:12])) // encrypted data err := a.putEncryptedData(poolBuf, a.userKey, [2]int{a.Overhead, 0}, a.salt) if err != nil { poolBuf.Reset() return } // last server hash a.lastServerHash = tools.HmacMD5(a.userKey, poolBuf.Bytes()[12:]) poolBuf.Write(a.lastServerHash[:4]) // packed data a.packData(poolBuf, data) } func (a *authChainA) packData(poolBuf *bytes.Buffer, data []byte) { a.encrypter.XORKeyStream(data, data) macKey := pool.Get(len(a.userKey) + 4) defer pool.Put(macKey) copy(macKey, a.userKey) binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID) a.packID++ length := uint16(len(data)) ^ binary.LittleEndian.Uint16(a.lastClientHash[14:16]) originalLength := poolBuf.Len() binary.Write(poolBuf, binary.LittleEndian, length) a.putMixedRandDataAndData(poolBuf, data) a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes()[originalLength:]) poolBuf.Write(a.lastClientHash[:2]) } func (a *authChainA) putMixedRandDataAndData(poolBuf *bytes.Buffer, data []byte) { randDataLength := a.randDataLength(len(data), a.lastClientHash, &a.randomClient) if len(data) == 0 { tools.AppendRandBytes(poolBuf, randDataLength) return } if randDataLength > 0 { startPos := getRandStartPos(randDataLength, &a.randomClient) tools.AppendRandBytes(poolBuf, startPos) poolBuf.Write(data) tools.AppendRandBytes(poolBuf, randDataLength-startPos) return } poolBuf.Write(data) } func getRandStartPos(length int, random *tools.XorShift128Plus) int { if length == 0 { return 0 } return int(int64(random.Next()%8589934609) % int64(length)) } func (a *authChainA) getRandLength(length int, lastHash []byte, random *tools.XorShift128Plus) int { if length > 1440 { return 0 } random.InitFromBinAndLength(lastHash, length) if length > 1300 { return int(random.Next() % 31) } if length > 900 { return int(random.Next() % 127) } if length > 400 { return int(random.Next() % 521) } return int(random.Next() % 1021) } func (a *authChainA) initRC4Cipher() { key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(a.lastClientHash), 16) a.encrypter, _ = rc4.NewCipher(key) a.decrypter, _ = rc4.NewCipher(key) } func udpGetRandLength(lastHash []byte, random *tools.XorShift128Plus) int { random.InitFromBin(lastHash) return int(random.Next() % 127) } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/auth_chain_b.go ================================================ package protocol import ( "net" "sort" "github.com/metacubex/mihomo/transport/ssr/tools" ) func init() { register("auth_chain_b", newAuthChainB, 4) } type authChainB struct { *authChainA dataSizeList []int dataSizeList2 []int } func newAuthChainB(b *Base) Protocol { a := &authChainB{ authChainA: &authChainA{ Base: b, authData: &authData{}, userData: &userData{}, salt: "auth_chain_b", }, } a.initUserData() return a } func (a *authChainB) StreamConn(c net.Conn, iv []byte) net.Conn { p := &authChainB{ authChainA: &authChainA{ Base: a.Base, authData: a.next(), userData: a.userData, salt: a.salt, packID: 1, recvID: 1, }, } p.iv = iv p.randDataLength = p.getRandLength p.initDataSize() return &Conn{Conn: c, Protocol: p} } func (a *authChainB) initDataSize() { a.dataSizeList = a.dataSizeList[:0] a.dataSizeList2 = a.dataSizeList2[:0] a.randomServer.InitFromBin(a.Key) length := a.randomServer.Next()%8 + 4 for ; length > 0; length-- { a.dataSizeList = append(a.dataSizeList, int(a.randomServer.Next()%2340%2040%1440)) } sort.Ints(a.dataSizeList) length = a.randomServer.Next()%16 + 8 for ; length > 0; length-- { a.dataSizeList2 = append(a.dataSizeList2, int(a.randomServer.Next()%2340%2040%1440)) } sort.Ints(a.dataSizeList2) } func (a *authChainB) getRandLength(length int, lashHash []byte, random *tools.XorShift128Plus) int { if length >= 1440 { return 0 } random.InitFromBinAndLength(lashHash, length) pos := sort.Search(len(a.dataSizeList), func(i int) bool { return a.dataSizeList[i] >= length+a.Overhead }) finalPos := pos + int(random.Next()%uint64(len(a.dataSizeList))) if finalPos < len(a.dataSizeList) { return a.dataSizeList[finalPos] - length - a.Overhead } pos = sort.Search(len(a.dataSizeList2), func(i int) bool { return a.dataSizeList2[i] >= length+a.Overhead }) finalPos = pos + int(random.Next()%uint64(len(a.dataSizeList2))) if finalPos < len(a.dataSizeList2) { return a.dataSizeList2[finalPos] - length - a.Overhead } if finalPos < pos+len(a.dataSizeList2)-1 { return 0 } if length > 1300 { return int(random.Next() % 31) } if length > 900 { return int(random.Next() % 127) } if length > 400 { return int(random.Next() % 521) } return int(random.Next() % 1021) } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/auth_sha1_v4.go ================================================ package protocol import ( "bytes" "encoding/binary" "hash/adler32" "hash/crc32" "net" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/transport/ssr/tools" "github.com/metacubex/randv2" ) func init() { register("auth_sha1_v4", newAuthSHA1V4, 7) } type authSHA1V4 struct { *Base *authData iv []byte hasSentHeader bool rawTrans bool } func newAuthSHA1V4(b *Base) Protocol { return &authSHA1V4{Base: b, authData: &authData{}} } func (a *authSHA1V4) StreamConn(c net.Conn, iv []byte) net.Conn { p := &authSHA1V4{Base: a.Base, authData: a.next()} p.iv = iv return &Conn{Conn: c, Protocol: p} } func (a *authSHA1V4) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return c } func (a *authSHA1V4) Decode(dst, src *bytes.Buffer) error { if a.rawTrans { dst.ReadFrom(src) return nil } for src.Len() > 4 { if uint16(crc32.ChecksumIEEE(src.Bytes()[:2])&0xffff) != binary.LittleEndian.Uint16(src.Bytes()[2:4]) { src.Reset() return errAuthSHA1V4CRC32Error } length := int(binary.BigEndian.Uint16(src.Bytes()[:2])) if length >= 8192 || length < 7 { a.rawTrans = true src.Reset() return errAuthSHA1V4LengthError } if length > src.Len() { break } if adler32.Checksum(src.Bytes()[:length-4]) != binary.LittleEndian.Uint32(src.Bytes()[length-4:length]) { a.rawTrans = true src.Reset() return errAuthSHA1V4Adler32Error } pos := int(src.Bytes()[4]) if pos < 255 { pos += 4 } else { pos = int(binary.BigEndian.Uint16(src.Bytes()[5:7])) + 4 } dst.Write(src.Bytes()[pos : length-4]) src.Next(length) } return nil } func (a *authSHA1V4) Encode(buf *bytes.Buffer, b []byte) error { if !a.hasSentHeader { dataLength := getDataLength(b) a.packAuthData(buf, b[:dataLength]) b = b[dataLength:] a.hasSentHeader = true } for len(b) > 8100 { a.packData(buf, b[:8100]) b = b[8100:] } if len(b) > 0 { a.packData(buf, b) } return nil } func (a *authSHA1V4) DecodePacket(b []byte) ([]byte, error) { return b, nil } func (a *authSHA1V4) EncodePacket(buf *bytes.Buffer, b []byte) error { buf.Write(b) return nil } func (a *authSHA1V4) packData(poolBuf *bytes.Buffer, data []byte) { dataLength := len(data) randDataLength := a.getRandDataLength(dataLength) /* 2: uint16 BigEndian packedDataLength 2: uint16 LittleEndian crc32Data & 0xffff 3: maxRandDataLengthPrefix (min:1) 4: adler32Data */ packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4 if randDataLength < 128 { packedDataLength -= 2 } binary.Write(poolBuf, binary.BigEndian, uint16(packedDataLength)) binary.Write(poolBuf, binary.LittleEndian, uint16(crc32.ChecksumIEEE(poolBuf.Bytes()[poolBuf.Len()-2:])&0xffff)) a.packRandData(poolBuf, randDataLength) poolBuf.Write(data) binary.Write(poolBuf, binary.LittleEndian, adler32.Checksum(poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:])) } func (a *authSHA1V4) packAuthData(poolBuf *bytes.Buffer, data []byte) { dataLength := len(data) randDataLength := a.getRandDataLength(12 + dataLength) /* 2: uint16 BigEndian packedAuthDataLength 4: uint32 LittleEndian crc32Data 3: maxRandDataLengthPrefix (min: 1) 12: authDataLength 10: hmacSHA1DataLength */ packedAuthDataLength := 2 + 4 + 3 + randDataLength + 12 + dataLength + 10 if randDataLength < 128 { packedAuthDataLength -= 2 } salt := []byte("auth_sha1_v4") crcData := pool.Get(len(salt) + len(a.Key) + 2) defer pool.Put(crcData) binary.BigEndian.PutUint16(crcData, uint16(packedAuthDataLength)) copy(crcData[2:], salt) copy(crcData[2+len(salt):], a.Key) key := pool.Get(len(a.iv) + len(a.Key)) defer pool.Put(key) copy(key, a.iv) copy(key[len(a.iv):], a.Key) poolBuf.Write(crcData[:2]) binary.Write(poolBuf, binary.LittleEndian, crc32.ChecksumIEEE(crcData)) a.packRandData(poolBuf, randDataLength) a.putAuthData(poolBuf) poolBuf.Write(data) poolBuf.Write(tools.HmacSHA1(key, poolBuf.Bytes()[poolBuf.Len()-packedAuthDataLength+10:])[:10]) } func (a *authSHA1V4) packRandData(poolBuf *bytes.Buffer, size int) { if size < 128 { poolBuf.WriteByte(byte(size + 1)) tools.AppendRandBytes(poolBuf, size) return } poolBuf.WriteByte(255) binary.Write(poolBuf, binary.BigEndian, uint16(size+3)) tools.AppendRandBytes(poolBuf, size) } func (a *authSHA1V4) getRandDataLength(size int) int { if size > 1200 { return 0 } if size > 400 { return randv2.IntN(256) } return randv2.IntN(512) } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/base.go ================================================ package protocol import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/binary" "sync" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/randv2" ) type Base struct { Key []byte Overhead int Param string } type userData struct { userKey []byte userID [4]byte } type authData struct { clientID [4]byte connectionID uint32 mutex sync.Mutex } func (a *authData) next() *authData { r := &authData{} a.mutex.Lock() defer a.mutex.Unlock() if a.connectionID > 0xff000000 || a.connectionID == 0 { rand.Read(a.clientID[:]) a.connectionID = randv2.Uint32() & 0xffffff } a.connectionID++ copy(r.clientID[:], a.clientID[:]) r.connectionID = a.connectionID return r } func (a *authData) putAuthData(buf *bytes.Buffer) { binary.Write(buf, binary.LittleEndian, uint32(ntp.Now().Unix())) buf.Write(a.clientID[:]) binary.Write(buf, binary.LittleEndian, a.connectionID) } func (a *authData) putEncryptedData(b *bytes.Buffer, userKey []byte, paddings [2]int, salt string) error { encrypt := pool.Get(16) defer pool.Put(encrypt) binary.LittleEndian.PutUint32(encrypt, uint32(ntp.Now().Unix())) copy(encrypt[4:], a.clientID[:]) binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID) binary.LittleEndian.PutUint16(encrypt[12:], uint16(paddings[0])) binary.LittleEndian.PutUint16(encrypt[14:], uint16(paddings[1])) cipherKey := core.Kdf(base64.StdEncoding.EncodeToString(userKey)+salt, 16) block, err := aes.NewCipher(cipherKey) if err != nil { log.Warnln("New cipher error: %s", err.Error()) return err } iv := bytes.Repeat([]byte{0}, 16) cbcCipher := cipher.NewCBCEncrypter(block, iv) cbcCipher.CryptBlocks(encrypt, encrypt) b.Write(encrypt) return nil } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/origin.go ================================================ package protocol import ( "bytes" "net" N "github.com/metacubex/mihomo/common/net" ) type origin struct{} func init() { register("origin", newOrigin, 0) } func newOrigin(b *Base) Protocol { return &origin{} } func (o *origin) StreamConn(c net.Conn, iv []byte) net.Conn { return c } func (o *origin) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return c } func (o *origin) Decode(dst, src *bytes.Buffer) error { dst.ReadFrom(src) return nil } func (o *origin) Encode(buf *bytes.Buffer, b []byte) error { buf.Write(b) return nil } func (o *origin) DecodePacket(b []byte) ([]byte, error) { return b, nil } func (o *origin) EncodePacket(buf *bytes.Buffer, b []byte) error { buf.Write(b) return nil } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/packet.go ================================================ package protocol import ( "net" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" ) type PacketConn struct { N.EnhancePacketConn Protocol } func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { buf := pool.GetBuffer() defer pool.PutBuffer(buf) err := c.EncodePacket(buf, b) if err != nil { return 0, err } _, err = c.EnhancePacketConn.WriteTo(buf.Bytes(), addr) return len(b), err } func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { n, addr, err := c.EnhancePacketConn.ReadFrom(b) if err != nil { return n, addr, err } decoded, err := c.DecodePacket(b[:n]) if err != nil { return n, addr, err } copy(b, decoded) return len(decoded), addr, nil } func (c *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { data, put, addr, err = c.EnhancePacketConn.WaitReadFrom() if err != nil { return } data, err = c.DecodePacket(data) if err != nil { if put != nil { put() } data = nil put = nil return } return } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/protocol.go ================================================ package protocol import ( "bytes" "errors" "fmt" "net" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/randv2" ) var ( errAuthSHA1V4CRC32Error = errors.New("auth_sha1_v4 decode data wrong crc32") errAuthSHA1V4LengthError = errors.New("auth_sha1_v4 decode data wrong length") errAuthSHA1V4Adler32Error = errors.New("auth_sha1_v4 decode data wrong adler32") errAuthAES128MACError = errors.New("auth_aes128 decode data wrong mac") errAuthAES128LengthError = errors.New("auth_aes128 decode data wrong length") errAuthAES128ChksumError = errors.New("auth_aes128 decode data wrong checksum") errAuthChainLengthError = errors.New("auth_chain decode data wrong length") errAuthChainChksumError = errors.New("auth_chain decode data wrong checksum") ) type Protocol interface { StreamConn(net.Conn, []byte) net.Conn PacketConn(N.EnhancePacketConn) N.EnhancePacketConn Decode(dst, src *bytes.Buffer) error Encode(buf *bytes.Buffer, b []byte) error DecodePacket([]byte) ([]byte, error) EncodePacket(buf *bytes.Buffer, b []byte) error } type protocolCreator func(b *Base) Protocol var protocolList = make(map[string]struct { overhead int new protocolCreator }) func register(name string, c protocolCreator, o int) { protocolList[name] = struct { overhead int new protocolCreator }{overhead: o, new: c} } func PickProtocol(name string, b *Base) (Protocol, error) { if choice, ok := protocolList[name]; ok { b.Overhead += choice.overhead return choice.new(b), nil } return nil, fmt.Errorf("protocol %s not supported", name) } func getHeadSize(b []byte, defaultValue int) int { if len(b) < 2 { return defaultValue } headType := b[0] & 7 switch headType { case 1: return 7 case 4: return 19 case 3: return 4 + int(b[1]) } return defaultValue } func getDataLength(b []byte) int { bLength := len(b) dataLength := getHeadSize(b, 30) + randv2.IntN(32) if bLength < dataLength { return bLength } return dataLength } ================================================ FILE: core/Clash.Meta/transport/ssr/protocol/stream.go ================================================ package protocol import ( "bytes" "net" "github.com/metacubex/mihomo/common/pool" ) type Conn struct { net.Conn Protocol decoded bytes.Buffer underDecoded bytes.Buffer } func (c *Conn) Read(b []byte) (int, error) { if c.decoded.Len() > 0 { return c.decoded.Read(b) } buf := pool.Get(pool.RelayBufferSize) defer pool.Put(buf) n, err := c.Conn.Read(buf) if err != nil { return 0, err } c.underDecoded.Write(buf[:n]) err = c.Decode(&c.decoded, &c.underDecoded) if err != nil { return 0, err } n, _ = c.decoded.Read(b) return n, nil } func (c *Conn) Write(b []byte) (int, error) { bLength := len(b) buf := pool.GetBuffer() defer pool.PutBuffer(buf) err := c.Encode(buf, b) if err != nil { return 0, err } _, err = c.Conn.Write(buf.Bytes()) if err != nil { return 0, err } return bLength, nil } ================================================ FILE: core/Clash.Meta/transport/ssr/tools/bufPool.go ================================================ package tools import ( "bytes" "crypto/rand" "io" ) func AppendRandBytes(b *bytes.Buffer, length int) { b.ReadFrom(io.LimitReader(rand.Reader, int64(length))) } ================================================ FILE: core/Clash.Meta/transport/ssr/tools/crypto.go ================================================ package tools import ( "crypto/hmac" "crypto/md5" "crypto/sha1" ) const HmacSHA1Len = 10 func HmacMD5(key, data []byte) []byte { hmacMD5 := hmac.New(md5.New, key) hmacMD5.Write(data) return hmacMD5.Sum(nil) } func HmacSHA1(key, data []byte) []byte { hmacSHA1 := hmac.New(sha1.New, key) hmacSHA1.Write(data) return hmacSHA1.Sum(nil) } func MD5Sum(b []byte) []byte { h := md5.New() h.Write(b) return h.Sum(nil) } func SHA1Sum(b []byte) []byte { h := sha1.New() h.Write(b) return h.Sum(nil) } ================================================ FILE: core/Clash.Meta/transport/ssr/tools/random.go ================================================ package tools import ( "encoding/binary" "github.com/metacubex/mihomo/common/pool" ) // XorShift128Plus - a pseudorandom number generator type XorShift128Plus struct { s [2]uint64 } func (r *XorShift128Plus) Next() uint64 { x := r.s[0] y := r.s[1] r.s[0] = y x ^= x << 23 x ^= y ^ (x >> 17) ^ (y >> 26) r.s[1] = x return x + y } func (r *XorShift128Plus) InitFromBin(bin []byte) { var full []byte if len(bin) < 16 { full := pool.Get(16)[:0] defer pool.Put(full) full = append(full, bin...) for len(full) < 16 { full = append(full, 0) } } else { full = bin } r.s[0] = binary.LittleEndian.Uint64(full[:8]) r.s[1] = binary.LittleEndian.Uint64(full[8:16]) } func (r *XorShift128Plus) InitFromBinAndLength(bin []byte, length int) { var full []byte if len(bin) < 16 { full := pool.Get(16)[:0] defer pool.Put(full) full = append(full, bin...) for len(full) < 16 { full = append(full, 0) } } full = bin binary.LittleEndian.PutUint16(full, uint16(length)) r.s[0] = binary.LittleEndian.Uint64(full[:8]) r.s[1] = binary.LittleEndian.Uint64(full[8:16]) for i := 0; i < 4; i++ { r.Next() } } ================================================ FILE: core/Clash.Meta/transport/sudoku/address.go ================================================ package sudoku import ( "encoding/binary" "fmt" "io" "net" "strconv" "strings" ) func EncodeAddress(rawAddr string) ([]byte, error) { host, portStr, err := net.SplitHostPort(rawAddr) if err != nil { return nil, err } portInt, err := strconv.ParseUint(portStr, 10, 16) if err != nil { return nil, err } var buf []byte if i := strings.IndexByte(host, '%'); i >= 0 { // Zone identifiers are not representable in SOCKS5 IPv6 address encoding. host = host[:i] } if ip := net.ParseIP(host); ip != nil { if ip4 := ip.To4(); ip4 != nil { buf = append(buf, 0x01) // IPv4 buf = append(buf, ip4...) } else { buf = append(buf, 0x04) // IPv6 ip16 := ip.To16() if ip16 == nil { return nil, fmt.Errorf("invalid ipv6: %q", host) } buf = append(buf, ip16...) } } else { if len(host) > 255 { return nil, fmt.Errorf("domain too long") } buf = append(buf, 0x03) // domain buf = append(buf, byte(len(host))) buf = append(buf, host...) } var portBytes [2]byte binary.BigEndian.PutUint16(portBytes[:], uint16(portInt)) buf = append(buf, portBytes[:]...) return buf, nil } func DecodeAddress(r io.Reader) (string, error) { var atyp [1]byte if _, err := io.ReadFull(r, atyp[:]); err != nil { return "", err } switch atyp[0] { case 0x01: // IPv4 var ipBuf [net.IPv4len]byte if _, err := io.ReadFull(r, ipBuf[:]); err != nil { return "", err } var portBuf [2]byte if _, err := io.ReadFull(r, portBuf[:]); err != nil { return "", err } return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil case 0x04: // IPv6 var ipBuf [net.IPv6len]byte if _, err := io.ReadFull(r, ipBuf[:]); err != nil { return "", err } var portBuf [2]byte if _, err := io.ReadFull(r, portBuf[:]); err != nil { return "", err } return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil case 0x03: // domain var lengthBuf [1]byte if _, err := io.ReadFull(r, lengthBuf[:]); err != nil { return "", err } l := int(lengthBuf[0]) hostBuf := make([]byte, l) if _, err := io.ReadFull(r, hostBuf); err != nil { return "", err } var portBuf [2]byte if _, err := io.ReadFull(r, portBuf[:]); err != nil { return "", err } return net.JoinHostPort(string(hostBuf), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil default: return "", fmt.Errorf("unknown address type: %d", atyp[0]) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/config.go ================================================ package sudoku import ( "fmt" "strings" "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) // ProtocolConfig defines the configuration for the Sudoku protocol stack. // It is intentionally kept close to the upstream Sudoku project to ensure wire compatibility. type ProtocolConfig struct { // Client-only: "host:port". ServerAddress string // Pre-shared key (or ED25519 key material) used to derive crypto and tables. Key string // "aes-128-gcm", "chacha20-poly1305", or "none". AEADMethod string // Table is the single obfuscation table to use when table rotation is disabled. Table *sudoku.Table // Tables is an optional candidate set for table rotation. // If provided (len>0), the client will pick one table per connection and the server will // probe the handshake to detect which one was used, keeping the handshake format unchanged. // When Tables is set, Table may be nil. Tables []*sudoku.Table // Padding insertion ratio (0-100). Must satisfy PaddingMax >= PaddingMin. PaddingMin int PaddingMax int // EnablePureDownlink enables the pure Sudoku downlink mode. // When false, the connection uses the bandwidth-optimized packed downlink. EnablePureDownlink bool // Client-only: final target "host:port". TargetAddress string // Server-side handshake timeout (seconds). HandshakeTimeoutSeconds int // DisableHTTPMask disables all HTTP camouflage layers. DisableHTTPMask bool // HTTPMaskMode controls how the HTTP layer behaves: // - "legacy": write a fake HTTP/1.1 header then switch to raw stream (default, not CDN-compatible) // - "stream": real HTTP tunnel (split-stream), CDN-compatible // - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through // - "auto": try stream then fall back to poll // - "ws": WebSocket tunnel (GET upgrade), CDN-friendly HTTPMaskMode string // HTTPMaskTLSEnabled enables HTTPS for HTTP tunnel modes (client-side). // If false, the tunnel uses HTTP (no port-based inference). HTTPMaskTLSEnabled bool // HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side). HTTPMaskHost string // HTTPMaskPathRoot optionally prefixes all HTTP mask paths with a first-level segment. // Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ... HTTPMaskPathRoot string // HTTPMaskMultiplex controls multiplex behavior when HTTPMask tunnel modes are enabled: // - "off": disable reuse; each Dial establishes its own HTTPMask tunnel // - "auto": reuse underlying HTTP connections across multiple tunnel dials (HTTP/1.1 keep-alive / HTTP/2) // - "on": enable "single tunnel, multi-target" mux (Sudoku-level multiplex; Dial behaves like "auto" otherwise) HTTPMaskMultiplex string } func (c *ProtocolConfig) Validate() error { if c.Table == nil && len(c.Tables) == 0 { return fmt.Errorf("table cannot be nil (or provide tables)") } for i, t := range c.Tables { if t == nil { return fmt.Errorf("tables[%d] cannot be nil", i) } } if c.Key == "" { return fmt.Errorf("key cannot be empty") } switch c.AEADMethod { case "aes-128-gcm", "chacha20-poly1305", "none": default: return fmt.Errorf("invalid aead-method: %s, must be one of: aes-128-gcm, chacha20-poly1305, none", c.AEADMethod) } if c.PaddingMin < 0 || c.PaddingMin > 100 { return fmt.Errorf("padding-min must be between 0 and 100, got %d", c.PaddingMin) } if c.PaddingMax < 0 || c.PaddingMax > 100 { return fmt.Errorf("padding-max must be between 0 and 100, got %d", c.PaddingMax) } if c.PaddingMax < c.PaddingMin { return fmt.Errorf("padding-max (%d) must be >= padding-min (%d)", c.PaddingMax, c.PaddingMin) } if c.HandshakeTimeoutSeconds < 0 { return fmt.Errorf("handshake-timeout must be >= 0, got %d", c.HandshakeTimeoutSeconds) } switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMode)) { case "", "legacy", "stream", "poll", "auto", "ws": default: return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto, ws", c.HTTPMaskMode) } if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" { v = strings.Trim(v, "/") if v == "" || strings.Contains(v, "/") { return fmt.Errorf("invalid http-mask-path-root: must be a single path segment") } for i := 0; i < len(v); i++ { ch := v[i] switch { case ch >= 'a' && ch <= 'z': case ch >= 'A' && ch <= 'Z': case ch >= '0' && ch <= '9': case ch == '_' || ch == '-': default: return fmt.Errorf("invalid http-mask-path-root: contains invalid character %q", ch) } } } switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMultiplex)) { case "", "off", "auto", "on": default: return fmt.Errorf("invalid http-mask-multiplex: %s, must be one of: off, auto, on", c.HTTPMaskMultiplex) } return nil } func (c *ProtocolConfig) ValidateClient() error { if err := c.Validate(); err != nil { return err } if c.ServerAddress == "" { return fmt.Errorf("server address cannot be empty") } if c.TargetAddress == "" { return fmt.Errorf("target address cannot be empty") } return nil } func DefaultConfig() *ProtocolConfig { return &ProtocolConfig{ AEADMethod: "chacha20-poly1305", PaddingMin: 10, PaddingMax: 30, EnablePureDownlink: true, HandshakeTimeoutSeconds: 5, HTTPMaskMode: "legacy", HTTPMaskMultiplex: "off", } } func DerefInt(v *int, def int) int { if v == nil { return def } return *v } func DerefBool(v *bool, def bool) bool { if v == nil { return def } return *v } // ResolvePadding applies defaults and keeps min/max consistent when only one side is provided. func ResolvePadding(min, max *int, defMin, defMax int) (int, int) { paddingMin := DerefInt(min, defMin) paddingMax := DerefInt(max, defMax) switch { case min == nil && max != nil && paddingMax < paddingMin: paddingMin = paddingMax case max == nil && min != nil && paddingMax < paddingMin: paddingMax = paddingMin } return paddingMin, paddingMax } func NormalizeTableType(tableType string) (string, error) { normalized, err := sudoku.NormalizeASCIIMode(tableType) if err != nil { return "", fmt.Errorf("table-type must be prefer_ascii, prefer_entropy, up_ascii_down_entropy, or up_entropy_down_ascii") } return normalized, nil } func (c *ProtocolConfig) tableCandidates() []*sudoku.Table { if c == nil { return nil } if len(c.Tables) > 0 { return c.Tables } if c.Table != nil { return []*sudoku.Table{c.Table} } return nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/crypto/aead.go ================================================ package crypto import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "net" "golang.org/x/crypto/chacha20poly1305" ) type AEADConn struct { net.Conn aead cipher.AEAD readBuf bytes.Buffer nonceSize int } func (cc *AEADConn) CloseWrite() error { if cc == nil || cc.Conn == nil { return nil } if cw, ok := cc.Conn.(interface{ CloseWrite() error }); ok { return cw.CloseWrite() } return nil } func (cc *AEADConn) CloseRead() error { if cc == nil || cc.Conn == nil { return nil } if cr, ok := cc.Conn.(interface{ CloseRead() error }); ok { return cr.CloseRead() } return nil } func NewAEADConn(c net.Conn, key string, method string) (*AEADConn, error) { if method == "none" { return &AEADConn{Conn: c, aead: nil}, nil } h := sha256.New() h.Write([]byte(key)) keyBytes := h.Sum(nil) var aead cipher.AEAD var err error switch method { case "aes-128-gcm": block, _ := aes.NewCipher(keyBytes[:16]) aead, err = cipher.NewGCM(block) case "chacha20-poly1305": aead, err = chacha20poly1305.New(keyBytes) default: return nil, fmt.Errorf("unsupported cipher: %s", method) } if err != nil { return nil, err } return &AEADConn{ Conn: c, aead: aead, nonceSize: aead.NonceSize(), }, nil } func (cc *AEADConn) Write(p []byte) (int, error) { if cc.aead == nil { return cc.Conn.Write(p) } maxPayload := 65535 - cc.nonceSize - cc.aead.Overhead() totalWritten := 0 var frameBuf bytes.Buffer header := make([]byte, 2) nonce := make([]byte, cc.nonceSize) for len(p) > 0 { chunkSize := len(p) if chunkSize > maxPayload { chunkSize = maxPayload } chunk := p[:chunkSize] p = p[chunkSize:] if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return totalWritten, err } ciphertext := cc.aead.Seal(nil, nonce, chunk, nil) frameLen := len(nonce) + len(ciphertext) binary.BigEndian.PutUint16(header, uint16(frameLen)) frameBuf.Reset() frameBuf.Write(header) frameBuf.Write(nonce) frameBuf.Write(ciphertext) if _, err := cc.Conn.Write(frameBuf.Bytes()); err != nil { return totalWritten, err } totalWritten += chunkSize } return totalWritten, nil } func (cc *AEADConn) Read(p []byte) (int, error) { if cc.aead == nil { return cc.Conn.Read(p) } if cc.readBuf.Len() > 0 { return cc.readBuf.Read(p) } header := make([]byte, 2) if _, err := io.ReadFull(cc.Conn, header); err != nil { return 0, err } frameLen := int(binary.BigEndian.Uint16(header)) body := make([]byte, frameLen) if _, err := io.ReadFull(cc.Conn, body); err != nil { return 0, err } if len(body) < cc.nonceSize { return 0, errors.New("frame too short") } nonce := body[:cc.nonceSize] ciphertext := body[cc.nonceSize:] plaintext, err := cc.aead.Open(nil, nonce, ciphertext, nil) if err != nil { return 0, errors.New("decryption failed") } cc.readBuf.Write(plaintext) return cc.readBuf.Read(p) } ================================================ FILE: core/Clash.Meta/transport/sudoku/crypto/ed25519.go ================================================ package crypto import ( "crypto/rand" "encoding/hex" "errors" "fmt" "github.com/metacubex/edwards25519" ) // KeyPair holds the scalar private key and point public key type KeyPair struct { Private *edwards25519.Scalar Public *edwards25519.Point } // GenerateMasterKey generates a random master private key (scalar) and its public key (point) func GenerateMasterKey() (*KeyPair, error) { // 1. Generate random scalar x (32 bytes) var seed [64]byte if _, err := rand.Read(seed[:]); err != nil { return nil, err } x, err := edwards25519.NewScalar().SetUniformBytes(seed[:]) if err != nil { return nil, err } // 2. Calculate Public Key P = x * G P := new(edwards25519.Point).ScalarBaseMult(x) return &KeyPair{Private: x, Public: P}, nil } // SplitPrivateKey takes a master private key x and returns a new random split key (r, k) // such that x = r + k (mod L). // Returns hex encoded string of r || k (64 bytes) func SplitPrivateKey(x *edwards25519.Scalar) (string, error) { // 1. Generate random r (32 bytes) var seed [64]byte if _, err := rand.Read(seed[:]); err != nil { return "", err } r, err := edwards25519.NewScalar().SetUniformBytes(seed[:]) if err != nil { return "", err } // 2. Calculate k = x - r (mod L) k := new(edwards25519.Scalar).Subtract(x, r) // 3. Encode r and k rBytes := r.Bytes() kBytes := k.Bytes() full := make([]byte, 64) copy(full[:32], rBytes) copy(full[32:], kBytes) return hex.EncodeToString(full), nil } // RecoverPublicKey takes a split private key (r, k) or a master private key (x) // and returns the public key P. // Input can be: // - 32 bytes hex (Master Scalar x) // - 64 bytes hex (Split Key r || k) func RecoverPublicKey(keyHex string) (*edwards25519.Point, error) { keyBytes, err := hex.DecodeString(keyHex) if err != nil { return nil, fmt.Errorf("invalid hex: %w", err) } if len(keyBytes) == 32 { // Master Key x x, err := edwards25519.NewScalar().SetCanonicalBytes(keyBytes) if err != nil { return nil, fmt.Errorf("invalid scalar: %w", err) } return new(edwards25519.Point).ScalarBaseMult(x), nil } if len(keyBytes) == 64 { // Split Key r || k rBytes := keyBytes[:32] kBytes := keyBytes[32:] r, err := edwards25519.NewScalar().SetCanonicalBytes(rBytes) if err != nil { return nil, fmt.Errorf("invalid scalar r: %w", err) } k, err := edwards25519.NewScalar().SetCanonicalBytes(kBytes) if err != nil { return nil, fmt.Errorf("invalid scalar k: %w", err) } // sum = r + k sum := new(edwards25519.Scalar).Add(r, k) // P = sum * G return new(edwards25519.Point).ScalarBaseMult(sum), nil } return nil, errors.New("invalid key length: must be 32 bytes (Master) or 64 bytes (Split)") } // EncodePoint returns the hex string of the compressed point func EncodePoint(p *edwards25519.Point) string { return hex.EncodeToString(p.Bytes()) } // EncodeScalar returns the hex string of the scalar func EncodeScalar(s *edwards25519.Scalar) string { return hex.EncodeToString(s.Bytes()) } ================================================ FILE: core/Clash.Meta/transport/sudoku/crypto/record_conn.go ================================================ package crypto import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "net" "sync" "sync/atomic" "golang.org/x/crypto/chacha20poly1305" ) // KeyUpdateAfterBytes controls automatic key rotation based on plaintext bytes. // It is a package var (not config) to enable targeted tests with smaller thresholds. var KeyUpdateAfterBytes int64 = 32 << 20 // 32 MiB const ( recordHeaderSize = 12 // epoch(uint32) + seq(uint64) - also used as nonce+AAD. maxFrameBodySize = 65535 ) type recordKeys struct { baseSend []byte baseRecv []byte } // RecordConn is a framed AEAD net.Conn with: // - deterministic per-record nonce (epoch+seq) // - per-direction key rotation (epoch), driven by plaintext byte counters // - replay/out-of-order protection within the connection (strict seq check) // // Wire format per record: // - uint16 bodyLen // - header[12] = epoch(uint32 BE) || seq(uint64 BE) (plaintext) // - ciphertext = AEAD(header as nonce, plaintext, header as AAD) type RecordConn struct { net.Conn method string writeMu sync.Mutex readMu sync.Mutex keys recordKeys sendAEAD cipher.AEAD sendAEADEpoch uint32 recvAEAD cipher.AEAD recvAEADEpoch uint32 // Send direction state. sendEpoch uint32 sendSeq uint64 sendBytes int64 sendEpochUpdates uint32 // Receive direction state. recvEpoch uint32 recvSeq uint64 recvInitialized bool readBuf bytes.Buffer // writeFrame is a reusable buffer for [len||header||ciphertext] on the wire. // Guarded by writeMu. writeFrame []byte } func (c *RecordConn) CloseWrite() error { if c == nil { return nil } if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok { return cw.CloseWrite() } return nil } func (c *RecordConn) CloseRead() error { if c == nil { return nil } if cr, ok := c.Conn.(interface{ CloseRead() error }); ok { return cr.CloseRead() } return nil } func NewRecordConn(conn net.Conn, method string, baseSend, baseRecv []byte) (*RecordConn, error) { if conn == nil { return nil, fmt.Errorf("nil conn") } method = normalizeAEADMethod(method) if method != "none" { if err := validateBaseKey(baseSend); err != nil { return nil, fmt.Errorf("invalid send base key: %w", err) } if err := validateBaseKey(baseRecv); err != nil { return nil, fmt.Errorf("invalid recv base key: %w", err) } } rc := &RecordConn{Conn: conn, method: method} rc.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)} if err := rc.resetTrafficState(); err != nil { return nil, err } return rc, nil } func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error { if c == nil { return fmt.Errorf("nil conn") } if c.method != "none" { if err := validateBaseKey(baseSend); err != nil { return fmt.Errorf("invalid send base key: %w", err) } if err := validateBaseKey(baseRecv); err != nil { return fmt.Errorf("invalid recv base key: %w", err) } } c.writeMu.Lock() c.readMu.Lock() defer c.readMu.Unlock() defer c.writeMu.Unlock() c.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)} if err := c.resetTrafficState(); err != nil { return err } c.readBuf.Reset() c.sendAEAD = nil c.recvAEAD = nil c.sendAEADEpoch = 0 c.recvAEADEpoch = 0 return nil } func (c *RecordConn) resetTrafficState() error { sendEpoch, sendSeq, err := randomRecordCounters() if err != nil { return fmt.Errorf("initialize record counters: %w", err) } c.sendEpoch = sendEpoch c.sendSeq = sendSeq c.sendBytes = 0 c.sendEpochUpdates = 0 c.recvEpoch = 0 c.recvSeq = 0 c.recvInitialized = false return nil } func normalizeAEADMethod(method string) string { switch method { case "", "chacha20-poly1305": return "chacha20-poly1305" case "aes-128-gcm", "none": return method default: return method } } func validateBaseKey(b []byte) error { if len(b) < 32 { return fmt.Errorf("need at least 32 bytes, got %d", len(b)) } return nil } func cloneBytes(b []byte) []byte { if len(b) == 0 { return nil } return append([]byte(nil), b...) } func randomRecordCounters() (uint32, uint64, error) { epoch, err := randomNonZeroUint32() if err != nil { return 0, 0, err } seq, err := randomNonZeroUint64() if err != nil { return 0, 0, err } return epoch, seq, nil } func randomNonZeroUint32() (uint32, error) { var b [4]byte for { if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { return 0, err } v := binary.BigEndian.Uint32(b[:]) if v != 0 && v != ^uint32(0) { return v, nil } } } func randomNonZeroUint64() (uint64, error) { var b [8]byte for { if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { return 0, err } v := binary.BigEndian.Uint64(b[:]) if v != 0 && v != ^uint64(0) { return v, nil } } } func (c *RecordConn) newAEADFor(base []byte, epoch uint32) (cipher.AEAD, error) { if c.method == "none" { return nil, nil } key := deriveEpochKey(base, epoch, c.method) switch c.method { case "aes-128-gcm": block, err := aes.NewCipher(key[:16]) if err != nil { return nil, err } a, err := cipher.NewGCM(block) if err != nil { return nil, err } if a.NonceSize() != recordHeaderSize { return nil, fmt.Errorf("unexpected gcm nonce size: %d", a.NonceSize()) } return a, nil case "chacha20-poly1305": a, err := chacha20poly1305.New(key[:32]) if err != nil { return nil, err } if a.NonceSize() != recordHeaderSize { return nil, fmt.Errorf("unexpected chacha nonce size: %d", a.NonceSize()) } return a, nil default: return nil, fmt.Errorf("unsupported cipher: %s", c.method) } } func deriveEpochKey(base []byte, epoch uint32, method string) []byte { var b [4]byte binary.BigEndian.PutUint32(b[:], epoch) mac := hmac.New(sha256.New, base) _, _ = mac.Write([]byte("sudoku-record:")) _, _ = mac.Write([]byte(method)) _, _ = mac.Write(b[:]) return mac.Sum(nil) } func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) error { ku := atomic.LoadInt64(&KeyUpdateAfterBytes) if ku <= 0 || c.method == "none" { return nil } c.sendBytes += int64(addedPlain) threshold := ku * int64(c.sendEpochUpdates+1) if c.sendBytes < threshold { return nil } c.sendEpoch++ c.sendEpochUpdates++ nextSeq, err := randomNonZeroUint64() if err != nil { return fmt.Errorf("rotate record seq: %w", err) } c.sendSeq = nextSeq return nil } func (c *RecordConn) validateRecvPosition(epoch uint32, seq uint64) error { if !c.recvInitialized { return nil } if epoch < c.recvEpoch { return fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch) } if epoch == c.recvEpoch && seq != c.recvSeq { return fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq) } if epoch > c.recvEpoch { const maxJump = 8 if epoch-c.recvEpoch > maxJump { return fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump) } } return nil } func (c *RecordConn) markRecvPosition(epoch uint32, seq uint64) { c.recvEpoch = epoch c.recvSeq = seq + 1 c.recvInitialized = true } func (c *RecordConn) Write(p []byte) (int, error) { if c == nil || c.Conn == nil { return 0, net.ErrClosed } if c.method == "none" { return c.Conn.Write(p) } c.writeMu.Lock() defer c.writeMu.Unlock() total := 0 for len(p) > 0 { if c.sendAEAD == nil || c.sendAEADEpoch != c.sendEpoch { a, err := c.newAEADFor(c.keys.baseSend, c.sendEpoch) if err != nil { return total, err } c.sendAEAD = a c.sendAEADEpoch = c.sendEpoch } aead := c.sendAEAD maxPlain := maxFrameBodySize - recordHeaderSize - aead.Overhead() if maxPlain <= 0 { return total, errors.New("frame size too small") } n := len(p) if n > maxPlain { n = maxPlain } chunk := p[:n] p = p[n:] var header [recordHeaderSize]byte binary.BigEndian.PutUint32(header[:4], c.sendEpoch) binary.BigEndian.PutUint64(header[4:], c.sendSeq) c.sendSeq++ cipherLen := n + aead.Overhead() bodyLen := recordHeaderSize + cipherLen frameLen := 2 + bodyLen if bodyLen > maxFrameBodySize { return total, errors.New("frame too large") } if cap(c.writeFrame) < frameLen { c.writeFrame = make([]byte, frameLen) } frame := c.writeFrame[:frameLen] binary.BigEndian.PutUint16(frame[:2], uint16(bodyLen)) copy(frame[2:2+recordHeaderSize], header[:]) dst := frame[2+recordHeaderSize : 2+recordHeaderSize : frameLen] _ = aead.Seal(dst[:0], header[:], chunk, header[:]) if err := writeFull(c.Conn, frame); err != nil { return total, err } total += n if err := c.maybeBumpSendEpochLocked(n); err != nil { return total, err } } return total, nil } func (c *RecordConn) Read(p []byte) (int, error) { if c == nil || c.Conn == nil { return 0, net.ErrClosed } if c.method == "none" { return c.Conn.Read(p) } c.readMu.Lock() defer c.readMu.Unlock() if c.readBuf.Len() > 0 { return c.readBuf.Read(p) } var lenBuf [2]byte if _, err := io.ReadFull(c.Conn, lenBuf[:]); err != nil { return 0, err } bodyLen := int(binary.BigEndian.Uint16(lenBuf[:])) if bodyLen < recordHeaderSize { return 0, errors.New("frame too short") } if bodyLen > maxFrameBodySize { return 0, errors.New("frame too large") } body := make([]byte, bodyLen) if _, err := io.ReadFull(c.Conn, body); err != nil { return 0, err } header := body[:recordHeaderSize] ciphertext := body[recordHeaderSize:] epoch := binary.BigEndian.Uint32(header[:4]) seq := binary.BigEndian.Uint64(header[4:]) if err := c.validateRecvPosition(epoch, seq); err != nil { return 0, err } if c.recvAEAD == nil || c.recvAEADEpoch != epoch { a, err := c.newAEADFor(c.keys.baseRecv, epoch) if err != nil { return 0, err } c.recvAEAD = a c.recvAEADEpoch = epoch } aead := c.recvAEAD plaintext, err := aead.Open(nil, header, ciphertext, header) if err != nil { return 0, fmt.Errorf("decryption failed: epoch=%d seq=%d: %w", epoch, seq, err) } c.markRecvPosition(epoch, seq) c.readBuf.Write(plaintext) return c.readBuf.Read(p) } func writeFull(w io.Writer, b []byte) error { for len(b) > 0 { n, err := w.Write(b) if err != nil { return err } b = b[n:] } return nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/crypto/record_conn_test.go ================================================ package crypto import ( "bytes" "crypto/sha256" "encoding/binary" "io" "net" "testing" "time" ) type captureConn struct { bytes.Buffer } func (c *captureConn) Read(_ []byte) (int, error) { return 0, io.EOF } func (c *captureConn) Write(p []byte) (int, error) { return c.Buffer.Write(p) } func (c *captureConn) Close() error { return nil } func (c *captureConn) LocalAddr() net.Addr { return nil } func (c *captureConn) RemoteAddr() net.Addr { return nil } func (c *captureConn) SetDeadline(time.Time) error { return nil } func (c *captureConn) SetReadDeadline(time.Time) error { return nil } func (c *captureConn) SetWriteDeadline(time.Time) error { return nil } type replayConn struct { reader *bytes.Reader } func (c *replayConn) Read(p []byte) (int, error) { return c.reader.Read(p) } func (c *replayConn) Write(p []byte) (int, error) { return len(p), nil } func (c *replayConn) Close() error { return nil } func (c *replayConn) LocalAddr() net.Addr { return nil } func (c *replayConn) RemoteAddr() net.Addr { return nil } func (c *replayConn) SetDeadline(time.Time) error { return nil } func (c *replayConn) SetReadDeadline(time.Time) error { return nil } func (c *replayConn) SetWriteDeadline(time.Time) error { return nil } func TestRecordConn_FirstFrameUsesRandomizedCounters(t *testing.T) { pskSend := sha256.Sum256([]byte("record-send")) pskRecv := sha256.Sum256([]byte("record-recv")) raw := &captureConn{} writer, err := NewRecordConn(raw, "chacha20-poly1305", pskSend[:], pskRecv[:]) if err != nil { t.Fatalf("new writer: %v", err) } if writer.sendEpoch == 0 || writer.sendSeq == 0 { t.Fatalf("expected non-zero randomized counters, got epoch=%d seq=%d", writer.sendEpoch, writer.sendSeq) } want := []byte("record prefix camouflage") if _, err := writer.Write(want); err != nil { t.Fatalf("write: %v", err) } wire := raw.Bytes() if len(wire) < 2+recordHeaderSize { t.Fatalf("short frame: %d", len(wire)) } bodyLen := int(binary.BigEndian.Uint16(wire[:2])) if bodyLen != len(wire)-2 { t.Fatalf("body len mismatch: got %d want %d", bodyLen, len(wire)-2) } epoch := binary.BigEndian.Uint32(wire[2:6]) seq := binary.BigEndian.Uint64(wire[6:14]) if epoch == 0 || seq == 0 { t.Fatalf("wire header still starts from zero: epoch=%d seq=%d", epoch, seq) } reader, err := NewRecordConn(&replayConn{reader: bytes.NewReader(wire)}, "chacha20-poly1305", pskRecv[:], pskSend[:]) if err != nil { t.Fatalf("new reader: %v", err) } got := make([]byte, len(want)) if _, err := io.ReadFull(reader, got); err != nil { t.Fatalf("read: %v", err) } if !bytes.Equal(got, want) { t.Fatalf("plaintext mismatch: got %q want %q", got, want) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/directional_hint_test.go ================================================ package sudoku import ( "bytes" "io" "net" "testing" "github.com/metacubex/mihomo/transport/sudoku/crypto" sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) func TestDirectionalCustomTableRotationHintRoundTrip(t *testing.T) { key := "directional-rotate-key" target := "8.8.8.8:53" serverTables, err := NewServerTablesWithCustomPatterns(ClientAEADSeed(key), "up_ascii_down_entropy", "", []string{"xpxvvpvv", "vxpvxvvp"}) if err != nil { t.Fatalf("server tables: %v", err) } if len(serverTables) != 2 { t.Fatalf("expected 2 server tables, got %d", len(serverTables)) } clientTable, err := sudokuobfs.NewTableWithCustom(ClientAEADSeed(key), "up_ascii_down_entropy", "vxpvxvvp") if err != nil { t.Fatalf("client table: %v", err) } serverCfg := DefaultConfig() serverCfg.Key = key serverCfg.AEADMethod = "chacha20-poly1305" serverCfg.Tables = serverTables serverCfg.PaddingMin = 0 serverCfg.PaddingMax = 0 serverCfg.EnablePureDownlink = true serverCfg.HandshakeTimeoutSeconds = 5 serverCfg.DisableHTTPMask = true clientCfg := DefaultConfig() *clientCfg = *serverCfg clientCfg.ServerAddress = "example.com:443" serverConn, clientConn := net.Pipe() defer clientConn.Close() serverErr := make(chan error, 1) go func() { defer close(serverErr) defer serverConn.Close() c, meta, err := ServerHandshake(serverConn, serverCfg) if err != nil { serverErr <- err return } defer c.Close() session, err := ReadServerSession(c, meta) if err != nil { serverErr <- err return } if session.Type != SessionTypeTCP || session.Target != target { serverErr <- io.ErrUnexpectedEOF return } payload := make([]byte, len("client-payload")) if _, err := io.ReadFull(session.Conn, payload); err != nil { serverErr <- err return } if !bytes.Equal(payload, []byte("client-payload")) { serverErr <- io.ErrUnexpectedEOF return } if _, err := session.Conn.Write([]byte("server-reply")); err != nil { serverErr <- err } }() seed := ClientAEADSeed(clientCfg.Key) obfsConn := buildClientObfsConn(clientConn, clientCfg, clientTable) pskC2S, pskS2C := derivePSKDirectionalBases(seed) cConn, err := crypto.NewRecordConn(obfsConn, clientCfg.AEADMethod, pskC2S, pskS2C) if err != nil { t.Fatalf("setup crypto: %v", err) } defer cConn.Close() if _, err := kipHandshakeClient(cConn, seed, kipUserHashFromKey(clientCfg.Key), KIPFeatAll, clientTable.Hint(), true); err != nil { t.Fatalf("client handshake: %v", err) } addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode target: %v", err) } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write target: %v", err) } if _, err := cConn.Write([]byte("client-payload")); err != nil { t.Fatalf("write payload: %v", err) } reply := make([]byte, len("server-reply")) if _, err := io.ReadFull(cConn, reply); err != nil { t.Fatalf("read reply: %v", err) } if !bytes.Equal(reply, []byte("server-reply")) { t.Fatalf("unexpected reply: %q", reply) } if err := <-serverErr; err != nil { t.Fatalf("server: %v", err) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/early_handshake.go ================================================ package sudoku import ( "bytes" "crypto/ecdh" "crypto/rand" "encoding/hex" "fmt" "net" "time" "github.com/metacubex/mihomo/transport/sudoku/crypto" httpmaskobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask" sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) const earlyKIPHandshakeTTL = 60 * time.Second type EarlyCodecConfig struct { PSK string AEAD string EnablePureDownlink bool PaddingMin int PaddingMax int } type EarlyClientState struct { RequestPayload []byte cfg EarlyCodecConfig table *sudokuobfs.Table nonce [kipHelloNonceSize]byte ephemeral *ecdh.PrivateKey sessionC2S []byte sessionS2C []byte responseSet bool } type EarlyServerState struct { ResponsePayload []byte UserHash string cfg EarlyCodecConfig table *sudokuobfs.Table sessionC2S []byte sessionS2C []byte } type ReplayAllowFunc func(userHash string, nonce [kipHelloNonceSize]byte, now time.Time) bool type earlyMemoryConn struct { reader *bytes.Reader write bytes.Buffer } func newEarlyMemoryConn(readBuf []byte) *earlyMemoryConn { return &earlyMemoryConn{reader: bytes.NewReader(readBuf)} } func (c *earlyMemoryConn) Read(p []byte) (int, error) { if c == nil || c.reader == nil { return 0, net.ErrClosed } return c.reader.Read(p) } func (c *earlyMemoryConn) Write(p []byte) (int, error) { if c == nil { return 0, net.ErrClosed } return c.write.Write(p) } func (c *earlyMemoryConn) Close() error { return nil } func (c *earlyMemoryConn) LocalAddr() net.Addr { return earlyDummyAddr("local") } func (c *earlyMemoryConn) RemoteAddr() net.Addr { return earlyDummyAddr("remote") } func (c *earlyMemoryConn) SetDeadline(time.Time) error { return nil } func (c *earlyMemoryConn) SetReadDeadline(time.Time) error { return nil } func (c *earlyMemoryConn) SetWriteDeadline(time.Time) error { return nil } func (c *earlyMemoryConn) Written() []byte { return append([]byte(nil), c.write.Bytes()...) } type earlyDummyAddr string func (a earlyDummyAddr) Network() string { return string(a) } func (a earlyDummyAddr) String() string { return string(a) } func buildEarlyClientObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn { base := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false) downlinkReader := newClientDownlinkReader(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink) if downlinkReader == nil { return base } return newDirectionalConn(raw, downlinkReader, base) } func buildEarlyServerObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn { uplink := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false) downlinkWriter, closers := newServerDownlinkWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink) if downlinkWriter == nil { return uplink } return newDirectionalConn(raw, uplink, downlinkWriter, closers...) } func NewEarlyClientState(cfg EarlyCodecConfig, table *sudokuobfs.Table, tableHint uint32, hasTableHint bool, userHash [kipHelloUserHashSize]byte, feats uint32) (*EarlyClientState, error) { if table == nil { return nil, fmt.Errorf("nil table") } curve := ecdh.X25519() ephemeral, err := curve.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("ecdh generate failed: %w", err) } var nonce [kipHelloNonceSize]byte if _, err := rand.Read(nonce[:]); err != nil { return nil, fmt.Errorf("nonce generate failed: %w", err) } var clientPub [kipHelloPubSize]byte copy(clientPub[:], ephemeral.PublicKey().Bytes()) hello := newKIPClientHello(userHash, nonce, clientPub, feats, tableHint, hasTableHint) mem := newEarlyMemoryConn(nil) obfsConn := buildEarlyClientObfsConn(mem, cfg, table) pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK) rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskC2S, pskS2C) if err != nil { return nil, fmt.Errorf("client early crypto setup failed: %w", err) } if err := WriteKIPMessage(rc, KIPTypeClientHello, hello.EncodePayload()); err != nil { return nil, fmt.Errorf("write early client hello failed: %w", err) } return &EarlyClientState{ RequestPayload: mem.Written(), cfg: cfg, table: table, nonce: nonce, ephemeral: ephemeral, }, nil } func (s *EarlyClientState) ProcessResponse(payload []byte) error { if s == nil { return fmt.Errorf("nil client state") } mem := newEarlyMemoryConn(payload) obfsConn := buildEarlyClientObfsConn(mem, s.cfg, s.table) pskC2S, pskS2C := derivePSKDirectionalBases(s.cfg.PSK) rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, pskC2S, pskS2C) if err != nil { return fmt.Errorf("client early crypto setup failed: %w", err) } msg, err := ReadKIPMessage(rc) if err != nil { return fmt.Errorf("read early server hello failed: %w", err) } if msg.Type != KIPTypeServerHello { return fmt.Errorf("unexpected early handshake message: %d", msg.Type) } sh, err := DecodeKIPServerHelloPayload(msg.Payload) if err != nil { return fmt.Errorf("decode early server hello failed: %w", err) } if sh.Nonce != s.nonce { return fmt.Errorf("early handshake nonce mismatch") } shared, err := x25519SharedSecret(s.ephemeral, sh.ServerPub[:]) if err != nil { return fmt.Errorf("ecdh failed: %w", err) } s.sessionC2S, s.sessionS2C, err = deriveSessionDirectionalBases(s.cfg.PSK, shared, s.nonce) if err != nil { return fmt.Errorf("derive session keys failed: %w", err) } s.responseSet = true return nil } func (s *EarlyClientState) WrapConn(raw net.Conn) (net.Conn, error) { if s == nil { return nil, fmt.Errorf("nil client state") } if !s.responseSet { return nil, fmt.Errorf("early handshake not completed") } obfsConn := buildEarlyClientObfsConn(raw, s.cfg, s.table) rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionC2S, s.sessionS2C) if err != nil { return nil, fmt.Errorf("setup client session crypto failed: %w", err) } return rc, nil } func (s *EarlyClientState) Ready() bool { return s != nil && s.responseSet } func NewHTTPMaskClientEarlyHandshake(cfg EarlyCodecConfig, table *sudokuobfs.Table, tableHint uint32, hasTableHint bool, userHash [kipHelloUserHashSize]byte, feats uint32) (*httpmaskobfs.ClientEarlyHandshake, error) { state, err := NewEarlyClientState(cfg, table, tableHint, hasTableHint, userHash, feats) if err != nil { return nil, err } return &httpmaskobfs.ClientEarlyHandshake{ RequestPayload: state.RequestPayload, HandleResponse: state.ProcessResponse, Ready: state.Ready, WrapConn: state.WrapConn, }, nil } func ProcessEarlyClientPayload(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) { if len(payload) == 0 { return nil, fmt.Errorf("empty early payload") } if len(tables) == 0 { return nil, fmt.Errorf("no tables configured") } var firstErr error for _, table := range tables { state, err := processEarlyClientPayloadForTable(cfg, tables, table, payload, allowReplay) if err == nil { return state, nil } if firstErr == nil { firstErr = err } } if firstErr == nil { firstErr = fmt.Errorf("early handshake probe failed") } return nil, firstErr } func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, table *sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) { mem := newEarlyMemoryConn(payload) obfsConn := buildEarlyServerObfsConn(mem, cfg, table) pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK) rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskS2C, pskC2S) if err != nil { return nil, err } msg, err := ReadKIPMessage(rc) if err != nil { return nil, err } if msg.Type != KIPTypeClientHello { return nil, fmt.Errorf("unexpected handshake message: %d", msg.Type) } ch, err := DecodeKIPClientHelloPayload(msg.Payload) if err != nil { return nil, err } if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(earlyKIPHandshakeTTL.Seconds()) { return nil, fmt.Errorf("time skew/replay") } userHash := hex.EncodeToString(ch.UserHash[:]) if allowReplay != nil && !allowReplay(userHash, ch.Nonce, time.Now()) { return nil, fmt.Errorf("replay detected") } resolvedTable, err := ResolveClientHelloTable(table, tables, ch) if err != nil { return nil, fmt.Errorf("resolve table hint failed: %w", err) } curve := ecdh.X25519() serverEphemeral, err := curve.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("ecdh generate failed: %w", err) } shared, err := x25519SharedSecret(serverEphemeral, ch.ClientPub[:]) if err != nil { return nil, fmt.Errorf("ecdh failed: %w", err) } sessionC2S, sessionS2C, err := deriveSessionDirectionalBases(cfg.PSK, shared, ch.Nonce) if err != nil { return nil, fmt.Errorf("derive session keys failed: %w", err) } var serverPub [kipHelloPubSize]byte copy(serverPub[:], serverEphemeral.PublicKey().Bytes()) serverHello := &KIPServerHello{ Nonce: ch.Nonce, ServerPub: serverPub, SelectedFeats: ch.Features & KIPFeatAll, } respMem := newEarlyMemoryConn(nil) respObfs := buildEarlyServerObfsConn(respMem, cfg, resolvedTable) respConn, err := crypto.NewRecordConn(respObfs, cfg.AEAD, pskS2C, pskC2S) if err != nil { return nil, fmt.Errorf("server early crypto setup failed: %w", err) } if err := WriteKIPMessage(respConn, KIPTypeServerHello, serverHello.EncodePayload()); err != nil { return nil, fmt.Errorf("write early server hello failed: %w", err) } return &EarlyServerState{ ResponsePayload: respMem.Written(), UserHash: userHash, cfg: cfg, table: resolvedTable, sessionC2S: sessionC2S, sessionS2C: sessionS2C, }, nil } func (s *EarlyServerState) WrapConn(raw net.Conn) (net.Conn, error) { if s == nil { return nil, fmt.Errorf("nil server state") } obfsConn := buildEarlyServerObfsConn(raw, s.cfg, s.table) rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionS2C, s.sessionC2S) if err != nil { return nil, fmt.Errorf("setup server session crypto failed: %w", err) } return rc, nil } func NewHTTPMaskServerEarlyHandshake(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, allowReplay ReplayAllowFunc) *httpmaskobfs.TunnelServerEarlyHandshake { return &httpmaskobfs.TunnelServerEarlyHandshake{ Prepare: func(payload []byte) (*httpmaskobfs.PreparedServerEarlyHandshake, error) { state, err := ProcessEarlyClientPayload(cfg, tables, payload, allowReplay) if err != nil { return nil, err } return &httpmaskobfs.PreparedServerEarlyHandshake{ ResponsePayload: state.ResponsePayload, WrapConn: state.WrapConn, UserHash: state.UserHash, }, nil }, } } ================================================ FILE: core/Clash.Meta/transport/sudoku/features_test.go ================================================ package sudoku import ( "bytes" "io" "net" "testing" sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) func TestCustomTablesRotation_ProbedByServer(t *testing.T) { key := "rotate-test-key" target := "8.8.8.8:53" t1, err := sudokuobfs.NewTableWithCustom("rotate-seed", "prefer_entropy", "xpxvvpvv") if err != nil { t.Fatalf("t1: %v", err) } t2, err := sudokuobfs.NewTableWithCustom("rotate-seed", "prefer_entropy", "vxpvxvvp") if err != nil { t.Fatalf("t2: %v", err) } serverCfg := DefaultConfig() serverCfg.Key = key serverCfg.AEADMethod = "chacha20-poly1305" serverCfg.Tables = []*sudokuobfs.Table{t1, t2} serverCfg.PaddingMin = 0 serverCfg.PaddingMax = 0 serverCfg.EnablePureDownlink = true serverCfg.HandshakeTimeoutSeconds = 5 serverCfg.DisableHTTPMask = true clientCfg := DefaultConfig() *clientCfg = *serverCfg clientCfg.ServerAddress = "example.com:443" for i := 0; i < 10; i++ { serverConn, clientConn := net.Pipe() errCh := make(chan error, 1) go func() { defer close(errCh) defer serverConn.Close() c, meta, err := ServerHandshake(serverConn, serverCfg) if err != nil { errCh <- err return } session, err := ReadServerSession(c, meta) if err != nil { errCh <- err return } defer c.Close() if session.Type != SessionTypeTCP { errCh <- io.ErrUnexpectedEOF return } if session.Target != target { errCh <- io.ErrClosedPipe return } _, _ = session.Conn.Write([]byte{0xaa, 0xbb, 0xcc}) }() cConn, err := ClientHandshake(clientConn, clientCfg) if err != nil { t.Fatalf("client handshake: %v", err) } addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode addr: %v", err) } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write addr: %v", err) } buf := make([]byte, 3) if _, err := io.ReadFull(cConn, buf); err != nil { t.Fatalf("read: %v", err) } if !bytes.Equal(buf, []byte{0xaa, 0xbb, 0xcc}) { t.Fatalf("payload mismatch: %x", buf) } _ = cConn.Close() if err := <-errCh; err != nil { t.Fatalf("server: %v", err) } } } func TestDirectionalTrafficRoundTrip(t *testing.T) { tests := []struct { name string mode string pure bool }{ {name: "UpASCII_DownEntropy_Pure", mode: "up_ascii_down_entropy", pure: true}, {name: "UpASCII_DownEntropy_Packed", mode: "up_ascii_down_entropy", pure: false}, {name: "UpEntropy_DownASCII_Pure", mode: "up_entropy_down_ascii", pure: true}, {name: "UpEntropy_DownASCII_Packed", mode: "up_entropy_down_ascii", pure: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { key := "directional-test-key-" + tt.name target := "8.8.8.8:53" table, err := sudokuobfs.NewTableWithCustom(ClientAEADSeed(key), tt.mode, "xpxvvpvv") if err != nil { t.Fatalf("table: %v", err) } serverCfg := DefaultConfig() serverCfg.Key = key serverCfg.AEADMethod = "chacha20-poly1305" serverCfg.Table = table serverCfg.PaddingMin = 0 serverCfg.PaddingMax = 0 serverCfg.EnablePureDownlink = tt.pure serverCfg.HandshakeTimeoutSeconds = 5 serverCfg.DisableHTTPMask = true clientCfg := DefaultConfig() *clientCfg = *serverCfg clientCfg.ServerAddress = "example.com:443" serverConn, clientConn := net.Pipe() defer clientConn.Close() serverErr := make(chan error, 1) go func() { defer close(serverErr) defer serverConn.Close() c, meta, err := ServerHandshake(serverConn, serverCfg) if err != nil { serverErr <- err return } defer c.Close() session, err := ReadServerSession(c, meta) if err != nil { serverErr <- err return } if session.Type != SessionTypeTCP { serverErr <- io.ErrUnexpectedEOF return } if session.Target != target { serverErr <- io.ErrClosedPipe return } want := []byte("client-payload") got := make([]byte, len(want)) if _, err := io.ReadFull(session.Conn, got); err != nil { serverErr <- err return } if !bytes.Equal(got, want) { serverErr <- io.ErrUnexpectedEOF return } if _, err := session.Conn.Write([]byte("server-reply")); err != nil { serverErr <- err return } }() cConn, err := ClientHandshake(clientConn, clientCfg) if err != nil { t.Fatalf("client handshake: %v", err) } defer cConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode target: %v", err) } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write target: %v", err) } if _, err := cConn.Write([]byte("client-payload")); err != nil { t.Fatalf("write payload: %v", err) } reply := make([]byte, len("server-reply")) if _, err := io.ReadFull(cConn, reply); err != nil { t.Fatalf("read reply: %v", err) } if !bytes.Equal(reply, []byte("server-reply")) { t.Fatalf("unexpected reply: %q", reply) } if err := <-serverErr; err != nil { t.Fatalf("server: %v", err) } }) } } func TestDirectionalTrafficRoundTripTCP(t *testing.T) { tests := []struct { name string mode string pure bool }{ {name: "UpASCII_DownEntropy_Pure_TCP", mode: "up_ascii_down_entropy", pure: true}, {name: "UpEntropy_DownASCII_Packed_TCP", mode: "up_entropy_down_ascii", pure: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { key := "directional-tcp-test-key-" + tt.name target := "127.0.0.1:18080" table, err := sudokuobfs.NewTableWithCustom(ClientAEADSeed(key), tt.mode, "xpxvvpvv") if err != nil { t.Fatalf("table: %v", err) } serverCfg := DefaultConfig() serverCfg.Key = key serverCfg.AEADMethod = "chacha20-poly1305" serverCfg.Table = table serverCfg.PaddingMin = 0 serverCfg.PaddingMax = 0 serverCfg.EnablePureDownlink = tt.pure serverCfg.HandshakeTimeoutSeconds = 5 serverCfg.DisableHTTPMask = true ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } defer ln.Close() serverErr := make(chan error, 1) go func() { defer close(serverErr) raw, err := ln.Accept() if err != nil { serverErr <- err return } defer raw.Close() c, meta, err := ServerHandshake(raw, serverCfg) if err != nil { serverErr <- err return } defer c.Close() session, err := ReadServerSession(c, meta) if err != nil { serverErr <- err return } if session.Type != SessionTypeTCP || session.Target != target { serverErr <- io.ErrUnexpectedEOF return } want := []byte("client-payload") got := make([]byte, len(want)) if _, err := io.ReadFull(session.Conn, got); err != nil { serverErr <- err return } if !bytes.Equal(got, want) { serverErr <- io.ErrUnexpectedEOF return } if _, err := session.Conn.Write([]byte("server-reply")); err != nil { serverErr <- err return } }() clientCfg := DefaultConfig() *clientCfg = *serverCfg clientCfg.ServerAddress = ln.Addr().String() raw, err := net.Dial("tcp", clientCfg.ServerAddress) if err != nil { t.Fatalf("dial: %v", err) } defer raw.Close() cConn, err := ClientHandshake(raw, clientCfg) if err != nil { t.Fatalf("client handshake: %v", err) } defer cConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode target: %v", err) } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write target: %v", err) } if _, err := cConn.Write([]byte("client-payload")); err != nil { t.Fatalf("write payload: %v", err) } reply := make([]byte, len("server-reply")) if _, err := io.ReadFull(cConn, reply); err != nil { t.Fatalf("read reply: %v", err) } if !bytes.Equal(reply, []byte("server-reply")) { t.Fatalf("unexpected reply: %q", reply) } if err := <-serverErr; err != nil { t.Fatalf("server: %v", err) } }) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/handshake.go ================================================ package sudoku import ( "bufio" "bytes" "crypto/ecdh" "crypto/rand" "encoding/hex" "fmt" "io" "net" "strings" "sync" "time" "github.com/metacubex/mihomo/transport/sudoku/crypto" "github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask" "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) type SessionType int const ( SessionTypeTCP SessionType = iota SessionTypeUoT SessionTypeMultiplex ) type ServerSession struct { Conn net.Conn Type SessionType Target string // UserHash is a stable per-key identifier derived from the client hello payload. UserHash string } type HandshakeMeta struct { UserHash string } // SuspiciousError indicates a potential probing attempt or protocol violation. // When returned, Conn (if non-nil) should contain all bytes already consumed/buffered so the caller // can perform a best-effort fallback relay (e.g. to a local web server) without losing the request. type SuspiciousError struct { Err error Conn net.Conn } func (e *SuspiciousError) Error() string { if e == nil || e.Err == nil { return "" } return e.Err.Error() } func (e *SuspiciousError) Unwrap() error { return e.Err } type recordedConn struct { net.Conn recorded []byte } func (rc *recordedConn) GetBufferedAndRecorded() []byte { return rc.recorded } type prefixedRecorderConn struct { net.Conn prefix []byte } func (pc *prefixedRecorderConn) GetBufferedAndRecorded() []byte { var rest []byte if r, ok := pc.Conn.(interface{ GetBufferedAndRecorded() []byte }); ok { rest = r.GetBufferedAndRecorded() } out := make([]byte, 0, len(pc.prefix)+len(rest)) out = append(out, pc.prefix...) out = append(out, rest...) return out } // bufferedRecorderConn wraps a net.Conn and a shared bufio.Reader so we can expose buffered bytes. // This is used for legacy HTTP mask parsing errors so callers can fall back to a real HTTP server. type bufferedRecorderConn struct { net.Conn r *bufio.Reader recorder *bytes.Buffer mu sync.Mutex } func (bc *bufferedRecorderConn) Read(p []byte) (n int, err error) { n, err = bc.r.Read(p) if n > 0 && bc.recorder != nil { bc.mu.Lock() bc.recorder.Write(p[:n]) bc.mu.Unlock() } return n, err } func (bc *bufferedRecorderConn) GetBufferedAndRecorded() []byte { if bc == nil { return nil } bc.mu.Lock() defer bc.mu.Unlock() var recorded []byte if bc.recorder != nil { recorded = bc.recorder.Bytes() } buffered := 0 if bc.r != nil { buffered = bc.r.Buffered() } if buffered <= 0 { return recorded } peeked, _ := bc.r.Peek(buffered) full := make([]byte, len(recorded)+len(peeked)) copy(full, recorded) copy(full[len(recorded):], peeked) return full } type preBufferedConn struct { net.Conn buf []byte } func (p *preBufferedConn) Read(b []byte) (int, error) { if len(p.buf) > 0 { n := copy(b, p.buf) p.buf = p.buf[n:] return n, nil } if p.Conn == nil { return 0, io.EOF } return p.Conn.Read(b) } func (p *preBufferedConn) CloseWrite() error { if p == nil { return nil } if cw, ok := p.Conn.(interface{ CloseWrite() error }); ok { return cw.CloseWrite() } return nil } func (p *preBufferedConn) CloseRead() error { if p == nil { return nil } if cr, ok := p.Conn.(interface{ CloseRead() error }); ok { return cr.CloseRead() } return nil } type directionalConn struct { net.Conn reader io.Reader writer io.Writer closers []func() error } func newDirectionalConn(base net.Conn, reader io.Reader, writer io.Writer, closers ...func() error) net.Conn { return &directionalConn{ Conn: base, reader: reader, writer: writer, closers: closers, } } func (c *directionalConn) Read(p []byte) (int, error) { return c.reader.Read(p) } func (c *directionalConn) Write(p []byte) (int, error) { return c.writer.Write(p) } func (c *directionalConn) ReplaceWriter(writer io.Writer, closers ...func() error) { if c == nil { return } c.writer = writer c.closers = closers } func (c *directionalConn) Close() error { var firstErr error for _, fn := range c.closers { if fn == nil { continue } if err := fn(); err != nil && firstErr == nil { firstErr = err } } if err := c.Conn.Close(); err != nil && firstErr == nil { firstErr = err } return firstErr } func (c *directionalConn) CloseWrite() error { if c == nil { return nil } if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok { return cw.CloseWrite() } return nil } func (c *directionalConn) CloseRead() error { if c == nil { return nil } if cr, ok := c.Conn.(interface{ CloseRead() error }); ok { return cr.CloseRead() } return nil } func absInt64(v int64) int64 { if v < 0 { return -v } return v } func oppositeDirectionTable(table *sudoku.Table) *sudoku.Table { if table == nil { return nil } if other := table.OppositeDirection(); other != nil { return other } return table } func newClientDownlinkReader(raw net.Conn, table *sudoku.Table, paddingMin, paddingMax int, pureDownlink bool) io.Reader { downlinkTable := oppositeDirectionTable(table) if pureDownlink { if downlinkTable == table { return nil } return sudoku.NewConn(raw, downlinkTable, paddingMin, paddingMax, false) } return sudoku.NewPackedConn(raw, downlinkTable, paddingMin, paddingMax) } func newServerDownlinkWriter(raw net.Conn, table *sudoku.Table, paddingMin, paddingMax int, pureDownlink bool) (io.Writer, []func() error) { downlinkTable := oppositeDirectionTable(table) if pureDownlink { if downlinkTable == table { return nil, nil } return sudoku.NewConn(raw, downlinkTable, paddingMin, paddingMax, false), nil } packed := sudoku.NewPackedConn(raw, downlinkTable, paddingMin, paddingMax) return packed, []func() error{packed.Flush} } func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn { baseSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false) downlinkReader := newClientDownlinkReader(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink) if downlinkReader == nil { return baseSudoku } return newDirectionalConn(raw, downlinkReader, baseSudoku) } func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) { uplinkSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record) downlinkWriter, closers := newServerDownlinkWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink) if downlinkWriter == nil { return uplinkSudoku, uplinkSudoku } return uplinkSudoku, newDirectionalConn(raw, uplinkSudoku, downlinkWriter, closers...) } func isLegacyHTTPMaskMode(mode string) bool { switch strings.ToLower(strings.TrimSpace(mode)) { case "", "legacy": return true default: return false } } // ClientHandshake performs the client-side Sudoku handshake (no target request). func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) { if cfg == nil { return nil, fmt.Errorf("config is required") } if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } if !cfg.DisableHTTPMask && isLegacyHTTPMaskMode(cfg.HTTPMaskMode) { if err := httpmask.WriteRandomRequestHeaderWithPathRoot(rawConn, cfg.ServerAddress, cfg.HTTPMaskPathRoot); err != nil { return nil, fmt.Errorf("write http mask failed: %w", err) } } choice, err := pickClientTable(cfg) if err != nil { return nil, err } seed := ClientAEADSeed(cfg.Key) obfsConn := buildClientObfsConn(rawConn, cfg, choice.Table) pskC2S, pskS2C := derivePSKDirectionalBases(seed) rc, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskC2S, pskS2C) if err != nil { return nil, fmt.Errorf("setup crypto failed: %w", err) } if _, err := kipHandshakeClient(rc, seed, kipUserHashFromKey(cfg.Key), KIPFeatAll, choice.Hint, choice.HasHint); err != nil { _ = rc.Close() return nil, err } return rc, nil } func readFirstSessionMessage(conn net.Conn) (*KIPMessage, error) { for { msg, err := ReadKIPMessage(conn) if err != nil { return nil, err } if msg.Type == KIPTypeKeepAlive { continue } return msg, nil } } func maybeConsumeLegacyHTTPMask(rawConn net.Conn, r *bufio.Reader, cfg *ProtocolConfig) ([]byte, *SuspiciousError) { if rawConn == nil || r == nil || cfg == nil || cfg.DisableHTTPMask || !isLegacyHTTPMaskMode(cfg.HTTPMaskMode) { return nil, nil } peekBytes, _ := r.Peek(4) // ignore error; subsequent read will handle it if !httpmask.LooksLikeHTTPRequestStart(peekBytes) { return nil, nil } consumed, err := httpmask.ConsumeHeader(r) if err == nil { return consumed, nil } recorder := new(bytes.Buffer) if len(consumed) > 0 { recorder.Write(consumed) } badConn := &bufferedRecorderConn{Conn: rawConn, r: r, recorder: recorder} return consumed, &SuspiciousError{Err: fmt.Errorf("invalid http header: %w", err), Conn: badConn} } // ServerHandshake performs the server-side KIP handshake. func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, *HandshakeMeta, error) { if rawConn == nil { return nil, nil, fmt.Errorf("nil conn") } if cfg == nil { return nil, nil, fmt.Errorf("config is required") } if err := cfg.Validate(); err != nil { return nil, nil, fmt.Errorf("invalid config: %w", err) } if userHash, ok := httpmask.EarlyHandshakeUserHash(rawConn); ok { return rawConn, &HandshakeMeta{UserHash: userHash}, nil } handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second if handshakeTimeout <= 0 { handshakeTimeout = 5 * time.Second } bufReader := bufio.NewReader(rawConn) _ = rawConn.SetReadDeadline(time.Now().Add(handshakeTimeout)) defer func() { _ = rawConn.SetReadDeadline(time.Time{}) }() httpHeaderData, susp := maybeConsumeLegacyHTTPMask(rawConn, bufReader, cfg) if susp != nil { return nil, nil, susp } selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates()) if err != nil { combined := make([]byte, 0, len(httpHeaderData)+len(preRead)) combined = append(combined, httpHeaderData...) combined = append(combined, preRead...) return nil, nil, &SuspiciousError{Err: err, Conn: &recordedConn{Conn: rawConn, recorded: combined}} } baseConn := &preBufferedConn{Conn: rawConn, buf: preRead} sConn, obfsConn := buildServerObfsConn(baseConn, cfg, selectedTable, true) seed := ServerAEADSeed(cfg.Key) pskC2S, pskS2C := derivePSKDirectionalBases(seed) // Server side: recv is client->server, send is server->client. rc, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskS2C, pskC2S) if err != nil { return nil, nil, fmt.Errorf("setup crypto failed: %w", err) } msg, err := ReadKIPMessage(rc) if err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("handshake read failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } if msg.Type != KIPTypeClientHello { return nil, nil, &SuspiciousError{Err: fmt.Errorf("unexpected handshake message: %d", msg.Type), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } ch, err := DecodeKIPClientHelloPayload(msg.Payload) if err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("decode client hello failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(kipHandshakeSkew.Seconds()) { return nil, nil, &SuspiciousError{Err: fmt.Errorf("time skew/replay"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } userHashHex := hex.EncodeToString(ch.UserHash[:]) if !globalHandshakeReplay.allow(userHashHex, ch.Nonce, time.Now()) { return nil, nil, &SuspiciousError{Err: fmt.Errorf("replay"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } resolvedTable, err := ResolveClientHelloTable(selectedTable, cfg.tableCandidates(), ch) if err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("resolve table hint failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } if resolvedTable != selectedTable { downlinkWriter, closers := newServerDownlinkWriter(baseConn, resolvedTable, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink) switchable, ok := obfsConn.(*directionalConn) if !ok { return nil, nil, &SuspiciousError{Err: fmt.Errorf("switch downlink writer failed"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } switchable.ReplaceWriter(downlinkWriter, closers...) } curve := ecdh.X25519() serverEphemeral, err := curve.GenerateKey(rand.Reader) if err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("ecdh generate failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } shared, err := x25519SharedSecret(serverEphemeral, ch.ClientPub[:]) if err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("ecdh failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } sessC2S, sessS2C, err := deriveSessionDirectionalBases(seed, shared, ch.Nonce) if err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("derive session keys failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } var serverPub [kipHelloPubSize]byte copy(serverPub[:], serverEphemeral.PublicKey().Bytes()) sh := &KIPServerHello{ Nonce: ch.Nonce, ServerPub: serverPub, SelectedFeats: ch.Features & KIPFeatAll, } if err := WriteKIPMessage(rc, KIPTypeServerHello, sh.EncodePayload()); err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("write server hello failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } if err := rc.Rekey(sessS2C, sessC2S); err != nil { return nil, nil, &SuspiciousError{Err: fmt.Errorf("rekey failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}} } sConn.StopRecording() return rc, &HandshakeMeta{UserHash: userHashHex}, nil } // ReadServerSession consumes the first post-handshake KIP control message and returns the session intent. func ReadServerSession(conn net.Conn, meta *HandshakeMeta) (*ServerSession, error) { if conn == nil { return nil, fmt.Errorf("nil conn") } userHash := "" if meta != nil { userHash = meta.UserHash } first, err := readFirstSessionMessage(conn) if err != nil { return nil, err } switch first.Type { case KIPTypeStartUoT: return &ServerSession{Conn: conn, Type: SessionTypeUoT, UserHash: userHash}, nil case KIPTypeStartMux: return &ServerSession{Conn: conn, Type: SessionTypeMultiplex, UserHash: userHash}, nil case KIPTypeOpenTCP: target, err := DecodeAddress(bytes.NewReader(first.Payload)) if err != nil { return nil, fmt.Errorf("decode target address failed: %w", err) } return &ServerSession{Conn: conn, Type: SessionTypeTCP, Target: target, UserHash: userHash}, nil default: return nil, fmt.Errorf("unknown kip message: %d", first.Type) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/handshake_kip.go ================================================ package sudoku import ( "crypto/ecdh" "crypto/rand" "fmt" "io" "time" "github.com/metacubex/mihomo/transport/sudoku/crypto" ) const kipHandshakeSkew = 60 * time.Second func kipHandshakeClient(rc *crypto.RecordConn, seed string, userHash [kipHelloUserHashSize]byte, feats uint32, tableHint uint32, hasTableHint bool) (uint32, error) { if rc == nil { return 0, fmt.Errorf("nil conn") } curve := ecdh.X25519() ephemeral, err := curve.GenerateKey(rand.Reader) if err != nil { return 0, fmt.Errorf("ecdh generate failed: %w", err) } var nonce [kipHelloNonceSize]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { return 0, fmt.Errorf("nonce generate failed: %w", err) } var clientPub [kipHelloPubSize]byte copy(clientPub[:], ephemeral.PublicKey().Bytes()) ch := newKIPClientHello(userHash, nonce, clientPub, feats, tableHint, hasTableHint) if err := WriteKIPMessage(rc, KIPTypeClientHello, ch.EncodePayload()); err != nil { return 0, fmt.Errorf("write client hello failed: %w", err) } msg, err := ReadKIPMessage(rc) if err != nil { return 0, fmt.Errorf("read server hello failed: %w", err) } if msg.Type != KIPTypeServerHello { return 0, fmt.Errorf("unexpected handshake message: %d", msg.Type) } sh, err := DecodeKIPServerHelloPayload(msg.Payload) if err != nil { return 0, fmt.Errorf("decode server hello failed: %w", err) } if sh.Nonce != nonce { return 0, fmt.Errorf("handshake nonce mismatch") } shared, err := x25519SharedSecret(ephemeral, sh.ServerPub[:]) if err != nil { return 0, fmt.Errorf("ecdh failed: %w", err) } sessC2S, sessS2C, err := deriveSessionDirectionalBases(seed, shared, nonce) if err != nil { return 0, fmt.Errorf("derive session keys failed: %w", err) } if err := rc.Rekey(sessC2S, sessS2C); err != nil { return 0, fmt.Errorf("rekey failed: %w", err) } return sh.SelectedFeats, nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/handshake_test.go ================================================ package sudoku import ( "bytes" "fmt" "io" "net" "sync" "testing" "time" sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) func TestPackedConnRoundTrip_WithPadding(t *testing.T) { payload := []byte{0x3a, 0x1f, 0x71, 0x00, 0xff, 0x10, 0x22} tableTypes := []string{"prefer_ascii", "prefer_entropy"} for _, tt := range tableTypes { t.Run(tt, func(t *testing.T) { serverConn, clientConn := net.Pipe() defer serverConn.Close() defer clientConn.Close() table := sudokuobfs.NewTable("roundtrip-seed", tt) writer := sudokuobfs.NewPackedConn(serverConn, table, 30, 80) reader := sudokuobfs.NewPackedConn(clientConn, table, 30, 80) writeErr := make(chan error, 1) go func() { if _, err := writer.Write(payload); err != nil { writeErr <- err return } if err := writer.Flush(); err != nil { writeErr <- err return } writeErr <- serverConn.Close() }() done := make(chan struct{}) var got []byte var readErr error go func() { got, readErr = io.ReadAll(reader) close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("read timeout") } if err := <-writeErr; err != nil && err != io.EOF { t.Fatalf("write side error: %v", err) } if readErr != nil && readErr != io.EOF { t.Fatalf("read side error: %v", readErr) } if !bytes.Equal(got, payload) { t.Fatalf("payload mismatch, want %x got %x", payload, got) } }) } } func newPackedConfig(table *sudokuobfs.Table) *ProtocolConfig { cfg := DefaultConfig() cfg.Key = "sudoku-test-key" cfg.Table = table cfg.PaddingMin = 10 cfg.PaddingMax = 30 cfg.EnablePureDownlink = false cfg.ServerAddress = "example.com:443" cfg.DisableHTTPMask = true return cfg } func TestPackedDownlinkSoak(t *testing.T) { const sessions = 16 table := sudokuobfs.NewTable("soak-seed", "prefer_ascii") cfg := newPackedConfig(table) var wg sync.WaitGroup errCh := make(chan error, sessions*2) for i := 0; i < sessions; i++ { wg.Add(2) go func(id int) { defer wg.Done() runPackedTCPSession(id, cfg, errCh) }(i) go func(id int) { defer wg.Done() runPackedUoTSession(id, cfg, errCh) }(i) } done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: case <-time.After(10 * time.Second): t.Fatal("soak test timeout") } close(errCh) for err := range errCh { t.Fatalf("soak error: %v", err) } } func runPackedTCPSession(id int, cfg *ProtocolConfig, errCh chan<- error) { serverConn, clientConn := net.Pipe() target := fmt.Sprintf("1.1.1.%d:80", (id%200)+1) payload := []byte{0x42, byte(id)} // Server side go func() { c, meta, err := ServerHandshake(serverConn, cfg) if err != nil { errCh <- fmt.Errorf("server handshake tcp: %w", err) return } defer c.Close() session, err := ReadServerSession(c, meta) if err != nil { errCh <- fmt.Errorf("server read session tcp: %w", err) return } if session.Type != SessionTypeTCP { errCh <- fmt.Errorf("unexpected session type: %v", session.Type) return } if session.Target != target { errCh <- fmt.Errorf("target mismatch want %s got %s", target, session.Target) return } if _, err := session.Conn.Write(payload); err != nil { errCh <- fmt.Errorf("server write: %w", err) return } }() // Client side clientCfg := *cfg cConn, err := ClientHandshake(clientConn, &clientCfg) if err != nil { errCh <- fmt.Errorf("client handshake tcp: %w", err) return } defer cConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { errCh <- fmt.Errorf("encode address: %w", err) return } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { errCh <- fmt.Errorf("client send open tcp: %w", err) return } buf := make([]byte, len(payload)) if _, err := io.ReadFull(cConn, buf); err != nil { errCh <- fmt.Errorf("client read: %w", err) return } if !bytes.Equal(buf, payload) { errCh <- fmt.Errorf("payload mismatch want %x got %x", payload, buf) return } } func runPackedUoTSession(id int, cfg *ProtocolConfig, errCh chan<- error) { serverConn, clientConn := net.Pipe() target := "8.8.8.8:53" payload := []byte{0xaa, byte(id)} // Server side go func() { c, meta, err := ServerHandshake(serverConn, cfg) if err != nil { errCh <- fmt.Errorf("server handshake uot: %w", err) return } defer c.Close() session, err := ReadServerSession(c, meta) if err != nil { errCh <- fmt.Errorf("server read session uot: %w", err) return } if session.Type != SessionTypeUoT { errCh <- fmt.Errorf("unexpected session type: %v", session.Type) return } if err := WriteDatagram(session.Conn, target, payload); err != nil { errCh <- fmt.Errorf("server write datagram: %w", err) return } }() // Client side clientCfg := *cfg cConn, err := ClientHandshake(clientConn, &clientCfg) if err != nil { errCh <- fmt.Errorf("client handshake uot: %w", err) return } defer cConn.Close() if err := WriteKIPMessage(cConn, KIPTypeStartUoT, nil); err != nil { errCh <- fmt.Errorf("client start uot: %w", err) return } addr, data, err := ReadDatagram(cConn) if err != nil { errCh <- fmt.Errorf("client read datagram: %w", err) return } if addr != target { errCh <- fmt.Errorf("uot target mismatch want %s got %s", target, addr) return } if !bytes.Equal(data, payload) { errCh <- fmt.Errorf("uot payload mismatch want %x got %x", payload, data) return } } func TestCustomTableHandshake(t *testing.T) { table, err := sudokuobfs.NewTableWithCustom("custom-seed", "prefer_entropy", "xpxvvpvv") if err != nil { t.Fatalf("build custom table: %v", err) } cfg := newPackedConfig(table) errCh := make(chan error, 2) runPackedTCPSession(42, cfg, errCh) runPackedUoTSession(43, cfg, errCh) close(errCh) for err := range errCh { if err != nil { t.Fatalf("custom table handshake failed: %v", err) } } } ================================================ FILE: core/Clash.Meta/transport/sudoku/httpmask_tunnel.go ================================================ package sudoku import ( "context" "fmt" "net" "strings" "github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask" ) type HTTPMaskTunnelServer struct { cfg *ProtocolConfig ts *httpmask.TunnelServer } func newHTTPMaskEarlyCodecConfig(cfg *ProtocolConfig, psk string) EarlyCodecConfig { return EarlyCodecConfig{ PSK: psk, AEAD: cfg.AEADMethod, EnablePureDownlink: cfg.EnablePureDownlink, PaddingMin: cfg.PaddingMin, PaddingMax: cfg.PaddingMax, } } func newClientHTTPMaskEarlyHandshake(cfg *ProtocolConfig) (*httpmask.ClientEarlyHandshake, error) { choice, err := pickClientTable(cfg) if err != nil { return nil, err } return NewHTTPMaskClientEarlyHandshake( newHTTPMaskEarlyCodecConfig(cfg, ClientAEADSeed(cfg.Key)), choice.Table, choice.Hint, choice.HasHint, kipUserHashFromKey(cfg.Key), KIPFeatAll, ) } func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer { return newHTTPMaskTunnelServer(cfg, false) } func NewHTTPMaskTunnelServerWithFallback(cfg *ProtocolConfig) *HTTPMaskTunnelServer { return newHTTPMaskTunnelServer(cfg, true) } func newHTTPMaskTunnelServer(cfg *ProtocolConfig, passThroughOnReject bool) *HTTPMaskTunnelServer { if cfg == nil { return &HTTPMaskTunnelServer{} } var ts *httpmask.TunnelServer if !cfg.DisableHTTPMask { switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) { case "stream", "poll", "auto", "ws": ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{ Mode: cfg.HTTPMaskMode, PathRoot: cfg.HTTPMaskPathRoot, AuthKey: ServerAEADSeed(cfg.Key), EarlyHandshake: NewHTTPMaskServerEarlyHandshake( newHTTPMaskEarlyCodecConfig(cfg, ServerAEADSeed(cfg.Key)), cfg.tableCandidates(), globalHandshakeReplay.allow, ), // When upstream fallback is enabled, preserve rejected HTTP requests for the caller. PassThroughOnReject: passThroughOnReject, }) } } return &HTTPMaskTunnelServer{cfg: cfg, ts: ts} } // WrapConn inspects an accepted TCP connection and upgrades it to an HTTP tunnel stream when needed. // // Returns: // - done=true: this TCP connection has been fully handled (e.g., stream/poll control request), caller should return // - done=false: handshakeConn+cfg are ready for ServerHandshake func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Conn, cfg *ProtocolConfig, done bool, err error) { if rawConn == nil { return nil, nil, true, fmt.Errorf("nil conn") } if s == nil { return rawConn, nil, false, nil } if s.ts == nil { return rawConn, s.cfg, false, nil } res, c, err := s.ts.HandleConn(rawConn) if err != nil { return nil, nil, true, err } switch res { case httpmask.HandleDone: return nil, nil, true, nil case httpmask.HandlePassThrough: return c, s.cfg, false, nil case httpmask.HandleStartTunnel: inner := *s.cfg inner.DisableHTTPMask = true // HTTPMask tunnel modes (stream/poll/auto/ws) add extra round trips before the first // handshake bytes can reach ServerHandshake, especially under high concurrency. // Bump the handshake timeout for tunneled conns to avoid flaky timeouts while keeping // the default strict for raw TCP handshakes. const minTunneledHandshakeTimeoutSeconds = 15 if inner.HandshakeTimeoutSeconds <= 0 || inner.HandshakeTimeoutSeconds < minTunneledHandshakeTimeoutSeconds { inner.HandshakeTimeoutSeconds = minTunneledHandshakeTimeoutSeconds } return c, &inner, false, nil default: return nil, nil, true, nil } } type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error) // DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto/ws) and returns a stream carrying raw Sudoku bytes. func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) { if cfg == nil { return nil, fmt.Errorf("config is required") } if cfg.DisableHTTPMask { return nil, fmt.Errorf("http mask is disabled") } switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) { case "stream", "poll", "auto", "ws": default: return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode) } var ( earlyHandshake *httpmask.ClientEarlyHandshake err error ) if upgrade != nil { earlyHandshake, err = newClientHTTPMaskEarlyHandshake(cfg) if err != nil { return nil, err } } return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{ Mode: cfg.HTTPMaskMode, TLSEnabled: cfg.HTTPMaskTLSEnabled, HostOverride: cfg.HTTPMaskHost, PathRoot: cfg.HTTPMaskPathRoot, AuthKey: ClientAEADSeed(cfg.Key), EarlyHandshake: earlyHandshake, Upgrade: upgrade, Multiplex: cfg.HTTPMaskMultiplex, DialContext: dial, }) } ================================================ FILE: core/Clash.Meta/transport/sudoku/httpmask_tunnel_test.go ================================================ package sudoku import ( "bytes" "context" "fmt" "io" "net" "strings" "sync" "testing" "time" ) func startTunnelServer(t *testing.T, cfg *ProtocolConfig, handle func(*ServerSession) error) (addr string, stop func(), errCh <-chan error) { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } errC := make(chan error, 128) done := make(chan struct{}) tunnelSrv := NewHTTPMaskTunnelServer(cfg) var wg sync.WaitGroup var stopOnce sync.Once wg.Add(1) go func() { defer wg.Done() for { c, err := ln.Accept() if err != nil { close(done) return } wg.Add(1) go func(conn net.Conn) { defer wg.Done() handshakeConn, handshakeCfg, handled, err := tunnelSrv.WrapConn(conn) if err != nil { _ = conn.Close() if nerr, ok := err.(net.Error); ok && nerr.Timeout() { return } if err == io.EOF { return } errC <- err return } if handled { return } if handshakeConn == nil || handshakeCfg == nil { _ = conn.Close() errC <- fmt.Errorf("wrap conn returned nil") return } cConn, meta, err := ServerHandshake(handshakeConn, handshakeCfg) if err != nil { _ = handshakeConn.Close() if handshakeConn != conn { _ = conn.Close() } errC <- err return } defer cConn.Close() session, err := ReadServerSession(cConn, meta) if err != nil { errC <- err return } if handleErr := handle(session); handleErr != nil { errC <- handleErr } }(c) } }() stop = func() { stopOnce.Do(func() { _ = ln.Close() select { case <-done: case <-time.After(5 * time.Second): t.Fatalf("server did not stop") } ch := make(chan struct{}) go func() { wg.Wait() close(ch) }() select { case <-ch: case <-time.After(10 * time.Second): t.Fatalf("server goroutines did not exit") } close(errC) }) } return ln.Addr().String(), stop, errC } func newTunnelTestTable(t *testing.T, key string) *ProtocolConfig { t.Helper() tables, err := NewTablesWithCustomPatterns(ClientAEADSeed(key), "prefer_ascii", "", nil) if err != nil { t.Fatalf("build tables: %v", err) } if len(tables) != 1 { t.Fatalf("unexpected tables: %d", len(tables)) } cfg := DefaultConfig() cfg.Key = key cfg.AEADMethod = "chacha20-poly1305" cfg.Table = tables[0] cfg.PaddingMin = 0 cfg.PaddingMax = 0 cfg.HandshakeTimeoutSeconds = 5 cfg.EnablePureDownlink = true cfg.DisableHTTPMask = false return cfg } func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) { key := "tunnel-stream-key" target := "1.1.1.1:80" serverCfg := newTunnelTestTable(t, key) serverCfg.HTTPMaskMode = "stream" addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { if s.Type != SessionTypeTCP { return fmt.Errorf("unexpected session type: %v", s.Type) } if s.Target != target { return fmt.Errorf("target mismatch: %s", s.Target) } _, _ = s.Conn.Write([]byte("ok")) return nil }) defer stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() clientCfg := *serverCfg clientCfg.ServerAddress = addr clientCfg.HTTPMaskHost = "example.com" tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil) if err != nil { t.Fatalf("dial tunnel: %v", err) } defer tunnelConn.Close() handshakeCfg := clientCfg handshakeCfg.DisableHTTPMask = true cConn, err := ClientHandshake(tunnelConn, &handshakeCfg) if err != nil { t.Fatalf("client handshake: %v", err) } defer cConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode addr: %v", err) } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write addr: %v", err) } buf := make([]byte, 2) if _, err := io.ReadFull(cConn, buf); err != nil { t.Fatalf("read: %v", err) } if string(buf) != "ok" { t.Fatalf("unexpected payload: %q", buf) } stop() for err := range errCh { t.Fatalf("server error: %v", err) } } func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) { key := "tunnel-poll-key" target := "8.8.8.8:53" payload := []byte{0xaa, 0xbb, 0xcc, 0xdd} serverCfg := newTunnelTestTable(t, key) serverCfg.HTTPMaskMode = "poll" addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { if s.Type != SessionTypeUoT { return fmt.Errorf("unexpected session type: %v", s.Type) } gotAddr, gotPayload, err := ReadDatagram(s.Conn) if err != nil { return fmt.Errorf("server read datagram: %w", err) } if gotAddr != target { return fmt.Errorf("uot target mismatch: %s", gotAddr) } if !bytes.Equal(gotPayload, payload) { return fmt.Errorf("uot payload mismatch: %x", gotPayload) } if err := WriteDatagram(s.Conn, gotAddr, gotPayload); err != nil { return fmt.Errorf("server write datagram: %w", err) } return nil }) defer stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() clientCfg := *serverCfg clientCfg.ServerAddress = addr tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil) if err != nil { t.Fatalf("dial tunnel: %v", err) } defer tunnelConn.Close() handshakeCfg := clientCfg handshakeCfg.DisableHTTPMask = true cConn, err := ClientHandshake(tunnelConn, &handshakeCfg) if err != nil { t.Fatalf("client handshake: %v", err) } defer cConn.Close() if err := WriteKIPMessage(cConn, KIPTypeStartUoT, nil); err != nil { t.Fatalf("start uot: %v", err) } if err := WriteDatagram(cConn, target, payload); err != nil { t.Fatalf("write datagram: %v", err) } gotAddr, gotPayload, err := ReadDatagram(cConn) if err != nil { t.Fatalf("read datagram: %v", err) } if gotAddr != target { t.Fatalf("uot target mismatch: %s", gotAddr) } if !bytes.Equal(gotPayload, payload) { t.Fatalf("uot payload mismatch: %x", gotPayload) } stop() for err := range errCh { t.Fatalf("server error: %v", err) } } func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) { key := "tunnel-auto-key" target := "9.9.9.9:443" serverCfg := newTunnelTestTable(t, key) serverCfg.HTTPMaskMode = "auto" addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { if s.Type != SessionTypeTCP { return fmt.Errorf("unexpected session type: %v", s.Type) } if s.Target != target { return fmt.Errorf("target mismatch: %s", s.Target) } _, _ = s.Conn.Write([]byte("ok")) return nil }) defer stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() clientCfg := *serverCfg clientCfg.ServerAddress = addr tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil) if err != nil { t.Fatalf("dial tunnel: %v", err) } defer tunnelConn.Close() handshakeCfg := clientCfg handshakeCfg.DisableHTTPMask = true cConn, err := ClientHandshake(tunnelConn, &handshakeCfg) if err != nil { t.Fatalf("client handshake: %v", err) } defer cConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode addr: %v", err) } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write addr: %v", err) } buf := make([]byte, 2) if _, err := io.ReadFull(cConn, buf); err != nil { t.Fatalf("read: %v", err) } if string(buf) != "ok" { t.Fatalf("unexpected payload: %q", buf) } stop() for err := range errCh { t.Fatalf("server error: %v", err) } } func TestHTTPMaskTunnel_WS_TCPRoundTrip(t *testing.T) { key := "tunnel-ws-key" target := "1.1.1.1:80" serverCfg := newTunnelTestTable(t, key) serverCfg.HTTPMaskMode = "ws" addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { if s.Type != SessionTypeTCP { return fmt.Errorf("unexpected session type: %v", s.Type) } if s.Target != target { return fmt.Errorf("target mismatch: %s", s.Target) } _, _ = s.Conn.Write([]byte("ok")) return nil }) defer stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() clientCfg := *serverCfg clientCfg.ServerAddress = addr tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil) if err != nil { t.Fatalf("dial tunnel: %v", err) } defer tunnelConn.Close() handshakeCfg := clientCfg handshakeCfg.DisableHTTPMask = true cConn, err := ClientHandshake(tunnelConn, &handshakeCfg) if err != nil { t.Fatalf("client handshake: %v", err) } defer cConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode addr: %v", err) } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write addr: %v", err) } buf := make([]byte, 2) if _, err := io.ReadFull(cConn, buf); err != nil { t.Fatalf("read: %v", err) } if string(buf) != "ok" { t.Fatalf("unexpected payload: %q", buf) } stop() for err := range errCh { t.Fatalf("server error: %v", err) } } func TestHTTPMaskTunnel_EarlyHandshake_TCPRoundTrip(t *testing.T) { modes := []string{"stream", "poll", "ws"} for _, mode := range modes { t.Run(mode, func(t *testing.T) { key := "tunnel-early-" + mode target := "1.1.1.1:80" serverCfg := newTunnelTestTable(t, key) serverCfg.HTTPMaskMode = mode addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { if s.Type != SessionTypeTCP { return fmt.Errorf("unexpected session type: %v", s.Type) } if s.Target != target { return fmt.Errorf("target mismatch: %s", s.Target) } _, _ = s.Conn.Write([]byte("ok")) return nil }) defer stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() clientCfg := *serverCfg clientCfg.ServerAddress = addr handshakeCfg := clientCfg handshakeCfg.DisableHTTPMask = true tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, func(raw net.Conn) (net.Conn, error) { return ClientHandshake(raw, &handshakeCfg) }) if err != nil { t.Fatalf("dial tunnel: %v", err) } defer tunnelConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode addr: %v", err) } if err := WriteKIPMessage(tunnelConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write addr: %v", err) } buf := make([]byte, 2) if _, err := io.ReadFull(tunnelConn, buf); err != nil { t.Fatalf("read: %v", err) } if string(buf) != "ok" { t.Fatalf("unexpected payload: %q", buf) } stop() for err := range errCh { t.Fatalf("server error: %v", err) } }) } } func TestHTTPMaskTunnel_EarlyHandshake_AutoPathRoot_TCPRoundTrip(t *testing.T) { key := "tunnel-early-auto-pathroot" target := "1.1.1.1:80" serverCfg := newTunnelTestTable(t, key) serverCfg.HTTPMaskMode = "auto" serverCfg.HTTPMaskPathRoot = "httpmaskpath" addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { if s.Type != SessionTypeTCP { return fmt.Errorf("unexpected session type: %v", s.Type) } if s.Target != target { return fmt.Errorf("target mismatch: %s", s.Target) } _, _ = s.Conn.Write([]byte("ok")) return nil }) defer stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() clientCfg := *serverCfg clientCfg.ServerAddress = addr handshakeCfg := clientCfg handshakeCfg.DisableHTTPMask = true tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, func(raw net.Conn) (net.Conn, error) { return ClientHandshake(raw, &handshakeCfg) }) if err != nil { t.Fatalf("dial tunnel: %v", err) } defer tunnelConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { t.Fatalf("encode addr: %v", err) } if err := WriteKIPMessage(tunnelConn, KIPTypeOpenTCP, addrBuf); err != nil { t.Fatalf("write addr: %v", err) } buf := make([]byte, 2) if _, err := io.ReadFull(tunnelConn, buf); err != nil { t.Fatalf("read: %v", err) } if string(buf) != "ok" { t.Fatalf("unexpected payload: %q", buf) } stop() for err := range errCh { t.Fatalf("server error: %v", err) } } func TestHTTPMaskTunnel_Validation(t *testing.T) { cfg := DefaultConfig() cfg.Key = "k" cfg.Table = NewTable("seed", "prefer_ascii") cfg.ServerAddress = "127.0.0.1:1" cfg.DisableHTTPMask = true cfg.HTTPMaskMode = "stream" if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil { t.Fatalf("expected error for disabled http mask") } cfg.DisableHTTPMask = false cfg.HTTPMaskMode = "legacy" if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil { t.Fatalf("expected error for legacy mode") } } func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) { key := "tunnel-soak-key" target := "1.0.0.1:80" serverCfg := newTunnelTestTable(t, key) serverCfg.HTTPMaskMode = "stream" serverCfg.EnablePureDownlink = false const ( sessions = 8 payloadLen = 64 * 1024 ) addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { if s.Type != SessionTypeTCP { return fmt.Errorf("unexpected session type: %v", s.Type) } if s.Target != target { return fmt.Errorf("target mismatch: %s", s.Target) } buf := make([]byte, payloadLen) if _, err := io.ReadFull(s.Conn, buf); err != nil { return fmt.Errorf("server read payload: %w", err) } _, err := s.Conn.Write(buf) return err }) defer stop() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var wg sync.WaitGroup runErr := make(chan error, sessions) for i := 0; i < sessions; i++ { wg.Add(1) go func(id int) { defer wg.Done() clientCfg := *serverCfg clientCfg.ServerAddress = addr clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost) tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil) if err != nil { runErr <- fmt.Errorf("dial: %w", err) return } defer tunnelConn.Close() handshakeCfg := clientCfg handshakeCfg.DisableHTTPMask = true cConn, err := ClientHandshake(tunnelConn, &handshakeCfg) if err != nil { runErr <- fmt.Errorf("handshake: %w", err) return } defer cConn.Close() addrBuf, err := EncodeAddress(target) if err != nil { runErr <- fmt.Errorf("encode addr: %w", err) return } if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil { runErr <- fmt.Errorf("write addr: %w", err) return } payload := bytes.Repeat([]byte{byte(id)}, payloadLen) if _, err := cConn.Write(payload); err != nil { runErr <- fmt.Errorf("write payload: %w", err) return } echo := make([]byte, payloadLen) if _, err := io.ReadFull(cConn, echo); err != nil { runErr <- fmt.Errorf("read echo: %w", err) return } if !bytes.Equal(echo, payload) { runErr <- fmt.Errorf("echo mismatch") return } runErr <- nil }(i) } wg.Wait() close(runErr) for err := range runErr { if err != nil { t.Fatalf("soak: %v", err) } } stop() for err := range errCh { t.Fatalf("server error: %v", err) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/init.go ================================================ package sudoku import ( "encoding/hex" "fmt" "strings" "github.com/metacubex/edwards25519" "github.com/metacubex/mihomo/transport/sudoku/crypto" "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) func NewTable(key string, tableType string) *sudoku.Table { table, err := NewTableWithCustom(key, tableType, "") if err != nil { panic(fmt.Sprintf("[Sudoku] failed to init tables: %v", err)) } return table } func NewTableWithCustom(key string, tableType string, customTable string) (*sudoku.Table, error) { table, err := sudoku.NewTableWithCustom(key, tableType, customTable) if err != nil { return nil, err } return table, nil } // ClientAEADSeed returns a canonical "seed" that is stable between client private key material and server public key. func ClientAEADSeed(key string) string { key = strings.TrimSpace(key) if key == "" { return "" } b, err := hex.DecodeString(key) if err != nil { return key } // Client-side key material can be: // - public key: 32 bytes hex compressed point // - split private key: 64 bytes hex (r||k) // - master private scalar: 32 bytes hex (x) // - PSK string: non-hex // // 32-byte hex is ambiguous: it can be either a compressed public key or a // master private scalar. Official Sudoku runtime accepts public keys directly, // so when the bytes already decode as a point, preserve that point verbatim. if len(b) == 32 { if p, err := new(edwards25519.Point).SetBytes(b); err == nil { return hex.EncodeToString(p.Bytes()) } } if len(b) != 64 && len(b) != 32 { return key } if recovered, err := crypto.RecoverPublicKey(key); err == nil { return crypto.EncodePoint(recovered) } return key } // ServerAEADSeed returns a canonical seed for server-side configuration. // // When key is a public key (32-byte compressed point, hex), it returns the canonical point encoding. // When key is private key material (split/master scalar), it derives and returns the public key. func ServerAEADSeed(key string) string { key = strings.TrimSpace(key) if key == "" { return "" } b, err := hex.DecodeString(key) if err != nil { return key } // Prefer interpreting 32-byte hex as a public key point, to avoid accidental scalar parsing. if len(b) == 32 { if p, err := new(edwards25519.Point).SetBytes(b); err == nil { return hex.EncodeToString(p.Bytes()) } } // Fall back to client-side rules for private key materials / other formats. return ClientAEADSeed(key) } // GenKeyPair generates a client "available private key" and the corresponding server public key. func GenKeyPair() (privateKey, publicKey string, err error) { pair, err := crypto.GenerateMasterKey() if err != nil { return "", "", err } availablePrivateKey, err := crypto.SplitPrivateKey(pair.Private) if err != nil { return "", "", err } return availablePrivateKey, crypto.EncodePoint(pair.Public), nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/init_test.go ================================================ package sudoku import ( "crypto/rand" "encoding/hex" "testing" "github.com/metacubex/edwards25519" "github.com/stretchr/testify/require" ) func TestClientAEADSeed_IsStableForPrivAndPub(t *testing.T) { for i := 0; i < 64; i++ { priv, pub, err := GenKeyPair() require.NoError(t, err) require.Equal(t, pub, ClientAEADSeed(priv)) require.Equal(t, pub, ClientAEADSeed(pub)) require.Equal(t, pub, ServerAEADSeed(pub)) require.Equal(t, pub, ServerAEADSeed(priv)) } } func TestClientAEADSeed_Supports32ByteMasterScalar(t *testing.T) { for i := 0; i < 256; i++ { var seed [64]byte _, err := rand.Read(seed[:]) require.NoError(t, err) s, err := edwards25519.NewScalar().SetUniformBytes(seed[:]) require.NoError(t, err) keyHex := hex.EncodeToString(s.Bytes()) require.Len(t, keyHex, 64) // 32-byte hex is ambiguous: it can be either a master scalar or an // already-compressed public key. Public-key encoding wins when both parse. if _, err := new(edwards25519.Point).SetBytes(s.Bytes()); err == nil { continue } require.NotEqual(t, keyHex, ClientAEADSeed(keyHex)) require.Equal(t, ClientAEADSeed(keyHex), ServerAEADSeed(ClientAEADSeed(keyHex))) return } t.Fatal("failed to generate an unambiguous 32-byte master scalar") } func TestServerAEADSeed_LeavesPublicKeyAsIs(t *testing.T) { for i := 0; i < 64; i++ { priv, pub, err := GenKeyPair() require.NoError(t, err) require.Equal(t, pub, ServerAEADSeed(pub)) require.Equal(t, pub, ServerAEADSeed(priv)) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/kip.go ================================================ package sudoku import ( "bytes" "crypto/sha256" "encoding/binary" "encoding/hex" "errors" "fmt" "io" "strings" "time" sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) const ( kipMagic = "kip" KIPTypeClientHello byte = 0x01 KIPTypeServerHello byte = 0x02 KIPTypeOpenTCP byte = 0x10 KIPTypeStartMux byte = 0x11 KIPTypeStartUoT byte = 0x12 KIPTypeKeepAlive byte = 0x14 ) // KIP feature bits are advisory capability flags negotiated during the handshake. // They represent control-plane message families. const ( KIPFeatOpenTCP uint32 = 1 << 0 KIPFeatMux uint32 = 1 << 1 KIPFeatUoT uint32 = 1 << 2 KIPFeatKeepAlive uint32 = 1 << 4 KIPFeatAll = KIPFeatOpenTCP | KIPFeatMux | KIPFeatUoT | KIPFeatKeepAlive ) const ( kipHelloUserHashSize = 8 kipHelloNonceSize = 16 kipHelloPubSize = 32 kipMaxPayload = 64 * 1024 ) const kipClientHelloTableHintSize = 4 var errKIP = errors.New("kip protocol error") type KIPMessage struct { Type byte Payload []byte } func WriteKIPMessage(w io.Writer, typ byte, payload []byte) error { if w == nil { return fmt.Errorf("%w: nil writer", errKIP) } if len(payload) > kipMaxPayload { return fmt.Errorf("%w: payload too large: %d", errKIP, len(payload)) } var hdr [3 + 1 + 2]byte copy(hdr[:3], []byte(kipMagic)) hdr[3] = typ binary.BigEndian.PutUint16(hdr[4:], uint16(len(payload))) return writeAllChunks(w, hdr[:], payload) } func ReadKIPMessage(r io.Reader) (*KIPMessage, error) { if r == nil { return nil, fmt.Errorf("%w: nil reader", errKIP) } var hdr [3 + 1 + 2]byte if _, err := io.ReadFull(r, hdr[:]); err != nil { return nil, err } if string(hdr[:3]) != kipMagic { return nil, fmt.Errorf("%w: bad magic", errKIP) } typ := hdr[3] n := int(binary.BigEndian.Uint16(hdr[4:])) if n < 0 || n > kipMaxPayload { return nil, fmt.Errorf("%w: invalid payload length: %d", errKIP, n) } var payload []byte if n > 0 { payload = make([]byte, n) if _, err := io.ReadFull(r, payload); err != nil { return nil, err } } return &KIPMessage{Type: typ, Payload: payload}, nil } type KIPClientHello struct { Timestamp time.Time UserHash [kipHelloUserHashSize]byte Nonce [kipHelloNonceSize]byte ClientPub [kipHelloPubSize]byte Features uint32 TableHint uint32 HasTableHint bool } type KIPServerHello struct { Nonce [kipHelloNonceSize]byte ServerPub [kipHelloPubSize]byte SelectedFeats uint32 } func newKIPClientHello(userHash [kipHelloUserHashSize]byte, nonce [kipHelloNonceSize]byte, clientPub [kipHelloPubSize]byte, feats uint32, tableHint uint32, hasTableHint bool) *KIPClientHello { return &KIPClientHello{ Timestamp: time.Now(), UserHash: userHash, Nonce: nonce, ClientPub: clientPub, Features: feats, TableHint: tableHint, HasTableHint: hasTableHint, } } func kipUserHashFromKey(psk string) [kipHelloUserHashSize]byte { var out [kipHelloUserHashSize]byte psk = strings.TrimSpace(psk) if psk == "" { return out } // Align with upstream: when the client carries private key material (or even just a public key), // prefer hashing the raw hex bytes so different split/master keys can be distinguished. if keyBytes, err := hex.DecodeString(psk); err == nil && len(keyBytes) > 0 { sum := sha256.Sum256(keyBytes) copy(out[:], sum[:kipHelloUserHashSize]) return out } sum := sha256.Sum256([]byte(psk)) copy(out[:], sum[:kipHelloUserHashSize]) return out } func KIPUserHashHexFromKey(psk string) string { uh := kipUserHashFromKey(psk) return hex.EncodeToString(uh[:]) } func (m *KIPClientHello) EncodePayload() []byte { var b bytes.Buffer var tmp [8]byte binary.BigEndian.PutUint64(tmp[:], uint64(m.Timestamp.Unix())) b.Write(tmp[:]) b.Write(m.UserHash[:]) b.Write(m.Nonce[:]) b.Write(m.ClientPub[:]) var f [4]byte binary.BigEndian.PutUint32(f[:], m.Features) b.Write(f[:]) if m.HasTableHint { var hint [kipClientHelloTableHintSize]byte binary.BigEndian.PutUint32(hint[:], m.TableHint) b.Write(hint[:]) } return b.Bytes() } func DecodeKIPClientHelloPayload(payload []byte) (*KIPClientHello, error) { const minLen = 8 + kipHelloUserHashSize + kipHelloNonceSize + kipHelloPubSize + 4 if len(payload) < minLen { return nil, fmt.Errorf("%w: client hello too short", errKIP) } var h KIPClientHello ts := int64(binary.BigEndian.Uint64(payload[:8])) h.Timestamp = time.Unix(ts, 0) off := 8 copy(h.UserHash[:], payload[off:off+kipHelloUserHashSize]) off += kipHelloUserHashSize copy(h.Nonce[:], payload[off:off+kipHelloNonceSize]) off += kipHelloNonceSize copy(h.ClientPub[:], payload[off:off+kipHelloPubSize]) off += kipHelloPubSize h.Features = binary.BigEndian.Uint32(payload[off : off+4]) off += 4 if len(payload) >= off+kipClientHelloTableHintSize { h.TableHint = binary.BigEndian.Uint32(payload[off : off+kipClientHelloTableHintSize]) h.HasTableHint = true } return &h, nil } func ResolveClientHelloTable(selected *sudokuobfs.Table, candidates []*sudokuobfs.Table, hello *KIPClientHello) (*sudokuobfs.Table, error) { if selected == nil { return nil, fmt.Errorf("nil selected table") } if hello == nil || !hello.HasTableHint { return selected, nil } if selected.Hint() == hello.TableHint { return selected, nil } if len(candidates) == 0 { return nil, fmt.Errorf("no table candidates") } var hinted *sudokuobfs.Table for _, candidate := range candidates { if candidate == nil || candidate.Hint() != hello.TableHint { continue } hinted = candidate break } if hinted == nil { return nil, fmt.Errorf("unknown table hint: %d", hello.TableHint) } if hinted != selected && (!hinted.IsASCII || !selected.IsASCII) { return nil, fmt.Errorf("table hint %d mismatches probed uplink table", hello.TableHint) } return hinted, nil } func (m *KIPServerHello) EncodePayload() []byte { var b bytes.Buffer b.Write(m.Nonce[:]) b.Write(m.ServerPub[:]) var f [4]byte binary.BigEndian.PutUint32(f[:], m.SelectedFeats) b.Write(f[:]) return b.Bytes() } func DecodeKIPServerHelloPayload(payload []byte) (*KIPServerHello, error) { const want = kipHelloNonceSize + kipHelloPubSize + 4 if len(payload) != want { return nil, fmt.Errorf("%w: server hello bad len: %d", errKIP, len(payload)) } var h KIPServerHello off := 0 copy(h.Nonce[:], payload[off:off+kipHelloNonceSize]) off += kipHelloNonceSize copy(h.ServerPub[:], payload[off:off+kipHelloPubSize]) off += kipHelloPubSize h.SelectedFeats = binary.BigEndian.Uint32(payload[off : off+4]) return &h, nil } func writeFull(w io.Writer, b []byte) error { for len(b) > 0 { n, err := w.Write(b) if err != nil { return err } b = b[n:] } return nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/kip_test.go ================================================ package sudoku import ( "testing" sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) func TestKIPClientHelloTableHintRoundTrip(t *testing.T) { hello := &KIPClientHello{ Features: KIPFeatAll, TableHint: 0x12345678, HasTableHint: true, } decoded, err := DecodeKIPClientHelloPayload(hello.EncodePayload()) if err != nil { t.Fatalf("decode client hello: %v", err) } if !decoded.HasTableHint { t.Fatalf("expected decoded hello to carry table hint") } if decoded.TableHint != hello.TableHint { t.Fatalf("decoded table hint = %08x, want %08x", decoded.TableHint, hello.TableHint) } } func TestResolveClientHelloTableAllowsDirectionalASCIIRotation(t *testing.T) { tables, err := NewClientTablesWithCustomPatterns("seed", "up_ascii_down_entropy", "", []string{"xpxvvpvv", "vxpvxvvp"}) if err != nil { t.Fatalf("build tables: %v", err) } if len(tables) != 2 { t.Fatalf("expected 2 tables, got %d", len(tables)) } selected, err := ResolveClientHelloTable(tables[0], tables, &KIPClientHello{ TableHint: tables[1].Hint(), HasTableHint: true, }) if err != nil { t.Fatalf("resolve client hello table: %v", err) } if selected != tables[1] { t.Fatalf("resolved table mismatch") } } func TestResolveClientHelloTableRejectsEntropyMismatch(t *testing.T) { a, err := sudokuobfs.NewTableWithCustom("seed", "prefer_entropy", "xpxvvpvv") if err != nil { t.Fatalf("table a: %v", err) } b, err := sudokuobfs.NewTableWithCustom("seed", "prefer_entropy", "vxpvxvvp") if err != nil { t.Fatalf("table b: %v", err) } if _, err := ResolveClientHelloTable(a, []*sudokuobfs.Table{a, b}, &KIPClientHello{ TableHint: b.Hint(), HasTableHint: true, }); err == nil { t.Fatalf("expected entropy-table mismatch to fail") } } ================================================ FILE: core/Clash.Meta/transport/sudoku/multiplex/session.go ================================================ package multiplex import ( "encoding/binary" "errors" "fmt" "io" "net" "sync" "time" ) const ( frameOpen byte = 0x01 frameData byte = 0x02 frameClose byte = 0x03 frameReset byte = 0x04 ) const ( headerSize = 1 + 4 + 4 maxFrameSize = 256 * 1024 maxDataPayload = 32 * 1024 ) type acceptEvent struct { stream *stream payload []byte } type Session struct { conn net.Conn writeMu sync.Mutex streamsMu sync.Mutex streams map[uint32]*stream nextID uint32 acceptCh chan acceptEvent closed chan struct{} closeOnce sync.Once closeErr error } func NewClientSession(conn net.Conn) (*Session, error) { if conn == nil { return nil, fmt.Errorf("nil conn") } s := &Session{ conn: conn, streams: make(map[uint32]*stream), closed: make(chan struct{}), } go s.readLoop() return s, nil } func NewServerSession(conn net.Conn) (*Session, error) { if conn == nil { return nil, fmt.Errorf("nil conn") } s := &Session{ conn: conn, streams: make(map[uint32]*stream), acceptCh: make(chan acceptEvent, 256), closed: make(chan struct{}), } go s.readLoop() return s, nil } func (s *Session) IsClosed() bool { if s == nil { return true } select { case <-s.closed: return true default: return false } } func (s *Session) closedErr() error { s.streamsMu.Lock() err := s.closeErr s.streamsMu.Unlock() if err == nil { return io.ErrClosedPipe } return err } func (s *Session) closeWithError(err error) { if err == nil { err = io.ErrClosedPipe } s.closeOnce.Do(func() { s.streamsMu.Lock() if s.closeErr == nil { s.closeErr = err } streams := make([]*stream, 0, len(s.streams)) for _, st := range s.streams { streams = append(streams, st) } s.streams = make(map[uint32]*stream) s.streamsMu.Unlock() for _, st := range streams { st.closeNoSend(err) } close(s.closed) _ = s.conn.Close() }) } func (s *Session) Close() error { if s == nil { return nil } s.closeWithError(io.ErrClosedPipe) return nil } func (s *Session) registerStream(st *stream) { s.streamsMu.Lock() s.streams[st.id] = st s.streamsMu.Unlock() } func (s *Session) getStream(id uint32) *stream { s.streamsMu.Lock() st := s.streams[id] s.streamsMu.Unlock() return st } func (s *Session) removeStream(id uint32) { s.streamsMu.Lock() delete(s.streams, id) s.streamsMu.Unlock() } func (s *Session) nextStreamID() uint32 { s.streamsMu.Lock() s.nextID++ id := s.nextID if id == 0 { s.nextID++ id = s.nextID } s.streamsMu.Unlock() return id } func (s *Session) sendFrame(frameType byte, streamID uint32, payload []byte) error { if s.IsClosed() { return s.closedErr() } if len(payload) > maxFrameSize { return fmt.Errorf("mux payload too large: %d", len(payload)) } var header [headerSize]byte header[0] = frameType binary.BigEndian.PutUint32(header[1:5], streamID) binary.BigEndian.PutUint32(header[5:9], uint32(len(payload))) s.writeMu.Lock() defer s.writeMu.Unlock() if err := writeAllChunks(s.conn, header[:], payload); err != nil { s.closeWithError(err) return err } return nil } func (s *Session) sendReset(streamID uint32, msg string) { if msg == "" { msg = "reset" } _ = s.sendFrame(frameReset, streamID, []byte(msg)) _ = s.sendFrame(frameClose, streamID, nil) } func (s *Session) OpenStream(openPayload []byte) (net.Conn, error) { if s == nil { return nil, fmt.Errorf("nil session") } if s.IsClosed() { return nil, s.closedErr() } streamID := s.nextStreamID() st := newStream(s, streamID) s.registerStream(st) if err := s.sendFrame(frameOpen, streamID, openPayload); err != nil { st.closeNoSend(err) s.removeStream(streamID) return nil, fmt.Errorf("mux open failed: %w", err) } return st, nil } func (s *Session) AcceptStream() (net.Conn, []byte, error) { if s == nil { return nil, nil, fmt.Errorf("nil session") } if s.acceptCh == nil { return nil, nil, fmt.Errorf("accept is not supported on client sessions") } select { case ev := <-s.acceptCh: return ev.stream, ev.payload, nil case <-s.closed: return nil, nil, s.closedErr() } } func (s *Session) readLoop() { var header [headerSize]byte for { if _, err := io.ReadFull(s.conn, header[:]); err != nil { s.closeWithError(err) return } frameType := header[0] streamID := binary.BigEndian.Uint32(header[1:5]) n := int(binary.BigEndian.Uint32(header[5:9])) if n < 0 || n > maxFrameSize { s.closeWithError(fmt.Errorf("invalid mux frame length: %d", n)) return } var payload []byte if n > 0 { payload = make([]byte, n) if _, err := io.ReadFull(s.conn, payload); err != nil { s.closeWithError(err) return } } switch frameType { case frameOpen: if s.acceptCh == nil { s.sendReset(streamID, "unexpected open") continue } if streamID == 0 { s.sendReset(streamID, "invalid stream id") continue } if existing := s.getStream(streamID); existing != nil { s.sendReset(streamID, "stream already exists") continue } st := newStream(s, streamID) s.registerStream(st) go func() { select { case s.acceptCh <- acceptEvent{stream: st, payload: payload}: case <-s.closed: st.closeNoSend(io.ErrClosedPipe) s.removeStream(streamID) } }() case frameData: st := s.getStream(streamID) if st == nil { continue } if len(payload) == 0 { continue } st.enqueue(payload) case frameClose: st := s.getStream(streamID) if st == nil { continue } st.closeNoSend(io.EOF) s.removeStream(streamID) case frameReset: st := s.getStream(streamID) if st == nil { continue } msg := trimASCII(payload) if msg == "" { msg = "reset" } st.closeNoSend(errors.New(msg)) s.removeStream(streamID) default: s.closeWithError(fmt.Errorf("unknown mux frame type: %d", frameType)) return } } } func trimASCII(b []byte) string { i := 0 j := len(b) for i < j { c := b[i] if c != ' ' && c != '\n' && c != '\r' && c != '\t' { break } i++ } for j > i { c := b[j-1] if c != ' ' && c != '\n' && c != '\r' && c != '\t' { break } j-- } if i >= j { return "" } out := make([]byte, j-i) copy(out, b[i:j]) return string(out) } type stream struct { session *Session id uint32 mu sync.Mutex cond *sync.Cond closed bool closeErr error readBuf []byte queue [][]byte localAddr net.Addr remoteAddr net.Addr } func newStream(session *Session, id uint32) *stream { st := &stream{ session: session, id: id, localAddr: &net.TCPAddr{}, remoteAddr: &net.TCPAddr{}, } st.cond = sync.NewCond(&st.mu) return st } func (c *stream) enqueue(payload []byte) { c.mu.Lock() if c.closed { c.mu.Unlock() return } if len(c.readBuf) == 0 && len(c.queue) == 0 { c.readBuf = payload } else { c.queue = append(c.queue, payload) } c.cond.Signal() c.mu.Unlock() } func (c *stream) closeNoSend(err error) { if err == nil { err = io.EOF } c.mu.Lock() if c.closed { c.mu.Unlock() return } c.closed = true if c.closeErr == nil { c.closeErr = err } c.cond.Broadcast() c.mu.Unlock() } func (c *stream) closedErr() error { c.mu.Lock() defer c.mu.Unlock() if c.closeErr == nil { return io.ErrClosedPipe } return c.closeErr } func (c *stream) Read(p []byte) (int, error) { if len(p) == 0 { return 0, nil } c.mu.Lock() defer c.mu.Unlock() for len(c.readBuf) == 0 && len(c.queue) == 0 && !c.closed { c.cond.Wait() } if len(c.readBuf) == 0 && len(c.queue) > 0 { c.readBuf = c.queue[0] c.queue = c.queue[1:] } if len(c.readBuf) == 0 && c.closed { if c.closeErr == nil { return 0, io.ErrClosedPipe } return 0, c.closeErr } n := copy(p, c.readBuf) c.readBuf = c.readBuf[n:] return n, nil } func (c *stream) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } if c.session == nil || c.session.IsClosed() { if c.session != nil { return 0, c.session.closedErr() } return 0, io.ErrClosedPipe } c.mu.Lock() closed := c.closed c.mu.Unlock() if closed { return 0, c.closedErr() } written := 0 for len(p) > 0 { chunk := p if len(chunk) > maxDataPayload { chunk = p[:maxDataPayload] } if err := c.session.sendFrame(frameData, c.id, chunk); err != nil { return written, err } written += len(chunk) p = p[len(chunk):] } return written, nil } func (c *stream) Close() error { c.mu.Lock() if c.closed { c.mu.Unlock() return nil } c.closed = true if c.closeErr == nil { c.closeErr = io.ErrClosedPipe } c.cond.Broadcast() c.mu.Unlock() _ = c.session.sendFrame(frameClose, c.id, nil) c.session.removeStream(c.id) return nil } func (c *stream) CloseWrite() error { return c.Close() } func (c *stream) CloseRead() error { return c.Close() } func (c *stream) LocalAddr() net.Addr { return c.localAddr } func (c *stream) RemoteAddr() net.Addr { return c.remoteAddr } func (c *stream) SetDeadline(t time.Time) error { _ = c.SetReadDeadline(t) _ = c.SetWriteDeadline(t) return nil } func (c *stream) SetReadDeadline(time.Time) error { return nil } func (c *stream) SetWriteDeadline(time.Time) error { return nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/multiplex/write_chunks.go ================================================ package multiplex import "io" func writeAllChunks(w io.Writer, chunks ...[]byte) error { for _, chunk := range chunks { for len(chunk) > 0 { n, err := w.Write(chunk) if err != nil { return err } if n == 0 { return io.ErrShortWrite } chunk = chunk[n:] } } return nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/multiplex.go ================================================ package sudoku import ( "bytes" "context" "fmt" "net" "strings" "github.com/metacubex/mihomo/transport/sudoku/multiplex" ) // StartMultiplexClient upgrades an already-handshaked Sudoku tunnel into a multiplex session. func StartMultiplexClient(conn net.Conn) (*MultiplexClient, error) { if conn == nil { return nil, fmt.Errorf("nil conn") } if err := WriteKIPMessage(conn, KIPTypeStartMux, nil); err != nil { return nil, fmt.Errorf("write mux start failed: %w", err) } sess, err := multiplex.NewClientSession(conn) if err != nil { return nil, fmt.Errorf("start multiplex session failed: %w", err) } return &MultiplexClient{sess: sess}, nil } type MultiplexClient struct { sess *multiplex.Session } // Dial opens a new logical stream, writes the target address, and returns the stream as net.Conn. func (c *MultiplexClient) Dial(ctx context.Context, targetAddress string) (net.Conn, error) { if c == nil || c.sess == nil || c.sess.IsClosed() { return nil, fmt.Errorf("multiplex session is closed") } if strings.TrimSpace(targetAddress) == "" { return nil, fmt.Errorf("target address cannot be empty") } addrBuf, err := EncodeAddress(targetAddress) if err != nil { return nil, fmt.Errorf("encode target address failed: %w", err) } if ctx != nil && ctx.Err() != nil { return nil, ctx.Err() } stream, err := c.sess.OpenStream(addrBuf) if err != nil { return nil, err } return stream, nil } func (c *MultiplexClient) Close() error { if c == nil || c.sess == nil { return nil } return c.sess.Close() } func (c *MultiplexClient) IsClosed() bool { if c == nil || c.sess == nil { return true } return c.sess.IsClosed() } // AcceptMultiplexServer upgrades a server-side, already-handshaked Sudoku connection into a multiplex session. func AcceptMultiplexServer(conn net.Conn) (*MultiplexServer, error) { if conn == nil { return nil, fmt.Errorf("nil conn") } sess, err := multiplex.NewServerSession(conn) if err != nil { return nil, err } return &MultiplexServer{sess: sess}, nil } // MultiplexServer wraps a multiplex session created from a handshaked Sudoku tunnel connection. type MultiplexServer struct { sess *multiplex.Session } func (s *MultiplexServer) AcceptStream() (net.Conn, error) { if s == nil || s.sess == nil { return nil, fmt.Errorf("nil session") } c, _, err := s.sess.AcceptStream() return c, err } // AcceptTCP accepts a multiplex stream and returns the target address declared in the open frame. func (s *MultiplexServer) AcceptTCP() (net.Conn, string, error) { if s == nil || s.sess == nil { return nil, "", fmt.Errorf("nil session") } stream, payload, err := s.sess.AcceptStream() if err != nil { return nil, "", err } target, err := DecodeAddress(bytes.NewReader(payload)) if err != nil { _ = stream.Close() return nil, "", err } return stream, target, nil } func (s *MultiplexServer) Close() error { if s == nil || s.sess == nil { return nil } return s.sess.Close() } func (s *MultiplexServer) IsClosed() bool { if s == nil || s.sess == nil { return true } return s.sess.IsClosed() } ================================================ FILE: core/Clash.Meta/transport/sudoku/multiplex_test.go ================================================ package sudoku import ( "bytes" "context" "io" "net" "sync/atomic" "testing" "time" sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) func TestUserHash_StableAcrossTableRotation(t *testing.T) { tables := []*sudokuobfs.Table{ sudokuobfs.NewTable("seed-a", "prefer_ascii"), sudokuobfs.NewTable("seed-b", "prefer_ascii"), } key := "userhash-stability-key" serverCfg := DefaultConfig() serverCfg.Key = key serverCfg.AEADMethod = "chacha20-poly1305" serverCfg.Tables = tables serverCfg.PaddingMin = 0 serverCfg.PaddingMax = 0 serverCfg.EnablePureDownlink = true serverCfg.HandshakeTimeoutSeconds = 5 serverCfg.DisableHTTPMask = true ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } t.Cleanup(func() { _ = ln.Close() }) const attempts = 32 hashCh := make(chan string, attempts) errCh := make(chan error, attempts) go func() { for { c, err := ln.Accept() if err != nil { return } go func(conn net.Conn) { defer conn.Close() _, meta, err := ServerHandshake(conn, serverCfg) if err != nil { errCh <- err return } if meta == nil || meta.UserHash == "" { errCh <- io.ErrUnexpectedEOF return } hashCh <- meta.UserHash }(c) } }() clientCfg := DefaultConfig() *clientCfg = *serverCfg clientCfg.ServerAddress = ln.Addr().String() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() for i := 0; i < attempts; i++ { raw, err := (&net.Dialer{}).DialContext(ctx, "tcp", clientCfg.ServerAddress) if err != nil { t.Fatalf("dial %d: %v", i, err) } cConn, err := ClientHandshake(raw, clientCfg) if err != nil { _ = raw.Close() t.Fatalf("handshake %d: %v", i, err) } _ = cConn.Close() } unique := map[string]struct{}{} deadline := time.After(10 * time.Second) for i := 0; i < attempts; i++ { select { case err := <-errCh: t.Fatalf("server handshake error: %v", err) case h := <-hashCh: if h == "" { t.Fatalf("empty user hash") } if len(h) != 16 { t.Fatalf("unexpected user hash length: %d", len(h)) } unique[h] = struct{}{} case <-deadline: t.Fatalf("timeout waiting for server handshakes") } } if len(unique) != 1 { t.Fatalf("user hash should be stable across table rotation; got %d distinct values", len(unique)) } } func TestMultiplex_TCP_Echo(t *testing.T) { table := sudokuobfs.NewTable("seed", "prefer_ascii") key := "test-key-mux" target := "example.com:80" serverCfg := DefaultConfig() serverCfg.Key = key serverCfg.AEADMethod = "chacha20-poly1305" serverCfg.Table = table serverCfg.PaddingMin = 0 serverCfg.PaddingMax = 0 serverCfg.EnablePureDownlink = true serverCfg.HandshakeTimeoutSeconds = 5 serverCfg.DisableHTTPMask = true ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } t.Cleanup(func() { _ = ln.Close() }) var handshakes int64 var streams int64 done := make(chan struct{}) go func() { defer close(done) raw, err := ln.Accept() if err != nil { return } defer raw.Close() c, meta, err := ServerHandshake(raw, serverCfg) if err != nil { return } atomic.AddInt64(&handshakes, 1) session, err := ReadServerSession(c, meta) if err != nil { return } if session.Type != SessionTypeMultiplex { _ = c.Close() return } mux, err := AcceptMultiplexServer(c) if err != nil { return } defer mux.Close() for { stream, dst, err := mux.AcceptTCP() if err != nil { return } if dst != target { _ = stream.Close() return } atomic.AddInt64(&streams, 1) go func(c net.Conn) { defer c.Close() _, _ = io.Copy(c, c) }(stream) } }() clientCfg := DefaultConfig() *clientCfg = *serverCfg clientCfg.ServerAddress = ln.Addr().String() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() raw, err := (&net.Dialer{}).DialContext(ctx, "tcp", clientCfg.ServerAddress) if err != nil { t.Fatalf("dial: %v", err) } t.Cleanup(func() { _ = raw.Close() }) cConn, err := ClientHandshake(raw, clientCfg) if err != nil { t.Fatalf("client handshake: %v", err) } mux, err := StartMultiplexClient(cConn) if err != nil { _ = cConn.Close() t.Fatalf("start mux: %v", err) } defer mux.Close() for i := 0; i < 6; i++ { s, err := mux.Dial(ctx, target) if err != nil { t.Fatalf("dial stream %d: %v", i, err) } msg := []byte("hello-mux") if _, err := s.Write(msg); err != nil { _ = s.Close() t.Fatalf("write: %v", err) } buf := make([]byte, len(msg)) if _, err := io.ReadFull(s, buf); err != nil { _ = s.Close() t.Fatalf("read: %v", err) } _ = s.Close() if !bytes.Equal(buf, msg) { t.Fatalf("echo mismatch: got %q", buf) } } _ = mux.Close() select { case <-done: case <-time.After(5 * time.Second): t.Fatalf("server did not exit") } if got := atomic.LoadInt64(&handshakes); got != 1 { t.Fatalf("unexpected handshake count: %d", got) } if got := atomic.LoadInt64(&streams); got < 6 { t.Fatalf("unexpected stream count: %d", got) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/auth.go ================================================ package httpmask import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/binary" "strings" "time" "github.com/metacubex/http" ) const ( tunnelAuthHeaderKey = "Authorization" tunnelAuthHeaderPrefix = "Bearer " tunnelAuthQueryKey = "auth" ) type tunnelAuth struct { key [32]byte // derived HMAC key skew time.Duration } func newTunnelAuth(key string, skew time.Duration) *tunnelAuth { key = strings.TrimSpace(key) if key == "" { return nil } if skew <= 0 { skew = 60 * time.Second } // Domain separation: keep this HMAC key independent from other uses of cfg.Key. h := sha256.New() _, _ = h.Write([]byte("sudoku-httpmask-auth-v1:")) _, _ = h.Write([]byte(key)) var sum [32]byte h.Sum(sum[:0]) return &tunnelAuth{key: sum, skew: skew} } func (a *tunnelAuth) token(mode TunnelMode, method, path string, now time.Time) string { if a == nil { return "" } ts := now.Unix() sig := a.sign(mode, method, path, ts) var buf [8 + 16]byte binary.BigEndian.PutUint64(buf[:8], uint64(ts)) copy(buf[8:], sig[:]) return base64.RawURLEncoding.EncodeToString(buf[:]) } func (a *tunnelAuth) verify(headers map[string]string, mode TunnelMode, method, path string, now time.Time) bool { if a == nil { return true } if headers == nil { return false } return a.verifyValue(headers["authorization"], mode, method, path, now) } func (a *tunnelAuth) verifyValue(val string, mode TunnelMode, method, path string, now time.Time) bool { if a == nil { return true } val = strings.TrimSpace(val) if val == "" { return false } // Accept both "Bearer " and raw token forms (for forward proxies / CDNs that may normalize headers). if len(val) > len(tunnelAuthHeaderPrefix) && strings.EqualFold(val[:len(tunnelAuthHeaderPrefix)], tunnelAuthHeaderPrefix) { val = strings.TrimSpace(val[len(tunnelAuthHeaderPrefix):]) } if val == "" { return false } raw, err := base64.RawURLEncoding.DecodeString(val) if err != nil || len(raw) != 8+16 { return false } ts := int64(binary.BigEndian.Uint64(raw[:8])) nowTS := now.Unix() delta := nowTS - ts if delta < 0 { delta = -delta } if delta > int64(a.skew.Seconds()) { return false } want := a.sign(mode, method, path, ts) return subtle.ConstantTimeCompare(raw[8:], want[:]) == 1 } func (a *tunnelAuth) sign(mode TunnelMode, method, path string, ts int64) [16]byte { method = strings.ToUpper(strings.TrimSpace(method)) if method == "" { method = "GET" } path = strings.TrimSpace(path) var tsBuf [8]byte binary.BigEndian.PutUint64(tsBuf[:], uint64(ts)) mac := hmac.New(sha256.New, a.key[:]) _, _ = mac.Write([]byte(mode)) _, _ = mac.Write([]byte{0}) _, _ = mac.Write([]byte(method)) _, _ = mac.Write([]byte{0}) _, _ = mac.Write([]byte(path)) _, _ = mac.Write([]byte{0}) _, _ = mac.Write(tsBuf[:]) var full [32]byte mac.Sum(full[:0]) var out [16]byte copy(out[:], full[:16]) return out } type httpHeaderSetter = http.Header func applyTunnelAuthHeader(h httpHeaderSetter, auth *tunnelAuth, mode TunnelMode, method, path string) { if auth == nil || h == nil { return } token := auth.token(mode, method, path, time.Now()) if token == "" { return } h.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token) } func applyTunnelAuth(req *http.Request, auth *tunnelAuth, mode TunnelMode, method, path string) { if auth == nil || req == nil { return } token := auth.token(mode, method, path, time.Now()) if token == "" { return } req.Header.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token) if req.URL != nil { q := req.URL.Query() q.Set(tunnelAuthQueryKey, token) req.URL.RawQuery = q.Encode() } } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/early_handshake.go ================================================ package httpmask import ( "encoding/base64" "errors" "fmt" "net" "net/url" "strings" ) const ( tunnelEarlyDataQueryKey = "ed" tunnelEarlyDataHeader = "X-Sudoku-Early" ) type ClientEarlyHandshake struct { RequestPayload []byte HandleResponse func(payload []byte) error Ready func() bool WrapConn func(raw net.Conn) (net.Conn, error) } type TunnelServerEarlyHandshake struct { Prepare func(payload []byte) (*PreparedServerEarlyHandshake, error) } type PreparedServerEarlyHandshake struct { ResponsePayload []byte WrapConn func(raw net.Conn) (net.Conn, error) UserHash string } type earlyHandshakeMeta interface { HTTPMaskEarlyHandshakeUserHash() string } type earlyHandshakeConn struct { net.Conn userHash string } func (c *earlyHandshakeConn) HTTPMaskEarlyHandshakeUserHash() string { if c == nil { return "" } return c.userHash } func wrapEarlyHandshakeConn(conn net.Conn, userHash string) net.Conn { if conn == nil { return nil } return &earlyHandshakeConn{Conn: conn, userHash: userHash} } func EarlyHandshakeUserHash(conn net.Conn) (string, bool) { if conn == nil { return "", false } v, ok := conn.(earlyHandshakeMeta) if !ok { return "", false } return v.HTTPMaskEarlyHandshakeUserHash(), true } type authorizeResponse struct { token string earlyPayload []byte } func isTunnelTokenByte(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' } func parseAuthorizeResponse(body []byte) (*authorizeResponse, error) { s := strings.TrimSpace(string(body)) idx := strings.Index(s, "token=") if idx < 0 { return nil, errors.New("missing token") } s = s[idx+len("token="):] if s == "" { return nil, errors.New("empty token") } var b strings.Builder for i := 0; i < len(s); i++ { c := s[i] if isTunnelTokenByte(c) { b.WriteByte(c) continue } break } token := b.String() if token == "" { return nil, errors.New("empty token") } out := &authorizeResponse{token: token} if earlyLine := findAuthorizeField(body, "ed="); earlyLine != "" { decoded, err := base64.RawURLEncoding.DecodeString(earlyLine) if err != nil { return nil, fmt.Errorf("decode early authorize payload failed: %w", err) } out.earlyPayload = decoded } return out, nil } func findAuthorizeField(body []byte, prefix string) string { for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, prefix) { return strings.TrimSpace(strings.TrimPrefix(line, prefix)) } } return "" } func setEarlyDataQuery(rawURL string, payload []byte) (string, error) { if len(payload) == 0 { return rawURL, nil } u, err := url.Parse(rawURL) if err != nil { return "", err } q := u.Query() q.Set(tunnelEarlyDataQueryKey, base64.RawURLEncoding.EncodeToString(payload)) u.RawQuery = q.Encode() return u.String(), nil } func parseEarlyDataQuery(u *url.URL) ([]byte, error) { if u == nil { return nil, nil } val := strings.TrimSpace(u.Query().Get(tunnelEarlyDataQueryKey)) if val == "" { return nil, nil } return base64.RawURLEncoding.DecodeString(val) } func applyEarlyHandshakeOrUpgrade(raw net.Conn, opts TunnelDialOptions) (net.Conn, error) { out := raw if opts.EarlyHandshake != nil && opts.EarlyHandshake.WrapConn != nil && (opts.EarlyHandshake.Ready == nil || opts.EarlyHandshake.Ready()) { wrapped, err := opts.EarlyHandshake.WrapConn(raw) if err != nil { return nil, err } if wrapped != nil { out = wrapped } return out, nil } if opts.Upgrade != nil { wrapped, err := opts.Upgrade(raw) if err != nil { return nil, err } if wrapped != nil { out = wrapped } } return out, nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/halfpipe.go ================================================ package httpmask import ( "io" "net" "os" "sync" "time" ) type pipeDeadline struct { mu sync.Mutex timer *time.Timer cancel chan struct{} } func makePipeDeadline() pipeDeadline { return pipeDeadline{cancel: make(chan struct{})} } func (d *pipeDeadline) set(t time.Time) { d.mu.Lock() defer d.mu.Unlock() if d.timer != nil && !d.timer.Stop() { <-d.cancel } d.timer = nil closed := isClosedPipeChan(d.cancel) if t.IsZero() { if closed { d.cancel = make(chan struct{}) } return } if dur := time.Until(t); dur > 0 { if closed { d.cancel = make(chan struct{}) } d.timer = time.AfterFunc(dur, func() { close(d.cancel) }) return } if !closed { close(d.cancel) } } func (d *pipeDeadline) wait() <-chan struct{} { d.mu.Lock() ch := d.cancel d.mu.Unlock() return ch } func isClosedPipeChan(ch <-chan struct{}) bool { select { case <-ch: return true default: return false } } type halfPipeAddr struct{} func (halfPipeAddr) Network() string { return "pipe" } func (halfPipeAddr) String() string { return "pipe" } type halfPipeConn struct { wrMu sync.Mutex rdRx <-chan []byte rdTx chan<- int wrTx chan<- []byte wrRx <-chan int readOnce sync.Once writeOnce sync.Once localReadDone chan struct{} localWriteDone chan struct{} remoteReadDone <-chan struct{} remoteWriteDone <-chan struct{} readDeadline pipeDeadline writeDeadline pipeDeadline } func newHalfPipe() (net.Conn, net.Conn) { cb1 := make(chan []byte) cb2 := make(chan []byte) cn1 := make(chan int) cn2 := make(chan int) r1 := make(chan struct{}) w1 := make(chan struct{}) r2 := make(chan struct{}) w2 := make(chan struct{}) c1 := &halfPipeConn{ rdRx: cb1, rdTx: cn1, wrTx: cb2, wrRx: cn2, localReadDone: r1, localWriteDone: w1, remoteReadDone: r2, remoteWriteDone: w2, readDeadline: makePipeDeadline(), writeDeadline: makePipeDeadline(), } c2 := &halfPipeConn{ rdRx: cb2, rdTx: cn2, wrTx: cb1, wrRx: cn1, localReadDone: r2, localWriteDone: w2, remoteReadDone: r1, remoteWriteDone: w1, readDeadline: makePipeDeadline(), writeDeadline: makePipeDeadline(), } return c1, c2 } func (*halfPipeConn) LocalAddr() net.Addr { return halfPipeAddr{} } func (*halfPipeConn) RemoteAddr() net.Addr { return halfPipeAddr{} } func (c *halfPipeConn) Read(p []byte) (int, error) { switch { case isClosedPipeChan(c.localReadDone): return 0, io.ErrClosedPipe case isClosedPipeChan(c.remoteWriteDone): return 0, io.EOF case isClosedPipeChan(c.readDeadline.wait()): return 0, os.ErrDeadlineExceeded } select { case b := <-c.rdRx: n := copy(p, b) c.rdTx <- n return n, nil case <-c.localReadDone: return 0, io.ErrClosedPipe case <-c.remoteWriteDone: return 0, io.EOF case <-c.readDeadline.wait(): return 0, os.ErrDeadlineExceeded } } func (c *halfPipeConn) Write(p []byte) (int, error) { switch { case isClosedPipeChan(c.localWriteDone): return 0, io.ErrClosedPipe case isClosedPipeChan(c.remoteReadDone): return 0, io.ErrClosedPipe case isClosedPipeChan(c.writeDeadline.wait()): return 0, os.ErrDeadlineExceeded } c.wrMu.Lock() defer c.wrMu.Unlock() var ( total int rest = p ) for once := true; once || len(rest) > 0; once = false { select { case c.wrTx <- rest: n := <-c.wrRx rest = rest[n:] total += n case <-c.localWriteDone: return total, io.ErrClosedPipe case <-c.remoteReadDone: return total, io.ErrClosedPipe case <-c.writeDeadline.wait(): return total, os.ErrDeadlineExceeded } } return total, nil } func (c *halfPipeConn) CloseWrite() error { c.writeOnce.Do(func() { close(c.localWriteDone) }) return nil } func (c *halfPipeConn) CloseRead() error { c.readOnce.Do(func() { close(c.localReadDone) }) return nil } func (c *halfPipeConn) Close() error { _ = c.CloseRead() _ = c.CloseWrite() return nil } func (c *halfPipeConn) SetDeadline(t time.Time) error { c.readDeadline.set(t) c.writeDeadline.set(t) return nil } func (c *halfPipeConn) SetReadDeadline(t time.Time) error { c.readDeadline.set(t) return nil } func (c *halfPipeConn) SetWriteDeadline(t time.Time) error { c.writeDeadline.set(t) return nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/masker.go ================================================ package httpmask import ( "bufio" "bytes" "encoding/base64" "fmt" "io" "math/rand" "net" "strconv" "strings" "sync" "time" ) var ( userAgents = []string{ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36", } accepts = []string{ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "application/json, text/plain, */*", "application/octet-stream", "*/*", } acceptLanguages = []string{ "en-US,en;q=0.9", "en-GB,en;q=0.9", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7", "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", } acceptEncodings = []string{ "gzip, deflate, br", "gzip, deflate", "br, gzip, deflate", } paths = []string{ "/api/v1/upload", "/data/sync", "/uploads/raw", "/api/report", "/feed/update", "/v2/events", "/v1/telemetry", "/session", "/stream", "/ws", } contentTypes = []string{ "application/octet-stream", "application/x-protobuf", "application/json", } ) var ( rngPool = sync.Pool{ New: func() interface{} { return rand.New(rand.NewSource(time.Now().UnixNano())) }, } headerBufPool = sync.Pool{ New: func() interface{} { b := make([]byte, 0, 1024) return &b }, } ) // LooksLikeHTTPRequestStart reports whether peek4 looks like a supported HTTP/1.x request method prefix. func LooksLikeHTTPRequestStart(peek4 []byte) bool { if len(peek4) < 4 { return false } // Common methods: "GET ", "POST", "HEAD", "PUT ", "OPTI" (OPTIONS), "PATC" (PATCH), "DELE" (DELETE) return bytes.Equal(peek4, []byte("GET ")) || bytes.Equal(peek4, []byte("POST")) || bytes.Equal(peek4, []byte("HEAD")) || bytes.Equal(peek4, []byte("PUT ")) || bytes.Equal(peek4, []byte("OPTI")) || bytes.Equal(peek4, []byte("PATC")) || bytes.Equal(peek4, []byte("DELE")) } func trimPortForHost(host string) string { if host == "" { return host } // Accept "example.com:443" / "1.2.3.4:443" / "[::1]:443" h, _, err := net.SplitHostPort(host) if err == nil && h != "" { return h } // If it's not in host:port form, keep as-is. return host } func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte { ua := userAgents[r.Intn(len(userAgents))] accept := accepts[r.Intn(len(accepts))] lang := acceptLanguages[r.Intn(len(acceptLanguages))] enc := acceptEncodings[r.Intn(len(acceptEncodings))] buf = append(buf, "Host: "...) buf = append(buf, host...) buf = append(buf, "\r\nUser-Agent: "...) buf = append(buf, ua...) buf = append(buf, "\r\nAccept: "...) buf = append(buf, accept...) buf = append(buf, "\r\nAccept-Language: "...) buf = append(buf, lang...) buf = append(buf, "\r\nAccept-Encoding: "...) buf = append(buf, enc...) buf = append(buf, "\r\nConnection: keep-alive\r\n"...) // A couple of common cache headers; keep them static for simplicity. buf = append(buf, "Cache-Control: no-cache\r\nPragma: no-cache\r\n"...) return buf } // WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask. func WriteRandomRequestHeader(w io.Writer, host string) error { return WriteRandomRequestHeaderWithPathRoot(w, host, "") } // WriteRandomRequestHeaderWithPathRoot is like WriteRandomRequestHeader but prefixes all paths with pathRoot. // pathRoot must be a single segment (e.g. "aabbcc"); invalid inputs are treated as empty (disabled). func WriteRandomRequestHeaderWithPathRoot(w io.Writer, host string, pathRoot string) error { // Get RNG from pool r := rngPool.Get().(*rand.Rand) defer rngPool.Put(r) path := joinPathRoot(pathRoot, paths[r.Intn(len(paths))]) ctype := contentTypes[r.Intn(len(contentTypes))] // Use buffer pool bufPtr := headerBufPool.Get().(*[]byte) buf := *bufPtr buf = buf[:0] defer func() { if cap(buf) <= 4096 { *bufPtr = buf headerBufPool.Put(bufPtr) } }() // Weighted template selection. Keep a conservative default (POST w/ Content-Length), // but occasionally rotate to other realistic templates (e.g. WebSocket upgrade). switch r.Intn(10) { case 0, 1: // ~20% WebSocket-like upgrade hostNoPort := trimPortForHost(host) var keyBytes [16]byte for i := 0; i < len(keyBytes); i++ { keyBytes[i] = byte(r.Intn(256)) } wsKey := base64.StdEncoding.EncodeToString(keyBytes[:]) buf = append(buf, "GET "...) buf = append(buf, path...) buf = append(buf, " HTTP/1.1\r\n"...) buf = appendCommonHeaders(buf, host, r) buf = append(buf, "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...) buf = append(buf, wsKey...) buf = append(buf, "\r\nOrigin: https://"...) buf = append(buf, hostNoPort...) buf = append(buf, "\r\n\r\n"...) default: // ~80% POST upload // Random Content-Length: 4KB–10MB. Small enough to look plausible, large enough // to justify long-lived writes on keep-alive connections. const minCL = int64(4 * 1024) const maxCL = int64(10 * 1024 * 1024) contentLength := minCL + r.Int63n(maxCL-minCL+1) buf = append(buf, "POST "...) buf = append(buf, path...) buf = append(buf, " HTTP/1.1\r\n"...) buf = appendCommonHeaders(buf, host, r) buf = append(buf, "Content-Type: "...) buf = append(buf, ctype...) buf = append(buf, "\r\nContent-Length: "...) buf = strconv.AppendInt(buf, contentLength, 10) // A couple of extra headers seen in real clients. if r.Intn(2) == 0 { buf = append(buf, "\r\nX-Requested-With: XMLHttpRequest"...) } if r.Intn(3) == 0 { buf = append(buf, "\r\nReferer: https://"...) buf = append(buf, trimPortForHost(host)...) buf = append(buf, "/"...) } buf = append(buf, "\r\n\r\n"...) } _, err := w.Write(buf) return err } // ConsumeHeader 读取并消耗 HTTP 头部,返回消耗的数据和剩余的 reader 数据 // 如果不是 POST 请求或格式严重错误,返回 error func ConsumeHeader(r *bufio.Reader) ([]byte, error) { var consumed bytes.Buffer // 1. 读取请求行 // Use ReadSlice to avoid allocation if line fits in buffer line, err := r.ReadSlice('\n') if err != nil { return nil, err } consumed.Write(line) // Basic method validation: accept common HTTP/1.x methods used by our masker. // Keep it strict enough to reject obvious garbage. switch { case bytes.HasPrefix(line, []byte("POST ")), bytes.HasPrefix(line, []byte("GET ")), bytes.HasPrefix(line, []byte("HEAD ")), bytes.HasPrefix(line, []byte("PUT ")), bytes.HasPrefix(line, []byte("DELETE ")), bytes.HasPrefix(line, []byte("OPTIONS ")), bytes.HasPrefix(line, []byte("PATCH ")): default: return consumed.Bytes(), fmt.Errorf("invalid method or garbage: %s", strings.TrimSpace(string(line))) } // 2. 循环读取头部,直到遇到空行 for { line, err = r.ReadSlice('\n') if err != nil { return consumed.Bytes(), err } consumed.Write(line) // Check for empty line (\r\n or \n) // ReadSlice includes the delimiter n := len(line) if n == 2 && line[0] == '\r' && line[1] == '\n' { return consumed.Bytes(), nil } if n == 1 && line[0] == '\n' { return consumed.Bytes(), nil } } } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/pathroot.go ================================================ package httpmask import "strings" // normalizePathRoot normalizes the configured path root into "/" form. // // It is intentionally strict: only a single path segment is allowed, consisting of // [A-Za-z0-9_-]. Invalid inputs are treated as empty (disabled). func normalizePathRoot(root string) string { root = strings.TrimSpace(root) root = strings.Trim(root, "/") if root == "" { return "" } for i := 0; i < len(root); i++ { c := root[i] switch { case c >= 'a' && c <= 'z': case c >= 'A' && c <= 'Z': case c >= '0' && c <= '9': case c == '_' || c == '-': default: return "" } } return "/" + root } func joinPathRoot(root, path string) string { root = normalizePathRoot(root) if root == "" { return path } if path == "" { return root } if !strings.HasPrefix(path, "/") { path = "/" + path } return root + path } func stripPathRoot(root, fullPath string) (string, bool) { root = normalizePathRoot(root) if root == "" { return fullPath, true } if !strings.HasPrefix(fullPath, root+"/") { return "", false } return strings.TrimPrefix(fullPath, root), true } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/tunnel.go ================================================ package httpmask import ( "bufio" "bytes" "context" crand "crypto/rand" "encoding/base64" "errors" "fmt" "io" mrand "math/rand" "net" "net/url" "os" "strconv" "strings" "sync" "syscall" "time" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/http" "github.com/metacubex/http/httputil" "github.com/metacubex/tls" ) type TunnelMode string const ( TunnelModeLegacy TunnelMode = "legacy" TunnelModeStream TunnelMode = "stream" TunnelModePoll TunnelMode = "poll" TunnelModeAuto TunnelMode = "auto" TunnelModeWS TunnelMode = "ws" ) func normalizeTunnelMode(mode string) TunnelMode { switch strings.ToLower(strings.TrimSpace(mode)) { case "", string(TunnelModeLegacy): return TunnelModeLegacy case string(TunnelModeStream): return TunnelModeStream case string(TunnelModePoll): return TunnelModePoll case string(TunnelModeAuto): return TunnelModeAuto case string(TunnelModeWS): return TunnelModeWS default: // Be conservative: unknown => legacy return TunnelModeLegacy } } type HandleResult int const ( HandlePassThrough HandleResult = iota HandleStartTunnel HandleDone ) type TunnelDialOptions struct { Mode string TLSEnabled bool // when true, use HTTPS; otherwise, use HTTP (no port-based inference) HostOverride string // optional Host header / SNI host (without scheme); accepts "example.com" or "example.com:443" // PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints. // Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ... PathRoot string // AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing). // When set (non-empty), each HTTP request carries an Authorization bearer token derived from AuthKey. AuthKey string // EarlyHandshake folds the protocol handshake into the HTTP/WS setup round trip. // When the server accepts the early payload, DialTunnel returns a conn that is already post-handshake. // When the server does not echo early data, DialTunnel falls back to Upgrade. EarlyHandshake *ClientEarlyHandshake // Upgrade optionally wraps the raw tunnel conn and/or writes a small prelude before DialTunnel returns. // It is called with the raw tunnel conn; if it returns a non-nil conn, that conn is returned by DialTunnel. Upgrade func(raw net.Conn) (net.Conn, error) // Multiplex controls whether the caller should reuse underlying HTTP connections (HTTP/1.1 keep-alive / HTTP/2). // To reuse across multiple dials, create a TunnelClient per proxy and reuse it. // Values: "off" disables reuse; "auto"/"on" enables it. Multiplex string // DialContext overrides how the HTTP tunnel dials raw TCP/TLS connections. // It must not be nil; passing nil is a programming error. DialContext func(ctx context.Context, network, addr string) (net.Conn, error) } type TunnelClientOptions struct { TLSEnabled bool HostOverride string DialContext func(ctx context.Context, network, addr string) (net.Conn, error) MaxIdleConns int } type TunnelClient struct { transport *http.Transport target httpClientTarget } func NewTunnelClient(serverAddress string, opts TunnelClientOptions) (*TunnelClient, error) { maxIdle := opts.MaxIdleConns if maxIdle <= 0 { maxIdle = 32 } transport, target, err := buildHTTPTransport(serverAddress, opts.TLSEnabled, opts.HostOverride, opts.DialContext, maxIdle) if err != nil { return nil, err } return &TunnelClient{ transport: transport, target: target, }, nil } func (c *TunnelClient) CloseIdleConnections() { if c == nil || c.transport == nil { return } c.transport.CloseIdleConnections() } func (c *TunnelClient) DialTunnel(ctx context.Context, opts TunnelDialOptions) (net.Conn, error) { if c == nil || c.transport == nil { return nil, fmt.Errorf("nil tunnel client") } tm := normalizeTunnelMode(opts.Mode) if tm == TunnelModeLegacy { return nil, fmt.Errorf("legacy mode does not use http tunnel") } // Create a per-dial client while sharing the underlying Transport for connection reuse. // This matches upstream behavior and avoids potential client-level concurrency pitfalls. client := &http.Client{Transport: c.transport} switch tm { case TunnelModeStream: return dialStreamWithClient(ctx, client, c.target, opts) case TunnelModePoll: return dialPollWithClient(ctx, client, c.target, opts) case TunnelModeWS: return nil, fmt.Errorf("ws mode does not support TunnelClient reuse") case TunnelModeAuto: streamCtx, cancelX := context.WithTimeout(ctx, 3*time.Second) c1, errX := dialStreamWithClient(streamCtx, client, c.target, opts) cancelX() if errX == nil { return c1, nil } c2, errP := dialPollWithClient(ctx, client, c.target, opts) if errP == nil { return c2, nil } return nil, fmt.Errorf("auto tunnel failed: stream: %v; poll: %w", errX, errP) default: return dialStreamWithClient(ctx, client, c.target, opts) } } // DialTunnel establishes a bidirectional stream over HTTP: // - stream: a single streaming POST (request body uplink, response body downlink) // - poll: authorize + push/pull polling tunnel (base64 framed) // - auto: try stream then fall back to poll // // The returned net.Conn carries the raw Sudoku stream (no HTTP headers). func DialTunnel(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) { mode := normalizeTunnelMode(opts.Mode) if mode == TunnelModeLegacy { return nil, fmt.Errorf("legacy mode does not use http tunnel") } switch mode { case TunnelModeStream: return dialStreamFn(ctx, serverAddress, opts) case TunnelModePoll: return dialPollFn(ctx, serverAddress, opts) case TunnelModeWS: return dialWS(ctx, serverAddress, opts) case TunnelModeAuto: // "stream" can hang on some CDNs that buffer uploads until request body completes. // Keep it on a short leash so we can fall back to poll within the caller's deadline. streamCtx, cancelX := context.WithTimeout(ctx, 3*time.Second) c, errX := dialStreamFn(streamCtx, serverAddress, opts) cancelX() if errX == nil { return c, nil } c, errP := dialPollFn(ctx, serverAddress, opts) if errP == nil { return c, nil } return nil, fmt.Errorf("auto tunnel failed: stream: %v; poll: %w", errX, errP) default: return dialStreamFn(ctx, serverAddress, opts) } } var ( dialStreamFn = dialStream dialPollFn = dialPoll ) func canonicalHeaderHost(urlHost, scheme string) string { host, port, err := net.SplitHostPort(urlHost) if err != nil { return urlHost } defaultPort := "" switch scheme { case "https": defaultPort = "443" case "http": defaultPort = "80" } if defaultPort == "" || port != defaultPort { return urlHost } // If we strip the port from an IPv6 literal, re-add brackets to keep the Host header valid. if strings.Contains(host, ":") { return "[" + host + "]" } return host } func parseTunnelToken(body []byte) (string, error) { resp, err := parseAuthorizeResponse(body) if err != nil { return "", err } return resp.token, nil } type httpClientTarget struct { scheme string urlHost string headerHost string } func buildHTTPTransport(serverAddress string, tlsEnabled bool, hostOverride string, dialContext func(ctx context.Context, network, addr string) (net.Conn, error), maxIdleConns int) (*http.Transport, httpClientTarget, error) { if dialContext == nil { panic("httpmask: DialContext is nil") } scheme, urlHost, dialAddr, serverName, err := normalizeHTTPDialTarget(serverAddress, tlsEnabled, hostOverride) if err != nil { return nil, httpClientTarget{}, err } transport := &http.Transport{ ForceAttemptHTTP2: scheme == "https", DisableCompression: true, MaxIdleConns: maxIdleConns, MaxIdleConnsPerHost: maxIdleConns, IdleConnTimeout: 30 * time.Second, ResponseHeaderTimeout: 20 * time.Second, TLSHandshakeTimeout: 10 * time.Second, DialContext: func(dialCtx context.Context, network, _ string) (net.Conn, error) { return dialContext(dialCtx, network, dialAddr) }, } if scheme == "https" { var tlsConf *tls.Config tlsConf, err = ca.GetTLSConfig(ca.Option{TLSConfig: &tls.Config{ ServerName: serverName, MinVersion: tls.VersionTLS12, }}) if err != nil { return nil, httpClientTarget{}, err } transport.TLSClientConfig = tlsConf } return transport, httpClientTarget{ scheme: scheme, urlHost: urlHost, headerHost: canonicalHeaderHost(urlHost, scheme), }, nil } func newHTTPClient(serverAddress string, opts TunnelDialOptions, maxIdleConns int) (*http.Client, httpClientTarget, error) { transport, target, err := buildHTTPTransport(serverAddress, opts.TLSEnabled, opts.HostOverride, opts.DialContext, maxIdleConns) if err != nil { return nil, httpClientTarget{}, err } return &http.Client{Transport: transport}, target, nil } type sessionDialInfo struct { client *http.Client pushURL string pullURL string finURL string closeURL string headerHost string auth *tunnelAuth } type httpStatusError struct { code int status string } func (e *httpStatusError) Error() string { if e == nil { return "bad status" } if e.status != "" { return "bad status: " + e.status } return "bad status" } func isRetryableStatusCode(code int) bool { return code == http.StatusRequestTimeout || code == http.StatusTooManyRequests || code >= 500 } type idleConnCloser interface{ CloseIdleConnections() } func closeIdleConnections(client *http.Client) { if client == nil || client.Transport == nil { return } if c, ok := client.Transport.(idleConnCloser); ok { c.CloseIdleConnections() } } func dialSessionWithClient(ctx context.Context, client *http.Client, target httpClientTarget, mode TunnelMode, opts TunnelDialOptions) (*sessionDialInfo, error) { if client == nil { return nil, fmt.Errorf("nil http client") } auth := newTunnelAuth(opts.AuthKey, 0) authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/session")}).String() if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 { var err error authorizeURL, err = setEarlyDataQuery(authorizeURL, opts.EarlyHandshake.RequestPayload) if err != nil { return nil, err } } var bodyBytes []byte for attempt := 0; ; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil) if err != nil { return nil, err } req.Host = target.headerHost applyTunnelHeaders(req.Header, target.headerHost, mode) applyTunnelAuth(req, auth, mode, http.MethodGet, "/session") resp, err := client.Do(req) if err != nil { // Transient failure on reused keep-alive conns (multiplex=auto). Retry a few times. if attempt < 2 && (isDialError(err) || isRetryableRequestError(err)) { closeIdleConnections(client) select { case <-time.After(25 * time.Millisecond): continue case <-ctx.Done(): return nil, err } } return nil, err } bodyBytes, err = io.ReadAll(io.LimitReader(resp.Body, 4*1024)) _ = resp.Body.Close() if err != nil { if attempt < 2 && isRetryableRequestError(err) { closeIdleConnections(client) select { case <-time.After(25 * time.Millisecond): continue case <-ctx.Done(): return nil, err } } return nil, err } if resp.StatusCode != http.StatusOK { // Retry some transient proxy/CDN errors. if attempt < 2 && resp.StatusCode >= 500 { closeIdleConnections(client) select { case <-time.After(25 * time.Millisecond): continue case <-ctx.Done(): return nil, fmt.Errorf("%s authorize bad status: %s (%s)", mode, resp.Status, strings.TrimSpace(string(bodyBytes))) } } return nil, fmt.Errorf("%s authorize bad status: %s (%s)", mode, resp.Status, strings.TrimSpace(string(bodyBytes))) } break } authResp, err := parseAuthorizeResponse(bodyBytes) if err != nil { return nil, fmt.Errorf("%s authorize failed: %q", mode, strings.TrimSpace(string(bodyBytes))) } token := authResp.token if token == "" { return nil, fmt.Errorf("%s authorize empty token", mode) } if opts.EarlyHandshake != nil && len(authResp.earlyPayload) > 0 && opts.EarlyHandshake.HandleResponse != nil { if err := opts.EarlyHandshake.HandleResponse(authResp.earlyPayload); err != nil { return nil, err } } pushURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token)}).String() pullURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/stream"), RawQuery: "token=" + url.QueryEscape(token)}).String() finURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token) + "&fin=1"}).String() closeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token) + "&close=1"}).String() return &sessionDialInfo{ client: client, pushURL: pushURL, pullURL: pullURL, finURL: finURL, closeURL: closeURL, headerHost: target.headerHost, auth: auth, }, nil } func dialSession(ctx context.Context, serverAddress string, opts TunnelDialOptions, mode TunnelMode) (*sessionDialInfo, error) { client, target, err := newHTTPClient(serverAddress, opts, 32) if err != nil { return nil, err } return dialSessionWithClient(ctx, client, target, mode, opts) } func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mode TunnelMode, auth *tunnelAuth) { if client == nil || closeURL == "" || headerHost == "" { return } closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(closeCtx, http.MethodPost, closeURL, nil) if err != nil { return } req.Host = headerHost applyTunnelHeaders(req.Header, headerHost, mode) applyTunnelAuth(req, auth, mode, http.MethodPost, "/api/v1/upload") resp, err := client.Do(req) if err != nil || resp == nil { return } _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024)) _ = resp.Body.Close() } func bestEffortCloseWriteSession(client *http.Client, finURL, headerHost string, mode TunnelMode, auth *tunnelAuth) { if client == nil || finURL == "" || headerHost == "" { return } closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(closeCtx, http.MethodPost, finURL, nil) if err != nil { return } req.Host = headerHost applyTunnelHeaders(req.Header, headerHost, mode) applyTunnelAuth(req, auth, mode, http.MethodPost, "/api/v1/upload") resp, err := client.Do(req) if err != nil || resp == nil { return } _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024)) _ = resp.Body.Close() } func dialStreamWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) { // "stream" mode uses split-stream to stay CDN-friendly by default. return dialStreamSplitWithClient(ctx, client, target, opts) } func dialStream(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) { // "stream" mode uses split-stream to stay CDN-friendly by default. return dialStreamSplit(ctx, serverAddress, opts) } type queuedConn struct { rxc chan []byte closed chan struct{} writeCh chan []byte // writeClosed is closed by CloseWrite to stop accepting new payloads. // When closed, Write returns io.ErrClosedPipe, but Read is unaffected. writeClosed chan struct{} mu sync.Mutex readBuf []byte closeErr error localAddr net.Addr remoteAddr net.Addr } func (c *queuedConn) CloseWrite() error { if c == nil || c.writeClosed == nil { return nil } c.mu.Lock() if !isClosedPipeChan(c.writeClosed) { close(c.writeClosed) } c.mu.Unlock() return nil } func (c *queuedConn) closeWithError(err error) error { c.mu.Lock() select { case <-c.closed: c.mu.Unlock() return nil default: if err == nil { err = io.ErrClosedPipe } if c.closeErr == nil { c.closeErr = err } close(c.closed) } c.mu.Unlock() return nil } func (c *queuedConn) closedErr() error { c.mu.Lock() err := c.closeErr c.mu.Unlock() if err == nil { return io.ErrClosedPipe } return err } func (c *queuedConn) Read(b []byte) (n int, err error) { if len(c.readBuf) == 0 { select { case c.readBuf = <-c.rxc: case <-c.closed: return 0, c.closedErr() } } n = copy(b, c.readBuf) c.readBuf = c.readBuf[n:] return n, nil } func (c *queuedConn) Write(b []byte) (n int, err error) { if len(b) == 0 { return 0, nil } c.mu.Lock() select { case <-c.closed: c.mu.Unlock() return 0, c.closedErr() case <-c.writeClosed: c.mu.Unlock() return 0, io.ErrClosedPipe default: } c.mu.Unlock() payload := make([]byte, len(b)) copy(payload, b) select { case c.writeCh <- payload: return len(b), nil case <-c.closed: return 0, c.closedErr() case <-c.writeClosed: return 0, io.ErrClosedPipe } } func (c *queuedConn) LocalAddr() net.Addr { return c.localAddr } func (c *queuedConn) RemoteAddr() net.Addr { return c.remoteAddr } func (c *queuedConn) SetDeadline(time.Time) error { return nil } func (c *queuedConn) SetReadDeadline(time.Time) error { return nil } func (c *queuedConn) SetWriteDeadline(time.Time) error { return nil } type streamSplitConn struct { queuedConn ctx context.Context cancel context.CancelFunc client *http.Client pushURL string pullURL string finURL string closeURL string headerHost string auth *tunnelAuth } func (c *streamSplitConn) closeWithError(err error) error { _ = c.queuedConn.closeWithError(err) if c.cancel != nil { c.cancel() } bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModeStream, c.auth) return nil } func (c *streamSplitConn) Close() error { return c.closeWithError(io.ErrClosedPipe) } func newStreamSplitConnFromInfo(info *sessionDialInfo) *streamSplitConn { if info == nil { return nil } connCtx, cancel := context.WithCancel(context.Background()) c := &streamSplitConn{ ctx: connCtx, cancel: cancel, client: info.client, pushURL: info.pushURL, pullURL: info.pullURL, finURL: info.finURL, closeURL: info.closeURL, headerHost: info.headerHost, auth: info.auth, queuedConn: queuedConn{ rxc: make(chan []byte, 256), closed: make(chan struct{}), writeCh: make(chan []byte, 256), writeClosed: make(chan struct{}), localAddr: &net.TCPAddr{}, remoteAddr: &net.TCPAddr{}, }, } go c.pullLoop() go c.pushLoop() return c } func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) { info, err := dialSessionWithClient(ctx, client, target, TunnelModeStream, opts) if err != nil { return nil, err } c := newStreamSplitConnFromInfo(info) if c == nil { return nil, fmt.Errorf("failed to build stream split conn") } outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) if err != nil { _ = c.Close() return nil, err } return outConn, nil } func dialStreamSplit(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) { info, err := dialSession(ctx, serverAddress, opts, TunnelModeStream) if err != nil { return nil, err } c := newStreamSplitConnFromInfo(info) if c == nil { return nil, fmt.Errorf("failed to build stream split conn") } outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) if err != nil { _ = c.Close() return nil, err } return outConn, nil } func (c *streamSplitConn) pullLoop() { const ( // requestTimeout must be long enough for continuous high-throughput streams (e.g. mux + large downloads). // If it is too short, the client cancels the response mid-body and corrupts the byte stream. requestTimeout = 2 * time.Minute readChunkSize = 32 * 1024 idleBackoff = 25 * time.Millisecond maxDialRetry = 12 minBackoff = 10 * time.Millisecond maxBackoff = 250 * time.Millisecond ) var ( dialRetry int backoff = minBackoff ) buf := make([]byte, readChunkSize) for { select { case <-c.closed: return default: } reqCtx, cancel := context.WithTimeout(c.ctx, requestTimeout) req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, c.pullURL, nil) if err != nil { cancel() _ = c.closeWithError(fmt.Errorf("stream pull build request failed: %w", err)) return } req.Host = c.headerHost applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream) applyTunnelAuth(req, c.auth, TunnelModeStream, http.MethodGet, "/stream") resp, err := c.client.Do(req) if err != nil { cancel() if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry { dialRetry++ closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } _ = c.closeWithError(fmt.Errorf("stream pull request failed: %w", err)) return } dialRetry = 0 backoff = minBackoff if resp.StatusCode != http.StatusOK { if isRetryableStatusCode(resp.StatusCode) && dialRetry < maxDialRetry { dialRetry++ _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024)) _ = resp.Body.Close() cancel() closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } _ = resp.Body.Close() cancel() _ = c.closeWithError(fmt.Errorf("stream pull bad status: %s", resp.Status)) return } readAny := false for { n, rerr := resp.Body.Read(buf) if n > 0 { readAny = true payload := make([]byte, n) copy(payload, buf[:n]) select { case c.rxc <- payload: case <-c.closed: _ = resp.Body.Close() cancel() return } } if rerr != nil { _ = resp.Body.Close() cancel() if errors.Is(rerr, io.EOF) { // Long-poll ended; retry. break } // Some environments may sporadically reset the HTTP connection under load; treat // it as an ended long-poll and retry instead of tearing down the whole tunnel. if errors.Is(rerr, io.ErrUnexpectedEOF) || isRetryableRequestError(rerr) { break } _ = c.closeWithError(fmt.Errorf("stream pull read failed: %w", rerr)) return } } cancel() if !readAny { // Avoid tight loop if the server replied quickly with an empty body. select { case <-time.After(idleBackoff): case <-c.closed: return } } } } func (c *streamSplitConn) pushLoop() { const ( // Batching is critical for stability under high concurrency: every flush is a new TCP // connection in HTTP/1.1, and too many tiny uploads can overwhelm the accept backlog, // causing sporadic RSTs (connection reset by peer). // // Keep this below the server-side maxUploadBytes limit in streamPush(). maxBatchBytes = 512 * 1024 flushInterval = 25 * time.Millisecond requestTimeout = 20 * time.Second maxDialRetry = 12 minBackoff = 10 * time.Millisecond maxBackoff = 250 * time.Millisecond ) var ( buf bytes.Buffer timer = time.NewTimer(flushInterval) ) defer timer.Stop() flush := func() error { if buf.Len() == 0 { return nil } payload := buf.Bytes() reqCtx, cancel := context.WithTimeout(c.ctx, requestTimeout) req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.pushURL, bytes.NewReader(payload)) if err != nil { cancel() return err } // Be explicit: some http client forks won't auto-populate GetBody, which makes POST retries on stale // keep-alive connections flaky under multiplex=auto. req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(payload)), nil } req.Host = c.headerHost applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream) applyTunnelAuth(req, c.auth, TunnelModeStream, http.MethodPost, "/api/v1/upload") req.Header.Set("Content-Type", "application/octet-stream") resp, err := c.client.Do(req) if err != nil { cancel() return err } _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024)) _ = resp.Body.Close() cancel() if resp.StatusCode != http.StatusOK { return &httpStatusError{code: resp.StatusCode, status: resp.Status} } buf.Reset() return nil } flushWithRetry := func() error { dialRetry := 0 backoff := minBackoff for { if err := flush(); err == nil { return nil } else if se := (*httpStatusError)(nil); errors.As(err, &se) && isRetryableStatusCode(se.code) && dialRetry < maxDialRetry { dialRetry++ closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return io.ErrClosedPipe } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } else if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry { dialRetry++ closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return io.ErrClosedPipe } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } else { return err } } } resetTimer := func() { if !timer.Stop() { select { case <-timer.C: default: } } timer.Reset(flushInterval) } resetTimer() for { select { case b, ok := <-c.writeCh: if !ok { _ = flushWithRetry() return } if len(b) == 0 { continue } if buf.Len()+len(b) > maxBatchBytes { if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err)) return } resetTimer() } _, _ = buf.Write(b) if buf.Len() >= maxBatchBytes { if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err)) return } resetTimer() } case <-timer.C: if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err)) return } resetTimer() case <-c.writeClosed: // Drain any already-accepted writes so CloseWrite does not lose data. for { select { case b := <-c.writeCh: if len(b) == 0 { continue } if buf.Len()+len(b) > maxBatchBytes { if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err)) return } } _, _ = buf.Write(b) default: _ = flushWithRetry() bestEffortCloseWriteSession(c.client, c.finURL, c.headerHost, TunnelModeStream, c.auth) return } } case <-c.closed: _ = flushWithRetry() return } } } type pollConn struct { queuedConn ctx context.Context cancel context.CancelFunc client *http.Client pushURL string pullURL string finURL string closeURL string headerHost string auth *tunnelAuth } func isDialError(err error) bool { var urlErr *url.Error if errors.As(err, &urlErr) { return isDialError(urlErr.Err) } var opErr *net.OpError if errors.As(err, &opErr) { if opErr.Op == "dial" || opErr.Op == "connect" { return true } } return false } func isRetryableRequestError(err error) bool { if err == nil { return false } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return false } if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return true } // net/http may return this when reusing a keep-alive conn that the peer already closed. // Treat it as retryable: callers already implement bounded backoff retries. if strings.Contains(strings.ToLower(err.Error()), "server closed idle connection") { return true } // Unwrap common wrappers. var urlErr *url.Error if errors.As(err, &urlErr) { return isRetryableRequestError(urlErr.Err) } // Connection-level transient failures. if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) { return true } if errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed) { return true } var netErr net.Error if errors.As(err, &netErr) { return netErr.Timeout() || netErr.Temporary() } return false } func (c *pollConn) closeWithError(err error) error { _ = c.queuedConn.closeWithError(err) if c.cancel != nil { c.cancel() } bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModePoll, c.auth) return nil } func (c *pollConn) Close() error { return c.closeWithError(io.ErrClosedPipe) } func newPollConnFromInfo(info *sessionDialInfo) *pollConn { if info == nil { return nil } connCtx, cancel := context.WithCancel(context.Background()) c := &pollConn{ ctx: connCtx, cancel: cancel, client: info.client, pushURL: info.pushURL, pullURL: info.pullURL, finURL: info.finURL, closeURL: info.closeURL, headerHost: info.headerHost, auth: info.auth, queuedConn: queuedConn{ rxc: make(chan []byte, 128), closed: make(chan struct{}), writeCh: make(chan []byte, 256), writeClosed: make(chan struct{}), localAddr: &net.TCPAddr{}, remoteAddr: &net.TCPAddr{}, }, } go c.pullLoop() go c.pushLoop() return c } func dialPollWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) { info, err := dialSessionWithClient(ctx, client, target, TunnelModePoll, opts) if err != nil { return nil, err } c := newPollConnFromInfo(info) if c == nil { return nil, fmt.Errorf("failed to build poll conn") } outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) if err != nil { _ = c.Close() return nil, err } return outConn, nil } func dialPoll(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) { info, err := dialSession(ctx, serverAddress, opts, TunnelModePoll) if err != nil { return nil, err } c := newPollConnFromInfo(info) if c == nil { return nil, fmt.Errorf("failed to build poll conn") } outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) if err != nil { _ = c.Close() return nil, err } return outConn, nil } func (c *pollConn) pullLoop() { const ( maxDialRetry = 12 minBackoff = 10 * time.Millisecond maxBackoff = 250 * time.Millisecond ) var ( dialRetry int backoff = minBackoff ) for { select { case <-c.closed: return default: } reqCtx, cancel := context.WithTimeout(c.ctx, 30*time.Second) req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, c.pullURL, nil) if err != nil { cancel() _ = c.Close() return } req.Host = c.headerHost applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll) applyTunnelAuth(req, c.auth, TunnelModePoll, http.MethodGet, "/stream") resp, err := c.client.Do(req) if err != nil { cancel() if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry { dialRetry++ closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } _ = c.closeWithError(fmt.Errorf("poll pull request failed: %w", err)) return } dialRetry = 0 backoff = minBackoff if resp.StatusCode != http.StatusOK { if isRetryableStatusCode(resp.StatusCode) && dialRetry < maxDialRetry { dialRetry++ _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024)) _ = resp.Body.Close() cancel() closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } _ = resp.Body.Close() cancel() _ = c.closeWithError(fmt.Errorf("poll pull bad status: %s", resp.Status)) return } scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() if line == "" { continue } payload, err := base64.StdEncoding.DecodeString(line) if err != nil { _ = resp.Body.Close() _ = c.closeWithError(fmt.Errorf("poll pull decode failed: %w", err)) return } select { case c.rxc <- payload: case <-c.closed: _ = resp.Body.Close() return } } _ = resp.Body.Close() cancel() if err := scanner.Err(); err != nil { // Treat transient stream breaks (RST/EOF) as an ended long-poll and retry. if errors.Is(err, io.ErrUnexpectedEOF) || isRetryableRequestError(err) { continue } _ = c.closeWithError(fmt.Errorf("poll pull scan failed: %w", err)) return } } } func (c *pollConn) pushLoop() { const ( maxBatchBytes = 512 * 1024 flushInterval = 50 * time.Millisecond maxLineRawBytes = 16 * 1024 maxDialRetry = 12 minBackoff = 10 * time.Millisecond maxBackoff = 250 * time.Millisecond ) var ( buf bytes.Buffer pendingRaw int timer = time.NewTimer(flushInterval) ) defer timer.Stop() flush := func() error { if buf.Len() == 0 { return nil } payload := buf.Bytes() reqCtx, cancel := context.WithTimeout(c.ctx, 20*time.Second) req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.pushURL, bytes.NewReader(payload)) if err != nil { cancel() return err } req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(payload)), nil } req.Host = c.headerHost applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll) applyTunnelAuth(req, c.auth, TunnelModePoll, http.MethodPost, "/api/v1/upload") req.Header.Set("Content-Type", "text/plain") resp, err := c.client.Do(req) if err != nil { cancel() return err } _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024)) _ = resp.Body.Close() cancel() if resp.StatusCode != http.StatusOK { return &httpStatusError{code: resp.StatusCode, status: resp.Status} } buf.Reset() pendingRaw = 0 return nil } flushWithRetry := func() error { dialRetry := 0 backoff := minBackoff for { if err := flush(); err == nil { return nil } else if se := (*httpStatusError)(nil); errors.As(err, &se) && isRetryableStatusCode(se.code) && dialRetry < maxDialRetry { dialRetry++ closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return c.closedErr() } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } else if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry { dialRetry++ closeIdleConnections(c.client) select { case <-time.After(backoff): case <-c.closed: return c.closedErr() } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } else { return err } } } resetTimer := func() { if !timer.Stop() { select { case <-timer.C: default: } } timer.Reset(flushInterval) } resetTimer() for { select { case b, ok := <-c.writeCh: if !ok { _ = flushWithRetry() return } if len(b) == 0 { continue } // Split large writes into multiple base64 lines to cap per-line size. for len(b) > 0 { chunk := b if len(chunk) > maxLineRawBytes { chunk = b[:maxLineRawBytes] } b = b[len(chunk):] encLen := base64.StdEncoding.EncodedLen(len(chunk)) if pendingRaw+len(chunk) > maxBatchBytes || buf.Len()+encLen+1 > maxBatchBytes*2 { if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("poll push flush failed: %w", err)) return } } tmp := make([]byte, base64.StdEncoding.EncodedLen(len(chunk))) base64.StdEncoding.Encode(tmp, chunk) buf.Write(tmp) buf.WriteByte('\n') pendingRaw += len(chunk) } if pendingRaw >= maxBatchBytes { if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("poll push flush failed: %w", err)) return } resetTimer() } case <-timer.C: if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("poll push flush failed: %w", err)) return } resetTimer() case <-c.writeClosed: // Drain any already-accepted writes so CloseWrite does not lose data. for { select { case b := <-c.writeCh: if len(b) == 0 { continue } for len(b) > 0 { chunk := b if len(chunk) > maxLineRawBytes { chunk = b[:maxLineRawBytes] } b = b[len(chunk):] encLen := base64.StdEncoding.EncodedLen(len(chunk)) if pendingRaw+len(chunk) > maxBatchBytes || buf.Len()+encLen+1 > maxBatchBytes*2 { if err := flushWithRetry(); err != nil { _ = c.closeWithError(fmt.Errorf("poll push flush failed: %w", err)) return } } tmp := make([]byte, base64.StdEncoding.EncodedLen(len(chunk))) base64.StdEncoding.Encode(tmp, chunk) buf.Write(tmp) buf.WriteByte('\n') pendingRaw += len(chunk) } default: _ = flushWithRetry() bestEffortCloseWriteSession(c.client, c.finURL, c.headerHost, TunnelModePoll, c.auth) return } } case <-c.closed: _ = flushWithRetry() return } } } func normalizeHTTPDialTarget(serverAddress string, tlsEnabled bool, hostOverride string) (scheme, urlHost, dialAddr, serverName string, err error) { host, port, err := net.SplitHostPort(serverAddress) if err != nil { return "", "", "", "", fmt.Errorf("invalid server address %q: %w", serverAddress, err) } if hostOverride != "" { // Allow "example.com" or "example.com:443" if h, p, splitErr := net.SplitHostPort(hostOverride); splitErr == nil { if h != "" { hostOverride = h } if p != "" { port = p } } serverName = hostOverride urlHost = net.JoinHostPort(hostOverride, port) } else { serverName = host urlHost = net.JoinHostPort(host, port) } if tlsEnabled { scheme = "https" } else { scheme = "http" } dialAddr = net.JoinHostPort(host, port) return scheme, urlHost, dialAddr, trimPortForHost(serverName), nil } func applyTunnelHeaders(h http.Header, host string, mode TunnelMode) { r := rngPool.Get().(*mrand.Rand) ua := userAgents[r.Intn(len(userAgents))] accept := accepts[r.Intn(len(accepts))] lang := acceptLanguages[r.Intn(len(acceptLanguages))] enc := acceptEncodings[r.Intn(len(acceptEncodings))] rngPool.Put(r) h.Set("User-Agent", ua) h.Set("Accept", accept) h.Set("Accept-Language", lang) h.Set("Accept-Encoding", enc) h.Set("Cache-Control", "no-cache") h.Set("Pragma", "no-cache") h.Set("Connection", "keep-alive") h.Set("Host", host) h.Set("X-Sudoku-Tunnel", string(mode)) h.Set("X-Sudoku-Version", "1") } type TunnelServerOptions struct { Mode string // PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints. // Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ... PathRoot string // AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing). // When set (non-empty), the server requires each request to carry a valid Authorization bearer token. AuthKey string // AuthSkew controls allowed clock skew / replay window for AuthKey. 0 uses a conservative default. AuthSkew time.Duration // PassThroughOnReject controls how the server handles "recognized but rejected" tunnel requests // (e.g., wrong mode / wrong path / invalid token). When true, the request bytes are replayed back // to the caller as HandlePassThrough to allow higher-level fallback handling. PassThroughOnReject bool // PullReadTimeout controls how long the server long-poll waits for tunnel downlink data before replying with a keepalive newline. PullReadTimeout time.Duration // SessionTTL is a best-effort TTL to prevent leaked sessions. 0 uses a conservative default. SessionTTL time.Duration // EarlyHandshake optionally folds the protocol handshake into the initial HTTP/WS round trip. EarlyHandshake *TunnelServerEarlyHandshake } type TunnelServer struct { mode TunnelMode pathRoot string passThroughOnReject bool auth *tunnelAuth pullReadTimeout time.Duration sessionTTL time.Duration earlyHandshake *TunnelServerEarlyHandshake mu sync.Mutex sessions map[string]*tunnelSession } type tunnelSession struct { conn net.Conn lastActive time.Time } func NewTunnelServer(opts TunnelServerOptions) *TunnelServer { mode := normalizeTunnelMode(opts.Mode) if mode == TunnelModeLegacy { // Server-side "legacy" means: don't accept stream/poll tunnels; only passthrough. } pathRoot := normalizePathRoot(opts.PathRoot) auth := newTunnelAuth(opts.AuthKey, opts.AuthSkew) timeout := opts.PullReadTimeout if timeout <= 0 { timeout = 10 * time.Second } ttl := opts.SessionTTL if ttl <= 0 { ttl = 2 * time.Minute } return &TunnelServer{ mode: mode, pathRoot: pathRoot, auth: auth, passThroughOnReject: opts.PassThroughOnReject, pullReadTimeout: timeout, sessionTTL: ttl, earlyHandshake: opts.EarlyHandshake, sessions: make(map[string]*tunnelSession), } } // HandleConn inspects rawConn. If it is an HTTP tunnel request (X-Sudoku-Tunnel header), it is handled here and: // - returns HandleStartTunnel + a net.Conn that carries the raw Sudoku stream (stream mode or poll session pipe) // - or returns HandleDone if the HTTP request is a poll control request (push/pull) and no Sudoku handshake should run on this TCP conn // // If it is not an HTTP tunnel request (or server mode is legacy), it returns HandlePassThrough with a conn that replays any pre-read bytes. func (s *TunnelServer) HandleConn(rawConn net.Conn) (HandleResult, net.Conn, error) { if rawConn == nil { return HandleDone, nil, errors.New("nil conn") } // Small header read deadline to avoid stalling Accept loops. The actual Sudoku handshake has its own deadlines. _ = rawConn.SetReadDeadline(time.Now().Add(5 * time.Second)) var first [4]byte n, err := io.ReadFull(rawConn, first[:]) if err != nil { _ = rawConn.SetReadDeadline(time.Time{}) // Even if short-read, preserve bytes for downstream handlers. if n > 0 { return HandlePassThrough, newPreBufferedConn(rawConn, first[:n]), nil } return HandleDone, nil, err } pc := newPreBufferedConn(rawConn, first[:]) br := bufio.NewReader(pc) if !LooksLikeHTTPRequestStart(first[:]) { _ = rawConn.SetReadDeadline(time.Time{}) return HandlePassThrough, pc, nil } req, headerBytes, buffered, err := readHTTPHeader(br) _ = rawConn.SetReadDeadline(time.Time{}) if err != nil { // Not a valid HTTP request; hand it back to the legacy path with replay. prefix := make([]byte, 0, len(first)+len(headerBytes)+len(buffered)) if len(headerBytes) == 0 || !bytes.HasPrefix(headerBytes, first[:]) { prefix = append(prefix, first[:]...) } prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil } tunnelHeader := strings.ToLower(strings.TrimSpace(req.headers["x-sudoku-tunnel"])) if tunnelHeader == "" { // Some CDNs / forward proxies may strip unknown headers. When AuthKey is enabled, we can // safely infer the intended tunnel mode by verifying the Authorization token against // both stream/poll modes and picking the one that matches. if s.auth != nil { u, err := url.ParseRequestURI(req.target) if err == nil { path, ok := stripPathRoot(s.pathRoot, u.Path) if ok && s.isAllowedBasePath(path) { authVal := req.headers["authorization"] if authVal == "" { authVal = u.Query().Get(tunnelAuthQueryKey) } streamOK := s.auth.verifyValue(authVal, TunnelModeStream, req.method, path, time.Now()) pollOK := s.auth.verifyValue(authVal, TunnelModePoll, req.method, path, time.Now()) switch { case streamOK && !pollOK: tunnelHeader = string(TunnelModeStream) case pollOK && !streamOK: tunnelHeader = string(TunnelModePoll) } } } } if tunnelHeader == "" { // Not our tunnel; replay full bytes to legacy handler. prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil } } reject := func() (HandleResult, net.Conn, error) { prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) return HandlePassThrough, newRejectedPreBufferedConn(rawConn, prefix), nil } if s.mode == TunnelModeLegacy { if s.passThroughOnReject { return reject() } _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") _ = rawConn.Close() return HandleDone, nil, nil } switch TunnelMode(tunnelHeader) { case TunnelModeStream: if s.mode != TunnelModeStream && s.mode != TunnelModeAuto { if s.passThroughOnReject { return reject() } _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") _ = rawConn.Close() return HandleDone, nil, nil } return s.handleStream(rawConn, req, headerBytes, buffered) case TunnelModePoll: if s.mode != TunnelModePoll && s.mode != TunnelModeAuto { if s.passThroughOnReject { return reject() } _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") _ = rawConn.Close() return HandleDone, nil, nil } return s.handlePoll(rawConn, req, headerBytes, buffered) case TunnelModeWS: if s.mode != TunnelModeWS && s.mode != TunnelModeAuto { if s.passThroughOnReject { return reject() } _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") _ = rawConn.Close() return HandleDone, nil, nil } return s.handleWS(rawConn, req, headerBytes, buffered) default: if s.passThroughOnReject { return reject() } _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") _ = rawConn.Close() return HandleDone, nil, nil } } type httpRequestHeader struct { method string target string // path + query proto string headers map[string]string // lower-case keys } func readHTTPHeader(r *bufio.Reader) (*httpRequestHeader, []byte, []byte, error) { const maxHeaderBytes = 32 * 1024 var consumed bytes.Buffer readLine := func() ([]byte, error) { line, err := r.ReadSlice('\n') if len(line) > 0 { if consumed.Len()+len(line) > maxHeaderBytes { return line, fmt.Errorf("http header too large") } consumed.Write(line) } return line, err } // Request line line, err := readLine() if err != nil { return nil, consumed.Bytes(), readAllBuffered(r), err } lineStr := strings.TrimRight(string(line), "\r\n") parts := strings.SplitN(lineStr, " ", 3) if len(parts) != 3 { return nil, consumed.Bytes(), readAllBuffered(r), fmt.Errorf("invalid request line") } req := &httpRequestHeader{ method: parts[0], target: parts[1], proto: parts[2], headers: make(map[string]string), } // Headers for { line, err = readLine() if err != nil { return nil, consumed.Bytes(), readAllBuffered(r), err } trimmed := strings.TrimRight(string(line), "\r\n") if trimmed == "" { break } k, v, ok := strings.Cut(trimmed, ":") if !ok { continue } k = strings.ToLower(strings.TrimSpace(k)) v = strings.TrimSpace(v) if k == "" { continue } // Keep the first value; we only care about a small set. if _, exists := req.headers[k]; !exists { req.headers[k] = v } } return req, consumed.Bytes(), readAllBuffered(r), nil } func readAllBuffered(r *bufio.Reader) []byte { n := r.Buffered() if n <= 0 { return nil } b, err := r.Peek(n) if err != nil { return nil } out := make([]byte, n) copy(out, b) return out } type preBufferedConn struct { net.Conn buf []byte recorded []byte rejected bool } func (p *preBufferedConn) CloseWrite() error { if p == nil || p.Conn == nil { return nil } if cw, ok := p.Conn.(interface{ CloseWrite() error }); ok { return cw.CloseWrite() } return nil } func (p *preBufferedConn) CloseRead() error { if p == nil || p.Conn == nil { return nil } if cr, ok := p.Conn.(interface{ CloseRead() error }); ok { return cr.CloseRead() } return nil } func newPreBufferedConn(conn net.Conn, pre []byte) *preBufferedConn { cpy := make([]byte, len(pre)) copy(cpy, pre) return &preBufferedConn{Conn: conn, buf: cpy, recorded: cpy} } func newRejectedPreBufferedConn(conn net.Conn, pre []byte) *preBufferedConn { c := newPreBufferedConn(conn, pre) c.rejected = true return c } func (p *preBufferedConn) IsHTTPMaskRejected() bool { return p.rejected } func (p *preBufferedConn) GetBufferedAndRecorded() []byte { if len(p.recorded) == 0 { return nil } out := make([]byte, len(p.recorded)) copy(out, p.recorded) return out } func (p *preBufferedConn) Read(b []byte) (int, error) { if len(p.buf) > 0 { n := copy(b, p.buf) p.buf = p.buf[n:] return n, nil } return p.Conn.Read(b) } type bodyConn struct { net.Conn reader io.Reader writer io.WriteCloser tail io.Writer flush func() error } func (c *bodyConn) Read(p []byte) (int, error) { return c.reader.Read(p) } func (c *bodyConn) Write(p []byte) (int, error) { n, err := c.writer.Write(p) if c.flush != nil { _ = c.flush() } return n, err } func (c *bodyConn) Close() error { var firstErr error if c.writer != nil { if err := c.writer.Close(); err != nil && firstErr == nil { firstErr = err } // NewChunkedWriter does not write the final CRLF. Ensure a clean terminator. if c.tail != nil { _, _ = c.tail.Write([]byte("\r\n")) } else { _, _ = c.Conn.Write([]byte("\r\n")) } if c.flush != nil { _ = c.flush() } } if err := c.Conn.Close(); err != nil && firstErr == nil { firstErr = err } return firstErr } func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) { rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) { if s.passThroughOnReject { prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) return HandlePassThrough, newRejectedPreBufferedConn(rawConn, prefix), nil } _ = writeSimpleHTTPResponse(rawConn, code, body) _ = rawConn.Close() return HandleDone, nil, nil } u, err := url.ParseRequestURI(req.target) if err != nil { return rejectOrReply(http.StatusBadRequest, "bad request") } // Only accept plausible paths to reduce accidental exposure. path, ok := stripPathRoot(s.pathRoot, u.Path) if !ok || !s.isAllowedBasePath(path) { return rejectOrReply(http.StatusNotFound, "not found") } authVal := req.headers["authorization"] if authVal == "" { authVal = u.Query().Get(tunnelAuthQueryKey) } if !s.auth.verifyValue(authVal, TunnelModeStream, req.method, path, time.Now()) { return rejectOrReply(http.StatusNotFound, "not found") } token := u.Query().Get("token") closeFlag := u.Query().Get("close") == "1" finFlag := u.Query().Get("fin") == "1" switch strings.ToUpper(req.method) { case http.MethodGet: if token == "" && path == "/session" { earlyPayload, err := parseEarlyDataQuery(u) if err != nil { return rejectOrReply(http.StatusBadRequest, "bad request") } return s.sessionAuthorize(rawConn, earlyPayload) } // Stream split-session: GET /stream?token=... => downlink poll. if token != "" && path == "/stream" { if s.passThroughOnReject && !s.sessionHas(token) { return rejectOrReply(http.StatusNotFound, "not found") } return s.streamPull(rawConn, token) } return rejectOrReply(http.StatusBadRequest, "bad request") case http.MethodPost: // Stream split-session: POST /api/v1/upload?token=... => uplink push. if token != "" && path == "/api/v1/upload" { if s.passThroughOnReject && !s.sessionHas(token) { return rejectOrReply(http.StatusNotFound, "not found") } if closeFlag { s.sessionClose(token) _ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "") _ = rawConn.Close() return HandleDone, nil, nil } if finFlag { s.sessionCloseWrite(token) _ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "") _ = rawConn.Close() return HandleDone, nil, nil } bodyReader, err := newRequestBodyReader(newPreBufferedConn(rawConn, buffered), req.headers) if err != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request") _ = rawConn.Close() return HandleDone, nil, nil } return s.streamPush(rawConn, token, bodyReader) } // Stream-one: single full-duplex POST. if err := writeTunnelResponseHeader(rawConn); err != nil { _ = rawConn.Close() return HandleDone, nil, err } bodyReader, err := newRequestBodyReader(newPreBufferedConn(rawConn, buffered), req.headers) if err != nil { _ = rawConn.Close() return HandleDone, nil, err } bw := bufio.NewWriterSize(rawConn, 32*1024) chunked := httputil.NewChunkedWriter(bw) stream := &bodyConn{ Conn: rawConn, reader: bodyReader, writer: chunked, tail: bw, flush: bw.Flush, } return HandleStartTunnel, stream, nil default: return rejectOrReply(http.StatusBadRequest, "bad request") } } func (s *TunnelServer) isAllowedBasePath(path string) bool { for _, p := range paths { if path == p { return true } } return false } func newRequestBodyReader(conn net.Conn, headers map[string]string) (io.Reader, error) { br := bufio.NewReaderSize(conn, 32*1024) te := strings.ToLower(headers["transfer-encoding"]) if strings.Contains(te, "chunked") { return httputil.NewChunkedReader(br), nil } if clStr := headers["content-length"]; clStr != "" { n, err := strconv.ParseInt(strings.TrimSpace(clStr), 10, 64) if err != nil || n < 0 { return nil, fmt.Errorf("invalid content-length") } return io.LimitReader(br, n), nil } return br, nil } func writeTunnelResponseHeader(w io.Writer) error { _, err := io.WriteString(w, "HTTP/1.1 200 OK\r\n"+ "Content-Type: application/octet-stream\r\n"+ "Transfer-Encoding: chunked\r\n"+ "Cache-Control: no-store\r\n"+ "Pragma: no-cache\r\n"+ "Connection: keep-alive\r\n"+ "X-Accel-Buffering: no\r\n"+ "\r\n") return err } func writeSimpleHTTPResponse(w io.Writer, code int, body string) error { if body == "" { body = http.StatusText(code) } body = strings.TrimRight(body, "\r\n") _, err := io.WriteString(w, fmt.Sprintf("HTTP/1.1 %d %s\r\nContent-Type: text/plain\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", code, http.StatusText(code), len(body), body)) return err } func writeTokenHTTPResponse(w io.Writer, token string) error { token = strings.TrimRight(token, "\r\n") return writeTokenHTTPResponseWithEarlyData(w, token, nil) } func writeTokenHTTPResponseWithEarlyData(w io.Writer, token string, earlyPayload []byte) error { token = strings.TrimRight(token, "\r\n") body := "token=" + token if len(earlyPayload) > 0 { body += "\ned=" + base64.RawURLEncoding.EncodeToString(earlyPayload) } _, err := io.WriteString(w, fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nCache-Control: no-store\r\nPragma: no-cache\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", len(body), body)) return err } func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) { rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) { if s.passThroughOnReject { prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) return HandlePassThrough, newRejectedPreBufferedConn(rawConn, prefix), nil } _ = writeSimpleHTTPResponse(rawConn, code, body) _ = rawConn.Close() return HandleDone, nil, nil } u, err := url.ParseRequestURI(req.target) if err != nil { return rejectOrReply(http.StatusBadRequest, "bad request") } path, ok := stripPathRoot(s.pathRoot, u.Path) if !ok || !s.isAllowedBasePath(path) { return rejectOrReply(http.StatusNotFound, "not found") } authVal := req.headers["authorization"] if authVal == "" { authVal = u.Query().Get(tunnelAuthQueryKey) } if !s.auth.verifyValue(authVal, TunnelModePoll, req.method, path, time.Now()) { return rejectOrReply(http.StatusNotFound, "not found") } token := u.Query().Get("token") closeFlag := u.Query().Get("close") == "1" finFlag := u.Query().Get("fin") == "1" switch strings.ToUpper(req.method) { case http.MethodGet: if token == "" && path == "/session" { earlyPayload, err := parseEarlyDataQuery(u) if err != nil { return rejectOrReply(http.StatusBadRequest, "bad request") } return s.sessionAuthorize(rawConn, earlyPayload) } if token != "" && path == "/stream" { if s.passThroughOnReject && !s.sessionHas(token) { return rejectOrReply(http.StatusNotFound, "not found") } return s.pollPull(rawConn, token) } return rejectOrReply(http.StatusBadRequest, "bad request") case http.MethodPost: if token == "" || path != "/api/v1/upload" { return rejectOrReply(http.StatusBadRequest, "bad request") } if s.passThroughOnReject && !s.sessionHas(token) { return rejectOrReply(http.StatusNotFound, "not found") } if closeFlag { s.sessionClose(token) _ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "") _ = rawConn.Close() return HandleDone, nil, nil } if finFlag { s.sessionCloseWrite(token) _ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "") _ = rawConn.Close() return HandleDone, nil, nil } bodyReader, err := newRequestBodyReader(newPreBufferedConn(rawConn, buffered), req.headers) if err != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request") _ = rawConn.Close() return HandleDone, nil, nil } return s.pollPush(rawConn, token, bodyReader) default: return rejectOrReply(http.StatusBadRequest, "bad request") } } func (s *TunnelServer) sessionAuthorize(rawConn net.Conn, earlyPayload []byte) (HandleResult, net.Conn, error) { token, err := newSessionToken() if err != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusInternalServerError, "internal error") _ = rawConn.Close() return HandleDone, nil, nil } c1, c2 := newHalfPipe() outConn := net.Conn(c1) var responsePayload []byte var userHash string if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil { prepared, err := s.earlyHandshake.Prepare(earlyPayload) if err != nil { _ = c1.Close() _ = c2.Close() if s.passThroughOnReject { return HandlePassThrough, newRejectedPreBufferedConn(rawConn, nil), nil } _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") _ = rawConn.Close() return HandleDone, nil, nil } responsePayload = prepared.ResponsePayload userHash = prepared.UserHash if prepared.WrapConn != nil { wrapped, err := prepared.WrapConn(c1) if err != nil { _ = c1.Close() _ = c2.Close() _ = writeSimpleHTTPResponse(rawConn, http.StatusInternalServerError, "internal error") _ = rawConn.Close() return HandleDone, nil, nil } if wrapped != nil { outConn = wrapEarlyHandshakeConn(wrapped, userHash) } } } s.mu.Lock() s.sessions[token] = &tunnelSession{conn: c2, lastActive: time.Now()} s.mu.Unlock() go s.reapLater(token) _ = writeTokenHTTPResponseWithEarlyData(rawConn, token, responsePayload) _ = rawConn.Close() return HandleStartTunnel, outConn, nil } func newSessionToken() (string, error) { var b [16]byte if _, err := crand.Read(b[:]); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b[:]), nil } func (s *TunnelServer) reapLater(token string) { ttl := s.sessionTTL if ttl <= 0 { return } timer := time.NewTimer(ttl) defer timer.Stop() for { <-timer.C s.mu.Lock() sess, ok := s.sessions[token] if !ok { s.mu.Unlock() return } idle := time.Since(sess.lastActive) if idle >= ttl { delete(s.sessions, token) s.mu.Unlock() _ = sess.conn.Close() return } next := ttl - idle s.mu.Unlock() // Avoid a tight loop under high-frequency activity; we only need best-effort cleanup. if next < 50*time.Millisecond { next = 50 * time.Millisecond } timer.Reset(next) } } func (s *TunnelServer) sessionHas(token string) bool { s.mu.Lock() _, ok := s.sessions[token] s.mu.Unlock() return ok } func (s *TunnelServer) sessionGet(token string) (*tunnelSession, bool) { s.mu.Lock() defer s.mu.Unlock() sess, ok := s.sessions[token] if !ok { return nil, false } sess.lastActive = time.Now() return sess, true } func (s *TunnelServer) sessionClose(token string) { s.mu.Lock() sess, ok := s.sessions[token] if ok { delete(s.sessions, token) } s.mu.Unlock() if ok { _ = sess.conn.Close() } } func (s *TunnelServer) sessionCloseWrite(token string) { sess, ok := s.sessionGet(token) if !ok || sess == nil || sess.conn == nil { return } if cw, ok := sess.conn.(interface{ CloseWrite() error }); ok { _ = cw.CloseWrite() return } _ = sess.conn.Close() } func (s *TunnelServer) pollPush(rawConn net.Conn, token string, body io.Reader) (HandleResult, net.Conn, error) { sess, ok := s.sessionGet(token) if !ok { _ = writeSimpleHTTPResponse(rawConn, http.StatusForbidden, "forbidden") _ = rawConn.Close() return HandleDone, nil, nil } payload, err := io.ReadAll(io.LimitReader(body, 1<<20)) // 1MiB per request cap if err != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request") _ = rawConn.Close() return HandleDone, nil, nil } lines := bytes.Split(payload, []byte{'\n'}) for _, line := range lines { line = bytes.TrimSpace(line) if len(line) == 0 { continue } decoded := make([]byte, base64.StdEncoding.DecodedLen(len(line))) n, decErr := base64.StdEncoding.Decode(decoded, line) if decErr != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request") _ = rawConn.Close() return HandleDone, nil, nil } if n == 0 { continue } _ = sess.conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) _, werr := sess.conn.Write(decoded[:n]) _ = sess.conn.SetWriteDeadline(time.Time{}) if werr != nil { s.sessionClose(token) _ = writeSimpleHTTPResponse(rawConn, http.StatusGone, "gone") _ = rawConn.Close() return HandleDone, nil, nil } } _ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "") _ = rawConn.Close() return HandleDone, nil, nil } func (s *TunnelServer) streamPush(rawConn net.Conn, token string, body io.Reader) (HandleResult, net.Conn, error) { sess, ok := s.sessionGet(token) if !ok { _ = writeSimpleHTTPResponse(rawConn, http.StatusForbidden, "forbidden") _ = rawConn.Close() return HandleDone, nil, nil } const maxUploadBytes = 1 << 20 payload, err := io.ReadAll(io.LimitReader(body, maxUploadBytes+1)) if err != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request") _ = rawConn.Close() return HandleDone, nil, nil } if len(payload) > maxUploadBytes { _ = writeSimpleHTTPResponse(rawConn, http.StatusRequestEntityTooLarge, "too large") _ = rawConn.Close() return HandleDone, nil, nil } if len(payload) > 0 { _ = sess.conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) _, werr := sess.conn.Write(payload) _ = sess.conn.SetWriteDeadline(time.Time{}) if werr != nil { s.sessionClose(token) _ = writeSimpleHTTPResponse(rawConn, http.StatusGone, "gone") _ = rawConn.Close() return HandleDone, nil, nil } } _ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "") _ = rawConn.Close() return HandleDone, nil, nil } func (s *TunnelServer) streamPull(rawConn net.Conn, token string) (HandleResult, net.Conn, error) { sess, ok := s.sessionGet(token) if !ok { _ = writeSimpleHTTPResponse(rawConn, http.StatusForbidden, "forbidden") _ = rawConn.Close() return HandleDone, nil, nil } // Streaming response (chunked) with raw bytes (no base64 framing). if err := writeTunnelResponseHeader(rawConn); err != nil { _ = rawConn.Close() return HandleDone, nil, err } bw := bufio.NewWriterSize(rawConn, 32*1024) cw := httputil.NewChunkedWriter(bw) defer func() { _ = cw.Close() _, _ = bw.WriteString("\r\n") _ = bw.Flush() _ = rawConn.Close() }() buf := make([]byte, 32*1024) for { _ = sess.conn.SetReadDeadline(time.Now().Add(s.pullReadTimeout)) n, err := sess.conn.Read(buf) if n > 0 { _, _ = cw.Write(buf[:n]) _ = bw.Flush() } if err != nil { if errors.Is(err, os.ErrDeadlineExceeded) { // End this long-poll response; client will re-issue. return HandleDone, nil, nil } if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed) { return HandleDone, nil, nil } s.sessionClose(token) return HandleDone, nil, nil } } } func (s *TunnelServer) pollPull(rawConn net.Conn, token string) (HandleResult, net.Conn, error) { sess, ok := s.sessionGet(token) if !ok { _ = writeSimpleHTTPResponse(rawConn, http.StatusForbidden, "forbidden") _ = rawConn.Close() return HandleDone, nil, nil } // Streaming response (chunked) with base64 lines. if err := writeTunnelResponseHeader(rawConn); err != nil { _ = rawConn.Close() return HandleDone, nil, err } bw := bufio.NewWriterSize(rawConn, 32*1024) cw := httputil.NewChunkedWriter(bw) defer func() { _ = cw.Close() _, _ = bw.WriteString("\r\n") _ = bw.Flush() _ = rawConn.Close() }() buf := make([]byte, 32*1024) for { _ = sess.conn.SetReadDeadline(time.Now().Add(s.pullReadTimeout)) n, err := sess.conn.Read(buf) if n > 0 { line := make([]byte, base64.StdEncoding.EncodedLen(n)) base64.StdEncoding.Encode(line, buf[:n]) _, _ = cw.Write(append(line, '\n')) _ = bw.Flush() } if err != nil { if errors.Is(err, os.ErrDeadlineExceeded) { // Keepalive: send an empty line then end this long-poll response. _, _ = cw.Write([]byte("\n")) _ = bw.Flush() return HandleDone, nil, nil } if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed) { return HandleDone, nil, nil } s.sessionClose(token) return HandleDone, nil, nil } } } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/tunnel_ws.go ================================================ package httpmask import ( "context" "encoding/base64" "fmt" "io" mrand "math/rand" "net" stdhttp "net/http" "net/url" "strings" "time" "github.com/gobwas/ws" "github.com/metacubex/tls" ) func normalizeWSSchemeFromAddress(serverAddress string, tlsEnabled bool) (string, string) { addr := strings.TrimSpace(serverAddress) if strings.Contains(addr, "://") { if u, err := url.Parse(addr); err == nil && u != nil { switch strings.ToLower(strings.TrimSpace(u.Scheme)) { case "ws": return "ws", u.Host case "wss": return "wss", u.Host } } } if tlsEnabled { return "wss", addr } return "ws", addr } func normalizeWSDialTarget(serverAddress string, tlsEnabled bool, hostOverride string) (scheme, urlHost, dialAddr, serverName string, err error) { scheme, addr := normalizeWSSchemeFromAddress(serverAddress, tlsEnabled) host, port, err := net.SplitHostPort(addr) if err != nil { // Allow ws(s)://host without port. if strings.Contains(addr, ":") { return "", "", "", "", fmt.Errorf("invalid server address %q: %w", serverAddress, err) } switch scheme { case "wss": port = "443" default: port = "80" } host = addr } if hostOverride != "" { // Allow "example.com" or "example.com:443" if h, p, splitErr := net.SplitHostPort(hostOverride); splitErr == nil { if h != "" { hostOverride = h } if p != "" { port = p } } serverName = hostOverride urlHost = net.JoinHostPort(hostOverride, port) } else { serverName = host urlHost = net.JoinHostPort(host, port) } dialAddr = net.JoinHostPort(host, port) return scheme, urlHost, dialAddr, trimPortForHost(serverName), nil } func applyWSHeaders(h stdhttp.Header, host string) { if h == nil { return } r := rngPool.Get().(*mrand.Rand) ua := userAgents[r.Intn(len(userAgents))] accept := accepts[r.Intn(len(accepts))] lang := acceptLanguages[r.Intn(len(acceptLanguages))] enc := acceptEncodings[r.Intn(len(acceptEncodings))] rngPool.Put(r) h.Set("User-Agent", ua) h.Set("Accept", accept) h.Set("Accept-Language", lang) h.Set("Accept-Encoding", enc) h.Set("Cache-Control", "no-cache") h.Set("Pragma", "no-cache") h.Set("X-Sudoku-Tunnel", string(TunnelModeWS)) h.Set("X-Sudoku-Version", "1") } func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) { if opts.DialContext == nil { panic("httpmask: DialContext is nil") } scheme, urlHost, dialAddr, serverName, err := normalizeWSDialTarget(serverAddress, opts.TLSEnabled, opts.HostOverride) if err != nil { return nil, err } httpScheme := "http" if scheme == "wss" { httpScheme = "https" } headerHost := canonicalHeaderHost(urlHost, httpScheme) auth := newTunnelAuth(opts.AuthKey, 0) u := &url.URL{ Scheme: scheme, Host: urlHost, Path: joinPathRoot(opts.PathRoot, "/ws"), } if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 { rawURL, err := setEarlyDataQuery(u.String(), opts.EarlyHandshake.RequestPayload) if err != nil { return nil, err } u, err = url.Parse(rawURL) if err != nil { return nil, err } } header := make(stdhttp.Header) applyWSHeaders(header, headerHost) if auth != nil { token := auth.token(TunnelModeWS, stdhttp.MethodGet, "/ws", time.Now()) if token != "" { header.Set("Authorization", "Bearer "+token) q := u.Query() q.Set(tunnelAuthQueryKey, token) u.RawQuery = q.Encode() } } d := ws.Dialer{ Host: headerHost, Header: ws.HandshakeHeaderHTTP(header), OnHeader: func(key, value []byte) error { if !strings.EqualFold(string(key), tunnelEarlyDataHeader) || opts.EarlyHandshake == nil || opts.EarlyHandshake.HandleResponse == nil { return nil } decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(string(value))) if err != nil { return err } return opts.EarlyHandshake.HandleResponse(decoded) }, NetDial: func(dialCtx context.Context, network, addr string) (net.Conn, error) { if addr == urlHost { addr = dialAddr } return opts.DialContext(dialCtx, network, addr) }, } if scheme == "wss" { tlsConfig := &tls.Config{ ServerName: serverName, MinVersion: tls.VersionTLS12, } d.TLSClient = func(conn net.Conn, hostname string) net.Conn { return tls.Client(conn, tlsConfig) } } conn, br, _, err := d.Dial(ctx, u.String()) if err != nil { return nil, err } if br != nil && br.Buffered() > 0 { pre := make([]byte, br.Buffered()) _, _ = io.ReadFull(br, pre) conn = newPreBufferedConn(conn, pre) } wsConn := newWSStreamConn(conn, ws.StateClientSide) upgraded, err := applyEarlyHandshakeOrUpgrade(wsConn, opts) if err != nil { _ = wsConn.Close() return nil, err } return upgraded, nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/tunnel_ws_server.go ================================================ package httpmask import ( "encoding/base64" "net" "net/http" "net/url" "strings" "time" "github.com/gobwas/ws" ) func looksLikeWebSocketUpgrade(headers map[string]string) bool { if headers == nil { return false } if !strings.EqualFold(strings.TrimSpace(headers["upgrade"]), "websocket") { return false } conn := headers["connection"] for _, part := range strings.Split(conn, ",") { if strings.EqualFold(strings.TrimSpace(part), "upgrade") { return true } } return false } func (s *TunnelServer) handleWS(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) { rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) { if s.passThroughOnReject { prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) return HandlePassThrough, newRejectedPreBufferedConn(rawConn, prefix), nil } _ = writeSimpleHTTPResponse(rawConn, code, body) _ = rawConn.Close() return HandleDone, nil, nil } u, err := url.ParseRequestURI(req.target) if err != nil { return rejectOrReply(http.StatusBadRequest, "bad request") } path, ok := stripPathRoot(s.pathRoot, u.Path) if !ok || path != "/ws" { return rejectOrReply(http.StatusNotFound, "not found") } if strings.ToUpper(strings.TrimSpace(req.method)) != http.MethodGet { return rejectOrReply(http.StatusBadRequest, "bad request") } if !looksLikeWebSocketUpgrade(req.headers) { return rejectOrReply(http.StatusBadRequest, "bad request") } authVal := req.headers["authorization"] if authVal == "" { authVal = u.Query().Get(tunnelAuthQueryKey) } if !s.auth.verifyValue(authVal, TunnelModeWS, req.method, path, time.Now()) { return rejectOrReply(http.StatusNotFound, "not found") } earlyPayload, err := parseEarlyDataQuery(u) if err != nil { return rejectOrReply(http.StatusBadRequest, "bad request") } var prepared *PreparedServerEarlyHandshake if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil { prepared, err = s.earlyHandshake.Prepare(earlyPayload) if err != nil { return rejectOrReply(http.StatusNotFound, "not found") } } prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) wsConnRaw := newPreBufferedConn(rawConn, prefix) upgrader := ws.Upgrader{} if prepared != nil && len(prepared.ResponsePayload) > 0 { upgrader.OnBeforeUpgrade = func() (ws.HandshakeHeader, error) { h := http.Header{} h.Set(tunnelEarlyDataHeader, base64.RawURLEncoding.EncodeToString(prepared.ResponsePayload)) return ws.HandshakeHeaderHTTP(h), nil } } if _, err := upgrader.Upgrade(wsConnRaw); err != nil { _ = rawConn.Close() return HandleDone, nil, nil } outConn := net.Conn(newWSStreamConn(wsConnRaw, ws.StateServerSide)) if prepared != nil && prepared.WrapConn != nil { wrapped, err := prepared.WrapConn(outConn) if err != nil { _ = outConn.Close() return HandleDone, nil, nil } if wrapped != nil { outConn = wrapEarlyHandshakeConn(wrapped, prepared.UserHash) } } return HandleStartTunnel, outConn, nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/httpmask/ws_stream_conn.go ================================================ package httpmask import ( "errors" "fmt" "io" "net" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" ) type wsStreamConn struct { net.Conn state ws.State reader *wsutil.Reader controlHandler wsutil.FrameHandlerFunc } func newWSStreamConn(conn net.Conn, state ws.State) net.Conn { controlHandler := wsutil.ControlFrameHandler(conn, state) return &wsStreamConn{ Conn: conn, state: state, reader: &wsutil.Reader{ Source: conn, State: state, }, controlHandler: controlHandler, } } func (c *wsStreamConn) Read(b []byte) (n int, err error) { defer func() { if v := recover(); v != nil { err = fmt.Errorf("websocket error: %v", v) } }() for { n, err = c.reader.Read(b) if errors.Is(err, io.EOF) { err = nil } if !errors.Is(err, wsutil.ErrNoFrameAdvance) { return n, err } hdr, err2 := c.reader.NextFrame() if err2 != nil { return 0, err2 } if hdr.OpCode.IsControl() { if err := c.controlHandler(hdr, c.reader); err != nil { return 0, err } continue } if hdr.OpCode&(ws.OpBinary|ws.OpText) == 0 { if err := c.reader.Discard(); err != nil { return 0, err } continue } } } func (c *wsStreamConn) Write(b []byte) (int, error) { if err := wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, b); err != nil { return 0, err } return len(b), nil } func (c *wsStreamConn) Close() error { _ = wsutil.WriteMessage(c.Conn, c.state, ws.OpClose, ws.NewCloseFrameBody(ws.StatusNormalClosure, "")) return c.Conn.Close() } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/ascii_mode.go ================================================ package sudoku import ( "fmt" "strings" ) const ( asciiModeTokenASCII = "ascii" asciiModeTokenEntropy = "entropy" ) // ASCIIMode describes the preferred wire layout for each traffic direction. // Uplink is client->server, Downlink is server->client. type ASCIIMode struct { Uplink string Downlink string } // ParseASCIIMode accepts legacy symmetric values ("ascii"/"entropy"/"prefer_*") // and directional values like "up_ascii_down_entropy". func ParseASCIIMode(mode string) (ASCIIMode, error) { raw := strings.ToLower(strings.TrimSpace(mode)) switch raw { case "", "entropy", "prefer_entropy": return ASCIIMode{Uplink: asciiModeTokenEntropy, Downlink: asciiModeTokenEntropy}, nil case "ascii", "prefer_ascii": return ASCIIMode{Uplink: asciiModeTokenASCII, Downlink: asciiModeTokenASCII}, nil } if !strings.HasPrefix(raw, "up_") { return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode) } parts := strings.SplitN(strings.TrimPrefix(raw, "up_"), "_down_", 2) if len(parts) != 2 { return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode) } up, ok := normalizeASCIIModeToken(parts[0]) if !ok { return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode) } down, ok := normalizeASCIIModeToken(parts[1]) if !ok { return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode) } return ASCIIMode{Uplink: up, Downlink: down}, nil } // NormalizeASCIIMode returns the canonical config string for a supported mode. func NormalizeASCIIMode(mode string) (string, error) { parsed, err := ParseASCIIMode(mode) if err != nil { return "", err } return parsed.Canonical(), nil } func (m ASCIIMode) Canonical() string { if m.Uplink == asciiModeTokenASCII && m.Downlink == asciiModeTokenASCII { return "prefer_ascii" } if m.Uplink == asciiModeTokenEntropy && m.Downlink == asciiModeTokenEntropy { return "prefer_entropy" } return "up_" + m.Uplink + "_down_" + m.Downlink } func (m ASCIIMode) uplinkPreference() string { return singleDirectionPreference(m.Uplink) } func (m ASCIIMode) downlinkPreference() string { return singleDirectionPreference(m.Downlink) } func normalizeASCIIModeToken(token string) (string, bool) { switch strings.ToLower(strings.TrimSpace(token)) { case "ascii", "prefer_ascii": return asciiModeTokenASCII, true case "entropy", "prefer_entropy", "": return asciiModeTokenEntropy, true default: return "", false } } func singleDirectionPreference(token string) string { if token == asciiModeTokenASCII { return "prefer_ascii" } return "prefer_entropy" } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/ascii_mode_test.go ================================================ package sudoku import "testing" func TestNormalizeASCIIMode(t *testing.T) { tests := []struct { in string want string }{ {"", "prefer_entropy"}, {"entropy", "prefer_entropy"}, {"prefer_ascii", "prefer_ascii"}, {"up_ascii_down_entropy", "up_ascii_down_entropy"}, {"up_entropy_down_ascii", "up_entropy_down_ascii"}, {"up_prefer_ascii_down_prefer_entropy", "up_ascii_down_entropy"}, } for _, tt := range tests { got, err := NormalizeASCIIMode(tt.in) if err != nil { t.Fatalf("NormalizeASCIIMode(%q): %v", tt.in, err) } if got != tt.want { t.Fatalf("NormalizeASCIIMode(%q) = %q, want %q", tt.in, got, tt.want) } } if _, err := NormalizeASCIIMode("up_ascii_down_binary"); err == nil { t.Fatalf("expected invalid directional mode to fail") } } func TestNewTableWithCustomDirectionalOpposite(t *testing.T) { table, err := NewTableWithCustom("seed", "up_ascii_down_entropy", "xpxvvpvv") if err != nil { t.Fatalf("NewTableWithCustom: %v", err) } if !table.IsASCII { t.Fatalf("uplink table should be ascii") } opposite := table.OppositeDirection() if opposite == nil || opposite == table { t.Fatalf("expected distinct opposite table") } if opposite.IsASCII { t.Fatalf("downlink table should be entropy/custom") } symmetric, err := NewTableWithCustom("seed", "prefer_ascii", "xpxvvpvv") if err != nil { t.Fatalf("NewTableWithCustom symmetric: %v", err) } if symmetric.OppositeDirection() != symmetric { t.Fatalf("symmetric table should point to itself") } } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/conn.go ================================================ package sudoku import ( "bufio" "bytes" "net" "sync" "sync/atomic" ) const IOBufferSize = 32 * 1024 var perm4 = [24][4]byte{ {0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1}, {1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0}, {2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0}, {3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}, } type Conn struct { net.Conn table *Table reader *bufio.Reader recorder *bytes.Buffer recording atomic.Bool recordLock sync.Mutex rawBuf []byte pendingData pendingBuffer hintBuf [4]byte hintCount int writeMu sync.Mutex writeBuf []byte rng randomSource paddingThreshold uint64 } func (sc *Conn) CloseWrite() error { if sc == nil || sc.Conn == nil { return nil } if cw, ok := sc.Conn.(interface{ CloseWrite() error }); ok { return cw.CloseWrite() } return nil } func (sc *Conn) CloseRead() error { if sc == nil || sc.Conn == nil { return nil } if cr, ok := sc.Conn.(interface{ CloseRead() error }); ok { return cr.CloseRead() } return nil } func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn { localRng := newSeededRand() sc := &Conn{ Conn: c, table: table, reader: bufio.NewReaderSize(c, IOBufferSize), rawBuf: make([]byte, IOBufferSize), pendingData: newPendingBuffer(4096), writeBuf: make([]byte, 0, 4096), rng: localRng, paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax), } if record { sc.recorder = new(bytes.Buffer) sc.recording.Store(true) } return sc } func (sc *Conn) StopRecording() { sc.recordLock.Lock() sc.recording.Store(false) sc.recorder = nil sc.recordLock.Unlock() } func (sc *Conn) GetBufferedAndRecorded() []byte { if sc == nil { return nil } sc.recordLock.Lock() defer sc.recordLock.Unlock() var recorded []byte if sc.recorder != nil { recorded = sc.recorder.Bytes() } buffered := sc.reader.Buffered() if buffered > 0 { peeked, _ := sc.reader.Peek(buffered) full := make([]byte, len(recorded)+len(peeked)) copy(full, recorded) copy(full[len(recorded):], peeked) return full } return recorded } func (sc *Conn) Write(p []byte) (n int, err error) { if len(p) == 0 { return 0, nil } sc.writeMu.Lock() defer sc.writeMu.Unlock() sc.writeBuf = encodeSudokuPayload(sc.writeBuf[:0], sc.table, sc.rng, sc.paddingThreshold, p) return len(p), writeFull(sc.Conn, sc.writeBuf) } func (sc *Conn) Read(p []byte) (n int, err error) { if n, ok := drainPending(p, &sc.pendingData); ok { return n, nil } for { if sc.pendingData.available() > 0 { break } nr, rErr := sc.reader.Read(sc.rawBuf) if nr > 0 { chunk := sc.rawBuf[:nr] if sc.recording.Load() { sc.recordLock.Lock() if sc.recording.Load() && sc.recorder != nil { sc.recorder.Write(chunk) } sc.recordLock.Unlock() } layout := sc.table.layout for _, b := range chunk { if !layout.hintTable[b] { continue } sc.hintBuf[sc.hintCount] = b sc.hintCount++ if sc.hintCount == len(sc.hintBuf) { key := packHintsToKey(sc.hintBuf) val, ok := sc.table.DecodeMap[key] if !ok { return 0, ErrInvalidSudokuMapMiss } sc.pendingData.appendByte(val) sc.hintCount = 0 } } } if rErr != nil { return 0, rErr } if sc.pendingData.available() > 0 { break } } n, _ = drainPending(p, &sc.pendingData) return n, nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/encode.go ================================================ package sudoku func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte { if len(p) == 0 { return dst[:0] } outCapacity := len(p)*6 + 1 if cap(dst) < outCapacity { dst = make([]byte, 0, outCapacity) } out := dst[:0] pads := table.PaddingPool padLen := len(pads) for _, b := range p { if shouldPad(rng, paddingThreshold) { out = append(out, pads[rng.Intn(padLen)]) } puzzles := table.EncodeTable[b] puzzle := puzzles[rng.Intn(len(puzzles))] perm := perm4[rng.Intn(len(perm4))] for _, idx := range perm { if shouldPad(rng, paddingThreshold) { out = append(out, pads[rng.Intn(padLen)]) } out = append(out, puzzle[idx]) } } if shouldPad(rng, paddingThreshold) { out = append(out, pads[rng.Intn(padLen)]) } return out } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/grid.go ================================================ package sudoku // Grid represents a 4x4 sudoku grid type Grid [16]uint8 // GenerateAllGrids generates all valid 4x4 Sudoku grids func GenerateAllGrids() []Grid { var grids []Grid var g Grid var backtrack func(int) backtrack = func(idx int) { if idx == 16 { grids = append(grids, g) return } row, col := idx/4, idx%4 br, bc := (row/2)*2, (col/2)*2 for num := uint8(1); num <= 4; num++ { valid := true for i := 0; i < 4; i++ { if g[row*4+i] == num || g[i*4+col] == num { valid = false break } } if valid { for r := 0; r < 2; r++ { for c := 0; c < 2; c++ { if g[(br+r)*4+(bc+c)] == num { valid = false break } } } } if valid { g[idx] = num backtrack(idx + 1) g[idx] = 0 } } } backtrack(0) return grids } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/layout.go ================================================ package sudoku import ( "fmt" "math/bits" "sort" "strings" ) type byteLayout struct { name string hintMask byte hintValue byte padMarker byte paddingPool []byte hintTable [256]bool encodeHint [4][16]byte encodeGroup [64]byte decodeGroup [256]byte groupValid [256]bool } func (l *byteLayout) isHint(b byte) bool { return l != nil && l.hintTable[b] } func (l *byteLayout) hintByte(val, pos byte) byte { return l.encodeHint[val&0x03][pos&0x0F] } func (l *byteLayout) groupByte(group byte) byte { return l.encodeGroup[group&0x3F] } func (l *byteLayout) decodePackedGroup(b byte) (byte, bool) { if l == nil { return 0, false } return l.decodeGroup[b], l.groupValid[b] } // resolveLayout picks the byte layout for a single traffic direction. // ASCII always wins if requested. Custom patterns are ignored when ASCII is preferred. func resolveLayout(mode string, customPattern string) (*byteLayout, error) { switch strings.ToLower(mode) { case "ascii", "prefer_ascii": return newASCIILayout(), nil case "entropy", "prefer_entropy", "": // fallback to entropy unless a custom pattern is provided default: return nil, fmt.Errorf("invalid ascii mode: %s", mode) } if strings.TrimSpace(customPattern) != "" { return newCustomLayout(customPattern) } return newEntropyLayout(), nil } func newASCIILayout() *byteLayout { padding := make([]byte, 0, 32) for i := 0; i < 32; i++ { padding = append(padding, byte(0x20+i)) } layout := &byteLayout{ name: "ascii", hintMask: 0x40, hintValue: 0x40, padMarker: 0x3F, paddingPool: padding, } for val := 0; val < 4; val++ { for pos := 0; pos < 16; pos++ { b := byte(0x40 | (byte(val) << 4) | byte(pos)) if b == 0x7F { b = '\n' } layout.encodeHint[val][pos] = b } } for group := 0; group < 64; group++ { b := byte(0x40 | byte(group)) if b == 0x7F { b = '\n' } layout.encodeGroup[group] = b } for b := 0; b < 256; b++ { wire := byte(b) if (wire & 0x40) == 0x40 { layout.hintTable[wire] = true layout.decodeGroup[wire] = wire & 0x3F layout.groupValid[wire] = true } } layout.hintTable['\n'] = true layout.decodeGroup['\n'] = 0x3F layout.groupValid['\n'] = true return layout } func newEntropyLayout() *byteLayout { padding := make([]byte, 0, 16) for i := 0; i < 8; i++ { padding = append(padding, byte(0x80+i)) padding = append(padding, byte(0x10+i)) } layout := &byteLayout{ name: "entropy", hintMask: 0x90, hintValue: 0x00, padMarker: 0x80, paddingPool: padding, } for val := 0; val < 4; val++ { for pos := 0; pos < 16; pos++ { layout.encodeHint[val][pos] = (byte(val) << 5) | byte(pos) } } for group := 0; group < 64; group++ { v := byte(group) layout.encodeGroup[group] = ((v & 0x30) << 1) | (v & 0x0F) } for b := 0; b < 256; b++ { wire := byte(b) if (wire & 0x90) != 0 { continue } layout.hintTable[wire] = true layout.decodeGroup[wire] = ((wire >> 1) & 0x30) | (wire & 0x0F) layout.groupValid[wire] = true } return layout } func newCustomLayout(pattern string) (*byteLayout, error) { cleaned := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(pattern), " ", "")) if len(cleaned) != 8 { return nil, fmt.Errorf("custom table must have 8 symbols, got %d", len(cleaned)) } var xBits, pBits, vBits []uint8 for i, c := range cleaned { bit := uint8(7 - i) switch c { case 'x': xBits = append(xBits, bit) case 'p': pBits = append(pBits, bit) case 'v': vBits = append(vBits, bit) default: return nil, fmt.Errorf("invalid char %q in custom table", c) } } if len(xBits) != 2 || len(pBits) != 2 || len(vBits) != 4 { return nil, fmt.Errorf("custom table must contain exactly 2 x, 2 p, 4 v") } xMask := byte(0) for _, b := range xBits { xMask |= 1 << b } encodeBits := func(val, pos byte, dropX int) byte { var out byte out |= xMask if dropX >= 0 { out &^= 1 << xBits[dropX] } if (val & 0x02) != 0 { out |= 1 << pBits[0] } if (val & 0x01) != 0 { out |= 1 << pBits[1] } for i, bit := range vBits { if (pos>>(3-uint8(i)))&0x01 == 1 { out |= 1 << bit } } return out } paddingSet := make(map[byte]struct{}) var padding []byte for drop := range xBits { for val := 0; val < 4; val++ { for pos := 0; pos < 16; pos++ { b := encodeBits(byte(val), byte(pos), drop) if bits.OnesCount8(b) >= 5 { if _, ok := paddingSet[b]; !ok { paddingSet[b] = struct{}{} padding = append(padding, b) } } } } } sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] }) if len(padding) == 0 { return nil, fmt.Errorf("custom table produced empty padding pool") } layout := &byteLayout{ name: fmt.Sprintf("custom(%s)", cleaned), hintMask: xMask, hintValue: xMask, padMarker: padding[0], paddingPool: padding, } for val := 0; val < 4; val++ { for pos := 0; pos < 16; pos++ { layout.encodeHint[val][pos] = encodeBits(byte(val), byte(pos), -1) } } for group := 0; group < 64; group++ { val := byte(group>>4) & 0x03 pos := byte(group) & 0x0F layout.encodeGroup[group] = encodeBits(val, pos, -1) } for b := 0; b < 256; b++ { wire := byte(b) if (wire & xMask) != xMask { continue } layout.hintTable[wire] = true var val, pos byte if wire&(1< packedProtectedPrefixBytes { limit = packedProtectedPrefixBytes } for padCount := 0; padCount < 1+pc.rng.Intn(2); padCount++ { out = pc.appendForcedPadding(out) } gap := pc.nextProtectedPrefixGap() effective := 0 for i := 0; i < limit; i++ { pc.bitBuf = (pc.bitBuf << 8) | uint64(p[i]) pc.bitCount += 8 for pc.bitCount >= 6 { pc.bitCount -= 6 group := byte(pc.bitBuf >> pc.bitCount) if pc.bitCount == 0 { pc.bitBuf = 0 } else { pc.bitBuf &= (1 << pc.bitCount) - 1 } out = pc.appendGroup(out, group&0x3F) } effective++ if effective >= gap { out = pc.appendForcedPadding(out) effective = 0 gap = pc.nextProtectedPrefixGap() } } return out, limit } func (pc *PackedConn) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } pc.writeMu.Lock() defer pc.writeMu.Unlock() needed := len(p)*3/2 + 32 if cap(pc.writeBuf) < needed { pc.writeBuf = make([]byte, 0, needed) } out := pc.writeBuf[:0] var prefixN int out, prefixN = pc.writeProtectedPrefix(out, p) i := prefixN n := len(p) for pc.bitCount > 0 && i < n { b := p[i] i++ pc.bitBuf = (pc.bitBuf << 8) | uint64(b) pc.bitCount += 8 for pc.bitCount >= 6 { pc.bitCount -= 6 group := byte(pc.bitBuf >> pc.bitCount) if pc.bitCount == 0 { pc.bitBuf = 0 } else { pc.bitBuf &= (1 << pc.bitCount) - 1 } out = pc.appendGroup(out, group&0x3F) } } for i+11 < n { for batch := 0; batch < 4; batch++ { b1, b2, b3 := p[i], p[i+1], p[i+2] i += 3 g1 := (b1 >> 2) & 0x3F g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F) g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03) g4 := b3 & 0x3F out = pc.appendGroup(out, g1) out = pc.appendGroup(out, g2) out = pc.appendGroup(out, g3) out = pc.appendGroup(out, g4) } } for i+2 < n { b1, b2, b3 := p[i], p[i+1], p[i+2] i += 3 g1 := (b1 >> 2) & 0x3F g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F) g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03) g4 := b3 & 0x3F out = pc.appendGroup(out, g1) out = pc.appendGroup(out, g2) out = pc.appendGroup(out, g3) out = pc.appendGroup(out, g4) } for ; i < n; i++ { b := p[i] pc.bitBuf = (pc.bitBuf << 8) | uint64(b) pc.bitCount += 8 for pc.bitCount >= 6 { pc.bitCount -= 6 group := byte(pc.bitBuf >> pc.bitCount) if pc.bitCount == 0 { pc.bitBuf = 0 } else { pc.bitBuf &= (1 << pc.bitCount) - 1 } out = pc.appendGroup(out, group&0x3F) } } if pc.bitCount > 0 { group := byte(pc.bitBuf << (6 - pc.bitCount)) pc.bitBuf = 0 pc.bitCount = 0 out = pc.appendGroup(out, group&0x3F) out = append(out, pc.padMarker) } out = pc.maybeAddPadding(out) if len(out) > 0 { pc.writeBuf = out[:0] return len(p), writeFull(pc.Conn, out) } pc.writeBuf = out[:0] return len(p), nil } func (pc *PackedConn) Flush() error { pc.writeMu.Lock() defer pc.writeMu.Unlock() out := pc.writeBuf[:0] if pc.bitCount > 0 { group := byte(pc.bitBuf << (6 - pc.bitCount)) pc.bitBuf = 0 pc.bitCount = 0 out = append(out, pc.table.layout.groupByte(group&0x3F)) out = append(out, pc.padMarker) } out = pc.maybeAddPadding(out) if len(out) > 0 { pc.writeBuf = out[:0] return writeFull(pc.Conn, out) } return nil } func writeFull(w io.Writer, b []byte) error { for len(b) > 0 { n, err := w.Write(b) if err != nil { return err } if n == 0 { return io.ErrShortWrite } b = b[n:] } return nil } func (pc *PackedConn) Read(p []byte) (int, error) { if n, ok := drainPending(p, &pc.pendingData); ok { return n, nil } for { nr, rErr := pc.reader.Read(pc.rawBuf) if nr > 0 { rBuf := pc.readBitBuf rBits := pc.readBits padMarker := pc.padMarker layout := pc.table.layout for _, b := range pc.rawBuf[:nr] { if !layout.hintTable[b] { if b == padMarker { rBuf = 0 rBits = 0 } continue } group, ok := layout.decodePackedGroup(b) if !ok { return 0, ErrInvalidSudokuMapMiss } rBuf = (rBuf << 6) | uint64(group) rBits += 6 if rBits >= 8 { rBits -= 8 val := byte(rBuf >> rBits) pc.pendingData.appendByte(val) if rBits == 0 { rBuf = 0 } else { rBuf &= (uint64(1) << rBits) - 1 } } } pc.readBitBuf = rBuf pc.readBits = rBits } if rErr != nil { if rErr == io.EOF { pc.readBitBuf = 0 pc.readBits = 0 } if pc.pendingData.available() > 0 { break } return 0, rErr } if pc.pendingData.available() > 0 { break } } n, _ := drainPending(p, &pc.pendingData) return n, nil } func (pc *PackedConn) getPaddingByte() byte { return pc.padPool[pc.rng.Intn(len(pc.padPool))] } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/packed_prefix_test.go ================================================ package sudoku import ( "bytes" "io" "math/rand" "net" "testing" "time" ) type mockConn struct { readBuf []byte writeBuf []byte } func (c *mockConn) Read(p []byte) (int, error) { if len(c.readBuf) == 0 { return 0, io.EOF } n := copy(p, c.readBuf) c.readBuf = c.readBuf[n:] return n, nil } func (c *mockConn) Write(p []byte) (int, error) { c.writeBuf = append(c.writeBuf, p...) return len(p), nil } func (c *mockConn) Close() error { return nil } func (c *mockConn) LocalAddr() net.Addr { return nil } func (c *mockConn) RemoteAddr() net.Addr { return nil } func (c *mockConn) SetDeadline(time.Time) error { return nil } func (c *mockConn) SetReadDeadline(time.Time) error { return nil } func (c *mockConn) SetWriteDeadline(time.Time) error { return nil } func TestPackedConn_ProtectedPrefixPadding(t *testing.T) { table := NewTable("packed-prefix-seed", "prefer_ascii") mock := &mockConn{} writer := NewPackedConn(mock, table, 0, 0) writer.rng = rand.New(rand.NewSource(1)) payload := bytes.Repeat([]byte{0}, 32) if _, err := writer.Write(payload); err != nil { t.Fatalf("write: %v", err) } wire := append([]byte(nil), mock.writeBuf...) if len(wire) < 20 { t.Fatalf("wire too short: %d", len(wire)) } firstHint := -1 nonHintCount := 0 maxHintRun := 0 currentHintRun := 0 for i, b := range wire[:20] { if table.layout.isHint(b) { if firstHint == -1 { firstHint = i } currentHintRun++ if currentHintRun > maxHintRun { maxHintRun = currentHintRun } continue } nonHintCount++ currentHintRun = 0 } if firstHint < 1 || firstHint > 2 { t.Fatalf("expected 1-2 leading padding bytes, first hint index=%d", firstHint) } if nonHintCount < 6 { t.Fatalf("expected dense prefix padding, got only %d non-hint bytes in first 20", nonHintCount) } if maxHintRun > 3 { t.Fatalf("prefix still exposes long hint run: %d", maxHintRun) } reader := NewPackedConn(&mockConn{readBuf: wire}, table, 0, 0) decoded := make([]byte, len(payload)) if _, err := io.ReadFull(reader, decoded); err != nil { t.Fatalf("read back: %v", err) } if !bytes.Equal(decoded, payload) { t.Fatalf("roundtrip mismatch") } } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/padding_prob.go ================================================ package sudoku const probOne = uint64(1) << 32 func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 { if r == nil { return 0 } if pMin < 0 { pMin = 0 } if pMax < pMin { pMax = pMin } if pMax > 100 { pMax = 100 } if pMin > 100 { pMin = 100 } min := uint64(pMin) * probOne / 100 max := uint64(pMax) * probOne / 100 if max <= min { return min } u := uint64(r.Uint32()) return min + (u * (max - min) >> 32) } func shouldPad(r randomSource, threshold uint64) bool { if threshold == 0 { return false } if threshold >= probOne { return true } if r == nil { return false } return uint64(r.Uint32()) < threshold } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/pending.go ================================================ package sudoku type pendingBuffer struct { data []byte off int } func newPendingBuffer(capacity int) pendingBuffer { return pendingBuffer{data: make([]byte, 0, capacity)} } func (p *pendingBuffer) available() int { if p == nil { return 0 } return len(p.data) - p.off } func (p *pendingBuffer) reset() { if p == nil { return } p.data = p.data[:0] p.off = 0 } func (p *pendingBuffer) ensureAppendCapacity(extra int) { if p == nil || extra <= 0 || p.off == 0 { return } if cap(p.data)-len(p.data) >= extra { return } unread := len(p.data) - p.off copy(p.data[:unread], p.data[p.off:]) p.data = p.data[:unread] p.off = 0 } func (p *pendingBuffer) appendByte(b byte) { p.ensureAppendCapacity(1) p.data = append(p.data, b) } func drainPending(dst []byte, pending *pendingBuffer) (int, bool) { if pending == nil || pending.available() == 0 { return 0, false } n := copy(dst, pending.data[pending.off:]) pending.off += n if pending.off == len(pending.data) { pending.reset() } return n, true } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/rand.go ================================================ package sudoku import ( crypto_rand "crypto/rand" "encoding/binary" "time" ) type randomSource interface { Uint32() uint32 Uint64() uint64 Intn(n int) int } type sudokuRand struct { state uint64 } func newSeededRand() *sudokuRand { seed := time.Now().UnixNano() var seedBytes [8]byte if _, err := crypto_rand.Read(seedBytes[:]); err == nil { seed = int64(binary.BigEndian.Uint64(seedBytes[:])) } return newSudokuRand(seed) } func newSudokuRand(seed int64) *sudokuRand { state := uint64(seed) if state == 0 { state = 0x9e3779b97f4a7c15 } return &sudokuRand{state: state} } func (r *sudokuRand) Uint64() uint64 { if r == nil { return 0 } r.state += 0x9e3779b97f4a7c15 z := r.state z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9 z = (z ^ (z >> 27)) * 0x94d049bb133111eb return z ^ (z >> 31) } func (r *sudokuRand) Uint32() uint32 { return uint32(r.Uint64() >> 32) } func (r *sudokuRand) Intn(n int) int { if n <= 1 { return 0 } return int((uint64(r.Uint32()) * uint64(n)) >> 32) } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/table.go ================================================ package sudoku import ( "crypto/sha256" "encoding/binary" "errors" "math/rand" "strings" ) var ( ErrInvalidSudokuMapMiss = errors.New("INVALID_SUDOKU_MAP_MISS") ) type Table struct { EncodeTable [256][][4]byte DecodeMap map[uint32]byte PaddingPool []byte IsASCII bool // 标记当前模式 layout *byteLayout opposite *Table hint uint32 } // NewTable initializes the obfuscation tables with built-in layouts. // Equivalent to calling NewTableWithCustom(key, mode, ""). func NewTable(key string, mode string) *Table { t, err := NewTableWithCustom(key, mode, "") if err != nil { panic(err) } return t } // NewTableWithCustom initializes the uplink/probe Sudoku table using either predefined // or directional layouts. Directional modes such as "up_ascii_down_entropy" return the // client->server table and internally attach the opposite direction table for runtime use. // The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive). func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) { asciiMode, err := ParseASCIIMode(mode) if err != nil { return nil, err } uplinkPattern := customPatternForToken(asciiMode.Uplink, customPattern) downlinkPattern := customPatternForToken(asciiMode.Downlink, customPattern) hint := tableHintFingerprint(key, asciiMode.Canonical(), uplinkPattern, downlinkPattern) uplink, err := newSingleDirectionTable(key, asciiMode.uplinkPreference(), uplinkPattern) if err != nil { return nil, err } uplink.hint = hint if asciiMode.Uplink == asciiMode.Downlink { uplink.opposite = uplink return uplink, nil } downlink, err := newSingleDirectionTable(key, asciiMode.downlinkPreference(), downlinkPattern) if err != nil { return nil, err } downlink.hint = hint uplink.opposite = downlink downlink.opposite = uplink return uplink, nil } func newSingleDirectionTable(key string, mode string, customPattern string) (*Table, error) { layout, err := resolveLayout(mode, customPattern) if err != nil { return nil, err } t := &Table{ DecodeMap: make(map[uint32]byte), IsASCII: layout.name == "ascii", layout: layout, } t.PaddingPool = append(t.PaddingPool, layout.paddingPool...) // 生成数独网格 (逻辑不变) allGrids := GenerateAllGrids() h := sha256.New() h.Write([]byte(key)) seed := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8])) rng := rand.New(rand.NewSource(seed)) shuffledGrids := make([]Grid, 288) copy(shuffledGrids, allGrids) rng.Shuffle(len(shuffledGrids), func(i, j int) { shuffledGrids[i], shuffledGrids[j] = shuffledGrids[j], shuffledGrids[i] }) // 预计算组合 var combinations [][]int var combine func(int, int, []int) combine = func(s, k int, c []int) { if k == 0 { tmp := make([]int, len(c)) copy(tmp, c) combinations = append(combinations, tmp) return } for i := s; i <= 16-k; i++ { c = append(c, i) combine(i+1, k-1, c) c = c[:len(c)-1] } } combine(0, 4, []int{}) // 构建映射表 for byteVal := 0; byteVal < 256; byteVal++ { targetGrid := shuffledGrids[byteVal] for _, positions := range combinations { var currentHints [4]byte // 1. 计算抽象提示 (Abstract Hints) // 我们先计算出 val 和 pos,后面再根据模式编码成 byte var rawParts [4]struct{ val, pos byte } for i, pos := range positions { val := targetGrid[pos] // 1..4 rawParts[i] = struct{ val, pos byte }{val, uint8(pos)} } // 检查唯一性 (数独逻辑) matchCount := 0 for _, g := range allGrids { match := true for _, p := range rawParts { if g[p.pos] != p.val { match = false break } } if match { matchCount++ if matchCount > 1 { break } } } if matchCount == 1 { // 唯一确定,生成最终编码字节 for i, p := range rawParts { currentHints[i] = t.layout.hintByte(p.val-1, p.pos) } t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints) // 生成解码键 (需要对 Hints 进行排序以忽略传输顺序) key := packHintsToKey(currentHints) t.DecodeMap[key] = byte(byteVal) } } } return t, nil } func customPatternForToken(token string, customPattern string) string { if token == asciiModeTokenEntropy { return customPattern } return "" } func (t *Table) OppositeDirection() *Table { if t == nil || t.opposite == nil { return t } return t.opposite } func (t *Table) Hint() uint32 { if t == nil { return 0 } return t.hint } func tableHintFingerprint(key string, mode string, uplinkPattern string, downlinkPattern string) uint32 { sum := sha256.Sum256([]byte(strings.Join([]string{ "sudoku-table-hint", key, mode, strings.ToLower(strings.TrimSpace(uplinkPattern)), strings.ToLower(strings.TrimSpace(downlinkPattern)), }, "\x00"))) return binary.BigEndian.Uint32(sum[:4]) } func packHintsToKey(hints [4]byte) uint32 { // Sorting network for 4 elements (Bubble sort unrolled) // Swap if a > b if hints[0] > hints[1] { hints[0], hints[1] = hints[1], hints[0] } if hints[2] > hints[3] { hints[2], hints[3] = hints[3], hints[2] } if hints[0] > hints[2] { hints[0], hints[2] = hints[2], hints[0] } if hints[1] > hints[3] { hints[1], hints[3] = hints[3], hints[1] } if hints[1] > hints[2] { hints[1], hints[2] = hints[2], hints[1] } return uint32(hints[0])<<24 | uint32(hints[1])<<16 | uint32(hints[2])<<8 | uint32(hints[3]) } ================================================ FILE: core/Clash.Meta/transport/sudoku/obfs/sudoku/table_set.go ================================================ package sudoku import "fmt" // TableSet is a small helper for managing multiple Sudoku tables (e.g. for per-connection rotation). // It is intentionally decoupled from the tunnel/app layers. type TableSet struct { Tables []*Table } // NewTableSet builds one or more tables from key/mode and a list of custom X/P/V patterns. // If patterns is empty, it builds a single default table (customPattern=""). func NewTableSet(key string, mode string, patterns []string) (*TableSet, error) { if len(patterns) == 0 { t, err := NewTableWithCustom(key, mode, "") if err != nil { return nil, err } return &TableSet{Tables: []*Table{t}}, nil } tables := make([]*Table, 0, len(patterns)) for i, pattern := range patterns { t, err := NewTableWithCustom(key, mode, pattern) if err != nil { return nil, fmt.Errorf("build table[%d] (%q): %w", i, pattern, err) } tables = append(tables, t) } return &TableSet{Tables: tables}, nil } func (ts *TableSet) Candidates() []*Table { if ts == nil { return nil } return ts.Tables } ================================================ FILE: core/Clash.Meta/transport/sudoku/replay.go ================================================ package sudoku import ( "sync" "time" ) var handshakeReplayTTL = 60 * time.Second type nonceSet struct { mu sync.Mutex m map[[kipHelloNonceSize]byte]time.Time maxEntries int lastPrune time.Time } func newNonceSet(maxEntries int) *nonceSet { if maxEntries <= 0 { maxEntries = 4096 } return &nonceSet{ m: make(map[[kipHelloNonceSize]byte]time.Time), maxEntries: maxEntries, } } func (s *nonceSet) allow(nonce [kipHelloNonceSize]byte, now time.Time, ttl time.Duration) bool { s.mu.Lock() defer s.mu.Unlock() if ttl <= 0 { ttl = 60 * time.Second } if now.Sub(s.lastPrune) > ttl/2 || len(s.m) > s.maxEntries { for k, exp := range s.m { if !now.Before(exp) { delete(s.m, k) } } s.lastPrune = now for len(s.m) > s.maxEntries { for k := range s.m { delete(s.m, k) break } } } if exp, ok := s.m[nonce]; ok && now.Before(exp) { return false } s.m[nonce] = now.Add(ttl) return true } type handshakeReplayProtector struct { users sync.Map // map[userHash string]*nonceSet } func (p *handshakeReplayProtector) allow(userHash string, nonce [kipHelloNonceSize]byte, now time.Time) bool { if userHash == "" { userHash = "_" } val, _ := p.users.LoadOrStore(userHash, newNonceSet(4096)) set, ok := val.(*nonceSet) if !ok || set == nil { set = newNonceSet(4096) p.users.Store(userHash, set) } return set.allow(nonce, now, handshakeReplayTTL) } var globalHandshakeReplay = &handshakeReplayProtector{} ================================================ FILE: core/Clash.Meta/transport/sudoku/session_keys.go ================================================ package sudoku import ( "crypto/ecdh" "crypto/sha256" "fmt" "io" "golang.org/x/crypto/hkdf" ) func derivePSKDirectionalBases(psk string) (c2s, s2c []byte) { sum := sha256.Sum256([]byte(psk)) c2sKey := make([]byte, 32) s2cKey := make([]byte, 32) if _, err := io.ReadFull(hkdf.Expand(sha256.New, sum[:], []byte("sudoku-psk-c2s")), c2sKey); err != nil { panic("sudoku: hkdf expand failed") } if _, err := io.ReadFull(hkdf.Expand(sha256.New, sum[:], []byte("sudoku-psk-s2c")), s2cKey); err != nil { panic("sudoku: hkdf expand failed") } return c2sKey, s2cKey } func deriveSessionDirectionalBases(psk string, shared []byte, nonce [kipHelloNonceSize]byte) (c2s, s2c []byte, err error) { sum := sha256.Sum256([]byte(psk)) ikm := make([]byte, 0, len(shared)+len(nonce)) ikm = append(ikm, shared...) ikm = append(ikm, nonce[:]...) prk := hkdf.Extract(sha256.New, ikm, sum[:]) c2sKey := make([]byte, 32) s2cKey := make([]byte, 32) if _, err := io.ReadFull(hkdf.Expand(sha256.New, prk, []byte("sudoku-session-c2s")), c2sKey); err != nil { return nil, nil, fmt.Errorf("hkdf expand c2s: %w", err) } if _, err := io.ReadFull(hkdf.Expand(sha256.New, prk, []byte("sudoku-session-s2c")), s2cKey); err != nil { return nil, nil, fmt.Errorf("hkdf expand s2c: %w", err) } return c2sKey, s2cKey, nil } func x25519SharedSecret(priv *ecdh.PrivateKey, peerPub []byte) ([]byte, error) { if priv == nil { return nil, fmt.Errorf("nil priv") } curve := ecdh.X25519() pk, err := curve.NewPublicKey(peerPub) if err != nil { return nil, fmt.Errorf("parse peer pub: %w", err) } secret, err := priv.ECDH(pk) if err != nil { return nil, fmt.Errorf("ecdh: %w", err) } return secret, nil } ================================================ FILE: core/Clash.Meta/transport/sudoku/table_probe.go ================================================ package sudoku import ( "bufio" "bytes" crand "crypto/rand" "errors" "fmt" "io" "net" "time" "github.com/metacubex/mihomo/transport/sudoku/crypto" "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) type clientTableChoice struct { Table *sudoku.Table Hint uint32 HasHint bool } func pickClientTable(cfg *ProtocolConfig) (clientTableChoice, error) { candidates := cfg.tableCandidates() if len(candidates) == 0 { return clientTableChoice{}, fmt.Errorf("no table configured") } if len(candidates) == 1 { return clientTableChoice{Table: candidates[0], Hint: candidates[0].Hint()}, nil } var b [1]byte if _, err := crand.Read(b[:]); err != nil { return clientTableChoice{}, fmt.Errorf("random table pick failed: %w", err) } idx := int(b[0]) % len(candidates) return clientTableChoice{Table: candidates[idx], Hint: candidates[idx].Hint(), HasHint: true}, nil } type readOnlyConn struct { *bytes.Reader } func (c *readOnlyConn) Write([]byte) (int, error) { return 0, io.ErrClosedPipe } func (c *readOnlyConn) Close() error { return nil } func (c *readOnlyConn) LocalAddr() net.Addr { return nil } func (c *readOnlyConn) RemoteAddr() net.Addr { return nil } func (c *readOnlyConn) SetDeadline(time.Time) error { return nil } func (c *readOnlyConn) SetReadDeadline(time.Time) error { return nil } func (c *readOnlyConn) SetWriteDeadline(time.Time) error { return nil } func drainBuffered(r *bufio.Reader) ([]byte, error) { n := r.Buffered() if n <= 0 { return nil, nil } out := make([]byte, n) _, err := io.ReadFull(r, out) return out, err } func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error { rc := &readOnlyConn{Reader: bytes.NewReader(probe)} _, obfsConn := buildServerObfsConn(rc, cfg, table, false) seed := ServerAEADSeed(cfg.Key) pskC2S, pskS2C := derivePSKDirectionalBases(seed) // Server side: recv is client->server, send is server->client. cConn, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskS2C, pskC2S) if err != nil { return err } msg, err := ReadKIPMessage(cConn) if err != nil { return err } if msg.Type != KIPTypeClientHello { return fmt.Errorf("unexpected handshake message: %d", msg.Type) } ch, err := DecodeKIPClientHelloPayload(msg.Payload) if err != nil { return err } if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(kipHandshakeSkew.Seconds()) { return fmt.Errorf("time skew/replay") } return nil } func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) { const ( maxProbeBytes = 64 * 1024 readChunk = 4 * 1024 ) if len(tables) == 0 { return nil, nil, fmt.Errorf("no table candidates") } if len(tables) > 255 { return nil, nil, fmt.Errorf("too many table candidates: %d", len(tables)) } // Copy so we can prune candidates without mutating the caller slice. candidates := make([]*sudoku.Table, 0, len(tables)) for _, t := range tables { if t != nil { candidates = append(candidates, t) } } if len(candidates) == 0 { return nil, nil, fmt.Errorf("no table candidates") } probe, err := drainBuffered(r) if err != nil { return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err) } tmp := make([]byte, readChunk) for { if len(candidates) == 1 { tail, err := drainBuffered(r) if err != nil { return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err) } probe = append(probe, tail...) return candidates[0], probe, nil } needMore := false next := candidates[:0] for _, table := range candidates { err := probeHandshakeBytes(probe, cfg, table) if err == nil { tail, err := drainBuffered(r) if err != nil { return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err) } probe = append(probe, tail...) return table, probe, nil } if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { needMore = true next = append(next, table) } // Definitive mismatch: drop table. } candidates = next if len(candidates) == 0 || !needMore { return nil, probe, fmt.Errorf("handshake table selection failed") } if len(probe) >= maxProbeBytes { return nil, probe, fmt.Errorf("handshake probe exceeded %d bytes", maxProbeBytes) } n, err := r.Read(tmp) if n > 0 { probe = append(probe, tmp[:n]...) } if err != nil { return nil, probe, fmt.Errorf("handshake probe read failed: %w", err) } } } ================================================ FILE: core/Clash.Meta/transport/sudoku/tables.go ================================================ package sudoku import ( "strings" "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" ) func normalizeCustomPatterns(customTable string, customTables []string) []string { patterns := customTables if len(patterns) == 0 && strings.TrimSpace(customTable) != "" { patterns = []string{customTable} } if len(patterns) == 0 { patterns = []string{""} } return patterns } func normalizeTablePatterns(tableType string, customTable string, customTables []string) ([]string, error) { patterns := normalizeCustomPatterns(customTable, customTables) if _, err := sudoku.ParseASCIIMode(tableType); err != nil { return nil, err } return patterns, nil } // NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns. // When customTables is non-empty it overrides customTable (matching upstream Sudoku behavior). // // Deprecated-ish: prefer NewClientTablesWithCustomPatterns / NewServerTablesWithCustomPatterns. func NewTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) { patterns, err := normalizeTablePatterns(tableType, customTable, customTables) if err != nil { return nil, err } tables := make([]*sudoku.Table, 0, len(patterns)) for _, pattern := range patterns { pattern = strings.TrimSpace(pattern) t, err := NewTableWithCustom(key, tableType, pattern) if err != nil { return nil, err } tables = append(tables, t) } return tables, nil } func NewClientTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) { return NewTablesWithCustomPatterns(key, tableType, customTable, customTables) } // NewServerTablesWithCustomPatterns matches upstream server behavior: when probeable custom table // rotation is enabled, also accept the default table to avoid forcing clients to update in lockstep. func NewServerTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) { patterns, err := normalizeTablePatterns(tableType, customTable, customTables) if err != nil { return nil, err } asciiMode, err := sudoku.ParseASCIIMode(tableType) if err != nil { return nil, err } if asciiMode.Uplink == "entropy" && len(patterns) > 0 && strings.TrimSpace(patterns[0]) != "" { patterns = append([]string{""}, patterns...) } return NewTablesWithCustomPatterns(key, tableType, "", patterns) } ================================================ FILE: core/Clash.Meta/transport/sudoku/tables_directional_test.go ================================================ package sudoku import "testing" func TestDirectionalCustomTableRotationCollapse(t *testing.T) { patterns := []string{"xpxvvpvv", "vxpvxvvp"} clientTables, err := NewClientTablesWithCustomPatterns("seed", "up_ascii_down_entropy", "", patterns) if err != nil { t.Fatalf("client tables: %v", err) } if len(clientTables) != 2 { t.Fatalf("expected ascii-uplink directional rotation to keep 2 tables, got %d", len(clientTables)) } if clientTables[0].Hint() == clientTables[1].Hint() { t.Fatalf("expected directional custom tables to carry distinct hints") } if got, want := clientTables[0].EncodeTable[0][0], clientTables[1].EncodeTable[0][0]; got != want { t.Fatalf("expected directional ascii uplink tables to share the same probe layout, got %x want %x", got, want) } if got, want := clientTables[0].OppositeDirection().EncodeTable[0][0], clientTables[1].OppositeDirection().EncodeTable[0][0]; got == want { t.Fatalf("expected directional downlink custom layouts to differ, both got %x", got) } clientTables, err = NewClientTablesWithCustomPatterns("seed", "up_entropy_down_ascii", "", patterns) if err != nil { t.Fatalf("client tables entropy uplink: %v", err) } if len(clientTables) != 2 { t.Fatalf("expected entropy-uplink rotation to keep 2 tables, got %d", len(clientTables)) } serverTables, err := NewServerTablesWithCustomPatterns("seed", "up_ascii_down_entropy", "", patterns) if err != nil { t.Fatalf("server tables: %v", err) } if len(serverTables) != 2 { t.Fatalf("expected ascii-uplink server directional table set to keep 2 tables, got %d", len(serverTables)) } if clientTables, err = NewClientTablesWithCustomPatterns("seed", "up_ascii_down_entropy", patterns[0], nil); err != nil { t.Fatalf("client table with single custom pattern: %v", err) } else if got, want := serverTables[0].OppositeDirection().EncodeTable[0][0], clientTables[0].OppositeDirection().EncodeTable[0][0]; got != want { t.Fatalf("expected server directional downlink table to preserve custom pattern, got %x want %x", got, want) } } ================================================ FILE: core/Clash.Meta/transport/sudoku/uot.go ================================================ package sudoku import ( "bytes" "encoding/binary" "errors" "fmt" "io" "net" "net/netip" "sync" "time" "github.com/metacubex/mihomo/log" ) const ( maxUoTPayload = 64 * 1024 ) // WriteDatagram sends a single UDP datagram frame over a reliable stream. func WriteDatagram(w io.Writer, addr string, payload []byte) error { addrBuf, err := EncodeAddress(addr) if err != nil { return fmt.Errorf("encode address: %w", err) } if addrLen := len(addrBuf); addrLen == 0 || addrLen > maxUoTPayload { return fmt.Errorf("address too long: %d", len(addrBuf)) } if payloadLen := len(payload); payloadLen > maxUoTPayload { return fmt.Errorf("payload too large: %d", payloadLen) } var header [4]byte binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf))) binary.BigEndian.PutUint16(header[2:], uint16(len(payload))) return writeAllChunks(w, header[:], addrBuf, payload) } // ReadDatagram parses a single UDP datagram frame from the reliable stream. func ReadDatagram(r io.Reader) (string, []byte, error) { addr, payloadLen, err := readDatagramHeaderAndAddress(r) if err != nil { return "", nil, err } payload := make([]byte, payloadLen) if _, err := io.ReadFull(r, payload); err != nil { return "", nil, err } return addr, payload, nil } // UoTPacketConn adapts a net.Conn with the Sudoku UoT framing to net.PacketConn. type UoTPacketConn struct { conn net.Conn writeMu sync.Mutex } func NewUoTPacketConn(conn net.Conn) *UoTPacketConn { return &UoTPacketConn{conn: conn} } func (c *UoTPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { for { addrStr, payloadLen, err := readDatagramHeaderAndAddress(c.conn) if err != nil { return 0, nil, err } udpAddr, err := parseDatagramUDPAddr(addrStr) if payloadLen > len(p) { if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil { return 0, nil, discardErr } return 0, nil, io.ErrShortBuffer } if err != nil { if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil { return 0, nil, discardErr } log.Debugln("[Sudoku][UoT] discard datagram with invalid address %s: %v", addrStr, err) continue } if _, err := io.ReadFull(c.conn, p[:payloadLen]); err != nil { return 0, nil, err } return payloadLen, udpAddr, nil } } func (c *UoTPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { if addr == nil { return 0, errors.New("address is nil") } c.writeMu.Lock() defer c.writeMu.Unlock() if err := WriteDatagram(c.conn, addr.String(), p); err != nil { return 0, err } return len(p), nil } func (c *UoTPacketConn) Close() error { return c.conn.Close() } func (c *UoTPacketConn) LocalAddr() net.Addr { return c.conn.LocalAddr() } func (c *UoTPacketConn) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) } func (c *UoTPacketConn) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) } func (c *UoTPacketConn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) } func readDatagramHeaderAndAddress(r io.Reader) (string, int, error) { var header [4]byte if _, err := io.ReadFull(r, header[:]); err != nil { return "", 0, err } addrLen := int(binary.BigEndian.Uint16(header[:2])) payloadLen := int(binary.BigEndian.Uint16(header[2:])) if addrLen <= 0 || addrLen > maxUoTPayload { return "", 0, fmt.Errorf("invalid address length: %d", addrLen) } if payloadLen < 0 || payloadLen > maxUoTPayload { return "", 0, fmt.Errorf("invalid payload length: %d", payloadLen) } addrBuf := make([]byte, addrLen) if _, err := io.ReadFull(r, addrBuf); err != nil { return "", 0, err } addr, err := DecodeAddress(bytes.NewReader(addrBuf)) if err != nil { return "", 0, fmt.Errorf("decode address: %w", err) } return addr, payloadLen, nil } func parseDatagramUDPAddr(addr string) (*net.UDPAddr, error) { addrPort, err := netip.ParseAddrPort(addr) if err != nil { return nil, err } return net.UDPAddrFromAddrPort(netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())), nil } func discardBytes(r io.Reader, n int) error { if n <= 0 { return nil } _, err := io.CopyN(io.Discard, r, int64(n)) return err } ================================================ FILE: core/Clash.Meta/transport/sudoku/write_chunks.go ================================================ package sudoku import "io" func writeAllChunks(w io.Writer, chunks ...[]byte) error { for _, chunk := range chunks { for len(chunk) > 0 { n, err := w.Write(chunk) if err != nil { return err } if n == 0 { return io.ErrShortWrite } chunk = chunk[n:] } } return nil } ================================================ FILE: core/Clash.Meta/transport/trojan/trojan.go ================================================ package trojan import ( "crypto/sha256" "encoding/binary" "encoding/hex" "errors" "io" "net" "sync" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/transport/socks5" ) const ( // max packet length maxLength = 8192 ) var ( DefaultALPN = []string{"h2", "http/1.1"} DefaultWebsocketALPN = []string{"http/1.1"} crlf = []byte{'\r', '\n'} ) type Command = byte const ( CommandTCP byte = 1 CommandUDP byte = 3 CommandMux byte = 0x7f KeyLength = 56 ) func WriteHeader(w io.Writer, hexPassword [KeyLength]byte, command Command, socks5Addr []byte) error { buf := pool.GetBuffer() defer pool.PutBuffer(buf) buf.Write(hexPassword[:]) buf.Write(crlf) buf.WriteByte(command) buf.Write(socks5Addr) buf.Write(crlf) _, err := w.Write(buf.Bytes()) return err } func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { buf := pool.GetBuffer() defer pool.PutBuffer(buf) buf.Write(socks5Addr) binary.Write(buf, binary.BigEndian, uint16(len(payload))) buf.Write(crlf) buf.Write(payload) return w.Write(buf.Bytes()) } func WritePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { if len(payload) <= maxLength { return writePacket(w, socks5Addr, payload) } offset := 0 total := len(payload) for { cursor := offset + maxLength if cursor > total { cursor = total } n, err := writePacket(w, socks5Addr, payload[offset:cursor]) if err != nil { return offset + n, err } offset = cursor if offset == total { break } } return total, nil } func ReadPacket(r io.Reader, payload []byte) (net.Addr, int, int, error) { addr, err := socks5.ReadAddr(r, payload) if err != nil { return nil, 0, 0, errors.New("read addr error") } uAddr := addr.UDPAddr() if uAddr == nil { return nil, 0, 0, errors.New("parse addr error") } if _, err = io.ReadFull(r, payload[:2]); err != nil { return nil, 0, 0, errors.New("read length error") } total := int(binary.BigEndian.Uint16(payload[:2])) if total > maxLength { return nil, 0, 0, errors.New("packet invalid") } // read crlf if _, err = io.ReadFull(r, payload[:2]); err != nil { return nil, 0, 0, errors.New("read crlf error") } length := len(payload) if total < length { length = total } if _, err = io.ReadFull(r, payload[:length]); err != nil { return nil, 0, 0, errors.New("read packet error") } return uAddr, length, total - length, nil } var _ N.EnhancePacketConn = (*PacketConn)(nil) type PacketConn struct { net.Conn remain int rAddr net.Addr mux sync.Mutex } func (pc *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { return WritePacket(pc, socks5.ParseAddrToSocksAddr(addr), b) } func (pc *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { pc.mux.Lock() defer pc.mux.Unlock() if pc.remain != 0 { length := len(b) if pc.remain < length { length = pc.remain } n, err := pc.Conn.Read(b[:length]) if err != nil { return 0, nil, err } pc.remain -= n addr := pc.rAddr if pc.remain == 0 { pc.rAddr = nil } return n, addr, nil } addr, n, remain, err := ReadPacket(pc.Conn, b) if err != nil { return 0, nil, err } if remain != 0 { pc.remain = remain pc.rAddr = addr } return n, addr, nil } func (pc *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { pc.mux.Lock() defer pc.mux.Unlock() destination, err := socks5.ReadAddr0(pc.Conn) if err != nil { return nil, nil, nil, err } udpAddr := destination.UDPAddr() if udpAddr == nil { return nil, nil, nil, errors.New("parse addr error") } addr = udpAddr data = pool.Get(pool.UDPBufferSize) put = func() { _ = pool.Put(data) } _, err = io.ReadFull(pc.Conn, data[:2+2]) // u16be length + CR LF if err != nil { if put != nil { put() } return nil, nil, nil, err } length := binary.BigEndian.Uint16(data) if length > 0 { data = data[:length] _, err = io.ReadFull(pc.Conn, data) if err != nil { if put != nil { put() } return nil, nil, nil, err } } else { if put != nil { put() } return nil, nil, addr, nil } return } func NewPacketConn(conn net.Conn) *PacketConn { return &PacketConn{Conn: conn} } func Key(password string) (key [56]byte) { hash := sha256.Sum224([]byte(password)) hex.Encode(key[:], hash[:]) return } ================================================ FILE: core/Clash.Meta/transport/trusttunnel/client.go ================================================ package trusttunnel import ( "context" "errors" "fmt" "io" "net" "net/url" "sync" "sync/atomic" "time" "github.com/metacubex/mihomo/common/httputils" "github.com/metacubex/mihomo/common/once" "github.com/metacubex/mihomo/component/dialer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" "golang.org/x/exp/slices" ) type DialOptionsFunc func() []dialer.Option type ClientOptions struct { Dialer C.Dialer DialOptions DialOptionsFunc // for quic Server string Username string Password string TLSConfig *vmess.TLSConfig QUIC bool QUICCongestionControl string QUICCwnd int QUICBBRProfile string HealthCheck bool MaxConnections int MinStreams int MaxStreams int } type Client struct { ctx context.Context dialer C.Dialer dialOptions DialOptionsFunc server string auth string roundTripper http.RoundTripper startOnce sync.Once healthCheck bool healthCheckTimer *time.Timer count atomic.Int64 } func NewClient(ctx context.Context, options ClientOptions) (client *Client, err error) { client = &Client{ ctx: ctx, dialer: options.Dialer, dialOptions: options.DialOptions, server: options.Server, auth: buildAuth(options.Username, options.Password), } if options.QUIC { if len(options.TLSConfig.NextProtos) == 0 { options.TLSConfig.NextProtos = []string{"h3"} } else if !slices.Contains(options.TLSConfig.NextProtos, "h3") { return nil, errors.New("require alpn h3") } err = client.quicRoundTripper(options.TLSConfig, options.QUICCongestionControl, options.QUICCwnd, options.QUICBBRProfile) if err != nil { return nil, err } } else { if len(options.TLSConfig.NextProtos) == 0 { options.TLSConfig.NextProtos = []string{"h2"} } else if !slices.Contains(options.TLSConfig.NextProtos, "h2") { return nil, errors.New("require alpn h2") } client.h2RoundTripper(options.TLSConfig) } if options.HealthCheck { client.healthCheck = true } return client, nil } func (c *Client) h2RoundTripper(tlsConfig *vmess.TLSConfig) { // use h2c mode to disallow the net/http fallback to http1.1 protocols := new(http.Protocols) protocols.SetUnencryptedHTTP2(true) c.roundTripper = &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { conn, err := c.dialer.DialContext(ctx, network, c.server) if err != nil { return nil, err } tlsConn, err := vmess.StreamTLSConn(ctx, conn, tlsConfig) if err != nil { _ = conn.Close() return nil, err } return tlsConn, nil }, Protocols: protocols, IdleConnTimeout: DefaultSessionTimeout, } } func (c *Client) start() { if c.healthCheck { c.healthCheckTimer = time.NewTimer(DefaultHealthCheckTimeout) go c.loopHealthCheck() } } func (c *Client) loopHealthCheck() { for { select { case <-c.healthCheckTimer.C: case <-c.ctx.Done(): c.healthCheckTimer.Stop() return } ctx, cancel := context.WithTimeout(c.ctx, DefaultHealthCheckTimeout) _ = c.HealthCheck(ctx) cancel() } } func (c *Client) resetHealthCheckTimer() { if c.healthCheckTimer == nil { return } c.healthCheckTimer.Reset(DefaultHealthCheckTimeout) } func (c *Client) roundTrip(request *http.Request, conn *httpConn) { c.startOnce.Do(c.start) pipeReader, pipeWriter := io.Pipe() request.Body = pipeReader *conn = httpConn{ writer: pipeWriter, created: make(chan struct{}), } c.count.Add(1) conn.closeFn = once.OnceFunc(func() { c.count.Add(-1) }) ctx, cancel := context.WithCancel(c.ctx) // requestCtx must alive during conn not closed conn.cancelFn = cancel // cancel ctx when conn closed go func() { timeout := time.AfterFunc(C.DefaultTCPTimeout, cancel) // only cancel when RoundTrip timeout defer timeout.Stop() // RoundTrip already returned, stop the timer request = request.WithContext(httputils.NewAddrContext(&conn.NetAddr, ctx)) response, err := c.roundTripper.RoundTrip(request) if err != nil { _ = pipeWriter.CloseWithError(err) _ = pipeReader.CloseWithError(err) conn.setup(nil, err) } else if response.StatusCode != http.StatusOK { _ = response.Body.Close() err = fmt.Errorf("unexpected status code: %d", response.StatusCode) _ = pipeWriter.CloseWithError(err) _ = pipeReader.CloseWithError(err) conn.setup(nil, err) } else { c.resetHealthCheckTimer() conn.setup(response.Body, nil) } }() } func (c *Client) newConnectRequest(host, userAgent string) *http.Request { request := &http.Request{ Method: http.MethodConnect, URL: &url.URL{ Scheme: "https", Host: c.server, // Use the proxy server authority so the pool keys reuse against the actual proxy endpoint. }, Header: make(http.Header), Host: host, // Send the actual CONNECT target as the Host header (:authority). } request.Header.Add("User-Agent", userAgent) request.Header.Add("Proxy-Authorization", c.auth) return request } func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) { request := c.newConnectRequest(host, TCPUserAgent) conn := &tcpConn{} c.roundTrip(request, &conn.httpConn) return conn, nil } func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) { request := c.newConnectRequest(UDPMagicAddress, UDPUserAgent) conn := &clientPacketConn{} c.roundTrip(request, &conn.httpConn) return conn, nil } func (c *Client) ListenICMP(ctx context.Context) (*IcmpConn, error) { request := c.newConnectRequest(ICMPMagicAddress, ICMPUserAgent) conn := &IcmpConn{} c.roundTrip(request, &conn.httpConn) return conn, nil } func (c *Client) Close() error { httputils.CloseTransport(c.roundTripper) if c.healthCheckTimer != nil { c.healthCheckTimer.Stop() } return nil } func (c *Client) ResetConnections() { httputils.CloseTransport(c.roundTripper) c.resetHealthCheckTimer() } func (c *Client) HealthCheck(ctx context.Context) error { defer c.resetHealthCheckTimer() request := c.newConnectRequest(HealthCheckMagicAddress, HealthCheckUserAgent) response, err := c.roundTripper.RoundTrip(request.WithContext(ctx)) if err != nil { return err } defer response.Body.Close() if response.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status code: %d", response.StatusCode) } return nil } type PoolClient struct { mutex sync.Mutex maxConnections int minStreams int maxStreams int ctx context.Context options ClientOptions clients []*Client } func NewPoolClient(ctx context.Context, options ClientOptions) (*PoolClient, error) { maxConnections := options.MaxConnections minStreams := options.MinStreams maxStreams := options.MaxStreams if maxConnections == 0 && minStreams == 0 && maxStreams == 0 { maxConnections = 8 minStreams = 5 } client, err := NewClient(ctx, options) // reserve one client and verify the configuration if err != nil { return nil, err } return &PoolClient{ maxConnections: maxConnections, minStreams: minStreams, maxStreams: maxStreams, ctx: ctx, options: options, clients: []*Client{client}, }, nil } func (c *PoolClient) Dial(ctx context.Context, host string) (net.Conn, error) { transport, err := c.getClient() if err != nil { return nil, err } return transport.Dial(ctx, host) } func (c *PoolClient) ListenPacket(ctx context.Context) (net.PacketConn, error) { transport, err := c.getClient() if err != nil { return nil, err } return transport.ListenPacket(ctx) } func (c *PoolClient) ListenICMP(ctx context.Context) (*IcmpConn, error) { transport, err := c.getClient() if err != nil { return nil, err } return transport.ListenICMP(ctx) } func (c *PoolClient) Close() error { c.mutex.Lock() defer c.mutex.Unlock() var errs []error for _, t := range c.clients { if err := t.Close(); err != nil { errs = append(errs, err) } } c.clients = nil return errors.Join(errs...) } func (c *PoolClient) getClient() (*Client, error) { c.mutex.Lock() defer c.mutex.Unlock() var transport *Client for _, t := range c.clients { if transport == nil || t.count.Load() < transport.count.Load() { transport = t } } if transport == nil { return c.newTransportLocked() } numStreams := int(transport.count.Load()) if numStreams == 0 { return transport, nil } if c.maxConnections > 0 { if len(c.clients) >= c.maxConnections || numStreams < c.minStreams { return transport, nil } } else { if c.maxStreams > 0 && numStreams < c.maxStreams { return transport, nil } } return c.newTransportLocked() } func (c *PoolClient) newTransportLocked() (*Client, error) { transport, err := NewClient(c.ctx, c.options) if err != nil { return nil, err } c.clients = append(c.clients, transport) return transport, nil } ================================================ FILE: core/Clash.Meta/transport/trusttunnel/doc.go ================================================ // Package trusttunnel copy and modify from: // https://github.com/xchacha20-poly1305/sing-trusttunnel/tree/v0.1.1 // adopt for mihomo package trusttunnel ================================================ FILE: core/Clash.Meta/transport/trusttunnel/icmp.go ================================================ package trusttunnel import ( "encoding/binary" "net/netip" "github.com/metacubex/mihomo/common/buf" ) type IcmpConn struct { httpConn } func (i *IcmpConn) WritePing(id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16) error { request := buf.NewSize(2 + 16 + 2 + 1 + 2) defer request.Release() buf.Must(binary.Write(request, binary.BigEndian, id)) destinationAddress := buildPaddingIP(destination) buf.Must1(request.Write(destinationAddress[:])) buf.Must(binary.Write(request, binary.BigEndian, sequenceNumber)) buf.Must(binary.Write(request, binary.BigEndian, ttl)) buf.Must(binary.Write(request, binary.BigEndian, size)) return buf.Error(i.writeFlush(request.Bytes())) } func (i *IcmpConn) ReadPing() (id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16, err error) { err = i.waitCreated() if err != nil { return } response := buf.NewSize(2 + 16 + 1 + 1 + 2) defer response.Release() _, err = response.ReadFullFrom(i.body, response.Cap()) if err != nil { return } buf.Must(binary.Read(response, binary.BigEndian, &id)) var sourceAddressBuffer [16]byte buf.Must1(response.Read(sourceAddressBuffer[:])) sourceAddress = parse16BytesIP(sourceAddressBuffer) buf.Must(binary.Read(response, binary.BigEndian, &icmpType)) buf.Must(binary.Read(response, binary.BigEndian, &code)) buf.Must(binary.Read(response, binary.BigEndian, &sequenceNumber)) return } func (i *IcmpConn) Close() error { return i.httpConn.Close() } func (i *IcmpConn) ReadPingRequest() (id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16, err error) { err = i.waitCreated() if err != nil { return } request := buf.NewSize(2 + 16 + 2 + 1 + 2) defer request.Release() _, err = request.ReadFullFrom(i.body, request.Cap()) if err != nil { return } buf.Must(binary.Read(request, binary.BigEndian, &id)) var destinationAddressBuffer [16]byte buf.Must1(request.Read(destinationAddressBuffer[:])) destination = parse16BytesIP(destinationAddressBuffer) buf.Must(binary.Read(request, binary.BigEndian, &sequenceNumber)) buf.Must(binary.Read(request, binary.BigEndian, &ttl)) buf.Must(binary.Read(request, binary.BigEndian, &size)) return } func (i *IcmpConn) WritePingResponse(id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16) error { response := buf.NewSize(2 + 16 + 1 + 1 + 2) defer response.Release() buf.Must(binary.Write(response, binary.BigEndian, id)) sourceAddressBytes := buildPaddingIP(sourceAddress) buf.Must1(response.Write(sourceAddressBytes[:])) buf.Must(binary.Write(response, binary.BigEndian, icmpType)) buf.Must(binary.Write(response, binary.BigEndian, code)) buf.Must(binary.Write(response, binary.BigEndian, sequenceNumber)) return buf.Error(i.writeFlush(response.Bytes())) } ================================================ FILE: core/Clash.Meta/transport/trusttunnel/packet.go ================================================ package trusttunnel import ( "encoding/binary" "math" "net" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/buf" E "github.com/metacubex/sing/common/exceptions" M "github.com/metacubex/sing/common/metadata" N "github.com/metacubex/sing/common/network" "github.com/metacubex/sing/common/rw" ) type packetConn struct { httpConn readWaitOptions N.ReadWaitOptions } func (c *packetConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { c.readWaitOptions = options return false } var ( _ N.NetPacketConn = (*clientPacketConn)(nil) _ N.FrontHeadroom = (*clientPacketConn)(nil) _ N.PacketReadWaiter = (*clientPacketConn)(nil) ) type clientPacketConn struct { packetConn } func (u *clientPacketConn) FrontHeadroom() int { return 4 + 16 + 2 + 16 + 2 + 1 + math.MaxUint8 } func (u *clientPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { buffer = u.readWaitOptions.NewPacketBuffer() destination, err = u.ReadPacket(buffer) if err != nil { buffer.Release() return nil, M.Socksaddr{}, err } u.readWaitOptions.PostReturn(buffer) return buffer, destination, nil } func (u *clientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { err = u.waitCreated() if err != nil { return M.Socksaddr{}, err } return u.readPacketFromServer(buffer) } func (u *clientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { buffer := buf.With(p) destination, err := u.ReadPacket(buffer) if err != nil { return 0, nil, err } return buffer.Len(), destination.UDPAddr(), nil } func (u *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { return u.writePacketToServer(buffer, destination) } func (u *clientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr)) if err != nil { return 0, err } return len(p), nil } func (u *clientPacketConn) readPacketFromServer(buffer *buf.Buffer) (destination M.Socksaddr, err error) { header := buf.NewSize(4 + 16 + 2 + 16 + 2) defer header.Release() _, err = header.ReadFullFrom(u.body, header.Cap()) if err != nil { return } var length uint32 common.Must(binary.Read(header, binary.BigEndian, &length)) var sourceAddressBuffer [16]byte common.Must1(header.Read(sourceAddressBuffer[:])) destination.Addr = parse16BytesIP(sourceAddressBuffer) common.Must(binary.Read(header, binary.BigEndian, &destination.Port)) common.Must(rw.SkipN(header, 16+2)) // To local address:port payloadLen := int(length) - (16 + 2 + 16 + 2) if payloadLen < 0 { return M.Socksaddr{}, E.New("invalid udp length: ", length) } _, err = buffer.ReadFullFrom(u.body, payloadLen) return } func (u *clientPacketConn) writePacketToServer(buffer *buf.Buffer, source M.Socksaddr) error { defer buffer.Release() if !source.IsIP() { return E.New("only support IP") } appName := AppName if len(appName) > math.MaxUint8 { appName = appName[:math.MaxUint8] } payloadLen := buffer.Len() headerLen := 4 + 16 + 2 + 16 + 2 + 1 + len(appName) lengthField := uint32(16 + 2 + 16 + 2 + 1 + len(appName) + payloadLen) destinationAddress := buildPaddingIP(source.Addr) var ( header *buf.Buffer headerInBuffer bool ) if buffer.Start() >= headerLen { headerBytes := buffer.ExtendHeader(headerLen) header = buf.With(headerBytes) headerInBuffer = true } else { header = buf.NewSize(headerLen) defer header.Release() } common.Must(binary.Write(header, binary.BigEndian, lengthField)) common.Must(header.WriteZeroN(16 + 2)) // Source address:port (unknown) common.Must1(header.Write(destinationAddress[:])) common.Must(binary.Write(header, binary.BigEndian, source.Port)) common.Must(binary.Write(header, binary.BigEndian, uint8(len(appName)))) common.Must1(header.WriteString(appName)) if !headerInBuffer { _, err := u.writer.Write(header.Bytes()) if err != nil { return err } } _, err := u.writer.Write(buffer.Bytes()) if err != nil { return err } if u.flusher != nil { u.flusher.Flush() } return nil } var ( _ N.NetPacketConn = (*serverPacketConn)(nil) _ N.FrontHeadroom = (*serverPacketConn)(nil) _ N.PacketReadWaiter = (*serverPacketConn)(nil) ) type serverPacketConn struct { packetConn } func (u *serverPacketConn) FrontHeadroom() int { return 4 + 16 + 2 + 16 + 2 } func (u *serverPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { buffer = u.readWaitOptions.NewPacketBuffer() destination, err = u.ReadPacket(buffer) if err != nil { buffer.Release() return nil, M.Socksaddr{}, err } u.readWaitOptions.PostReturn(buffer) return buffer, destination, nil } func (u *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { err = u.waitCreated() if err != nil { return M.Socksaddr{}, err } return u.readPacketFromClient(buffer) } func (u *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { buffer := buf.With(p) destination, err := u.ReadPacket(buffer) if err != nil { return 0, nil, err } return buffer.Len(), destination.UDPAddr(), nil } func (u *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { return u.writePacketToClient(buffer, destination) } func (u *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr)) if err != nil { return 0, err } return len(p), nil } func (u *serverPacketConn) readPacketFromClient(buffer *buf.Buffer) (destination M.Socksaddr, err error) { header := buf.NewSize(4 + 16 + 2 + 16 + 2 + 1) defer header.Release() _, err = header.ReadFullFrom(u.body, header.Cap()) if err != nil { return } var length uint32 common.Must(binary.Read(header, binary.BigEndian, &length)) var sourceAddressBuffer [16]byte common.Must1(header.Read(sourceAddressBuffer[:])) var sourcePort uint16 common.Must(binary.Read(header, binary.BigEndian, &sourcePort)) _ = sourcePort var destinationAddressBuffer [16]byte common.Must1(header.Read(destinationAddressBuffer[:])) destination.Addr = parse16BytesIP(destinationAddressBuffer) common.Must(binary.Read(header, binary.BigEndian, &destination.Port)) var appNameLen uint8 common.Must(binary.Read(header, binary.BigEndian, &appNameLen)) if appNameLen > 0 { err = rw.SkipN(u.body, int(appNameLen)) if err != nil { return M.Socksaddr{}, err } } payloadLen := int(length) - (16 + 2 + 16 + 2 + 1 + int(appNameLen)) if payloadLen < 0 { return M.Socksaddr{}, E.New("invalid udp length: ", length) } _, err = buffer.ReadFullFrom(u.body, payloadLen) return } func (u *serverPacketConn) writePacketToClient(buffer *buf.Buffer, source M.Socksaddr) error { defer buffer.Release() if !source.IsIP() { return E.New("only support IP") } payloadLen := buffer.Len() headerLen := 4 + 16 + 2 + 16 + 2 lengthField := uint32(16 + 2 + 16 + 2 + payloadLen) sourceAddress := buildPaddingIP(source.Addr) var destinationAddress [16]byte var destinationPort uint16 var ( header *buf.Buffer headerInBuffer bool ) if buffer.Start() >= headerLen { headerBytes := buffer.ExtendHeader(headerLen) header = buf.With(headerBytes) headerInBuffer = true } else { header = buf.NewSize(headerLen) defer header.Release() } common.Must(binary.Write(header, binary.BigEndian, lengthField)) common.Must1(header.Write(sourceAddress[:])) common.Must(binary.Write(header, binary.BigEndian, source.Port)) common.Must1(header.Write(destinationAddress[:])) common.Must(binary.Write(header, binary.BigEndian, destinationPort)) if !headerInBuffer { _, err := u.writer.Write(header.Bytes()) if err != nil { return err } } _, err := u.writer.Write(buffer.Bytes()) if err != nil { return err } if u.flusher != nil { u.flusher.Flush() } return nil } ================================================ FILE: core/Clash.Meta/transport/trusttunnel/protocol.go ================================================ package trusttunnel import ( "bytes" "encoding/base64" "errors" "io" "net" "net/http" "net/netip" "runtime" "strings" "sync" "time" "github.com/metacubex/mihomo/common/httputils" C "github.com/metacubex/mihomo/constant" ) const ( UDPMagicAddress = "_udp2" ICMPMagicAddress = "_icmp" HealthCheckMagicAddress = "_check" DefaultQuicStreamReceiveWindow = 131072 // Chrome's default DefaultConnectionTimeout = 30 * time.Second DefaultHealthCheckTimeout = 7 * time.Second DefaultQuicMaxIdleTimeout = 2 * (DefaultConnectionTimeout + DefaultHealthCheckTimeout) DefaultSessionTimeout = 30 * time.Second ) var ( AppName = C.Name Version = C.Version // TCPUserAgent is user-agent for TCP connections. // Format: TCPUserAgent = runtime.GOOS + " " + AppName + "/" + Version // UDPUserAgent is user-agent for UDP multiplexinh. // Format: _udp2 UDPUserAgent = runtime.GOOS + " " + UDPMagicAddress // ICMPUserAgent is user-agent for ICMP multiplexinh. // Format: _icmp ICMPUserAgent = runtime.GOOS + " " + ICMPMagicAddress HealthCheckUserAgent = runtime.GOOS ) func buildAuth(username string, password string) string { return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) } // parseBasicAuth parses an HTTP Basic Authentication strinh. // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). func parseBasicAuth(auth string) (username, password string, ok bool) { const prefix = "Basic " // Case insensitive prefix match. See Issue 22736. if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { return "", "", false } c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) if err != nil { return "", "", false } cs := string(c) username, password, ok = strings.Cut(cs, ":") if !ok { return "", "", false } return username, password, true } func parse16BytesIP(buffer [16]byte) netip.Addr { var zeroPrefix [12]byte isIPv4 := bytes.HasPrefix(buffer[:], zeroPrefix[:]) // Special: check ::1 isIPv4 = isIPv4 && !(buffer[12] == 0 && buffer[13] == 0 && buffer[14] == 0 && buffer[15] == 1) if isIPv4 { return netip.AddrFrom4([4]byte(buffer[12:16])) } return netip.AddrFrom16(buffer) } func buildPaddingIP(addr netip.Addr) (buffer [16]byte) { if addr.Is6() { return addr.As16() } ipv4 := addr.As4() copy(buffer[12:16], ipv4[:]) return buffer } type httpConn struct { writer io.Writer flusher http.Flusher body io.ReadCloser setupOnce sync.Once created chan struct{} createErr error cancelFn func() closeFn func() httputils.NetAddr // deadlines deadline *time.Timer } func (h *httpConn) setup(body io.ReadCloser, err error) { h.setupOnce.Do(func() { h.body = body h.createErr = err close(h.created) }) if h.createErr != nil && body != nil { // conn already closed before setup _ = body.Close() } } func (h *httpConn) waitCreated() error { <-h.created if h.body != nil { return nil } return h.createErr } func (h *httpConn) Close() error { var errorArr []error h.setup(nil, net.ErrClosed) if closer, ok := h.writer.(io.Closer); ok { errorArr = append(errorArr, closer.Close()) } if h.body != nil { errorArr = append(errorArr, h.body.Close()) } if h.cancelFn != nil { h.cancelFn() } if h.closeFn != nil { h.closeFn() } return errors.Join(errorArr...) } func (h *httpConn) writeFlush(p []byte) (n int, err error) { n, err = h.writer.Write(p) if h.flusher != nil { h.flusher.Flush() } return n, err } func (h *httpConn) SetReadDeadline(t time.Time) error { return h.SetDeadline(t) } func (h *httpConn) SetWriteDeadline(t time.Time) error { return h.SetDeadline(t) } func (h *httpConn) SetDeadline(t time.Time) error { if t.IsZero() { if h.deadline != nil { h.deadline.Stop() h.deadline = nil } return nil } d := time.Until(t) if h.deadline != nil { h.deadline.Reset(d) return nil } h.deadline = time.AfterFunc(d, func() { h.Close() }) return nil } var _ net.Conn = (*tcpConn)(nil) type tcpConn struct { httpConn } func (t *tcpConn) Read(b []byte) (n int, err error) { err = t.waitCreated() if err != nil { return 0, err } n, err = t.body.Read(b) return } func (t *tcpConn) Write(b []byte) (int, error) { return t.writeFlush(b) } ================================================ FILE: core/Clash.Meta/transport/trusttunnel/quic.go ================================================ package trusttunnel import ( "context" "errors" "net" "runtime" "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" "github.com/metacubex/tls" ) func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlName string, cwnd int, bbrProfile string) error { stdConfig, err := tlsConfig.ToStdConfig() if err != nil { return err } c.roundTripper = &http3.Transport{ TLSClientConfig: stdConfig, QUICConfig: &quic.Config{ Versions: []quic.Version{quic.Version1}, MaxIdleTimeout: DefaultQuicMaxIdleTimeout, InitialStreamReceiveWindow: DefaultQuicStreamReceiveWindow, DisablePathMTUDiscovery: !(runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "darwin"), Allow0RTT: false, }, Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { err := tlsConfig.ECH.ClientHandle(ctx, tlsCfg) if err != nil { return nil, err } _, quicConn, err := common.DialQuic(ctx, addr, c.dialOptions(), c.dialer, tlsCfg, cfg, true) if err != nil { return nil, err } common.SetCongestionController(quicConn, congestionControlName, cwnd, bbrProfile) return quicConn, nil }, } return nil } func (s *Service) configHTTP3Server(tlsConfig *tls.Config, udpConn net.PacketConn) error { tlsConfig = http3.ConfigureTLSConfig(tlsConfig) quicListener, err := quic.ListenEarly(udpConn, tlsConfig, &quic.Config{ Versions: []quic.Version{quic.Version1}, MaxIdleTimeout: DefaultQuicMaxIdleTimeout, MaxIncomingStreams: 1 << 60, Allow0RTT: true, }) if err != nil { return err } h3Server := &http3.Server{ Handler: s, IdleTimeout: DefaultSessionTimeout, ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context { common.SetCongestionController(conn, s.quicCongestionControl, s.quicCwnd, s.quicBBRProfile) return ctx }, } s.h3Server = h3Server s.udpConn = udpConn go func() { sErr := h3Server.ServeListener(quicListener) if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) { s.logger.ErrorContext(s.ctx, "HTTP3 server close: ", sErr) } }() return nil } ================================================ FILE: core/Clash.Meta/transport/trusttunnel/service.go ================================================ package trusttunnel import ( "context" "errors" "net" "time" "github.com/metacubex/mihomo/common/httputils" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/http" "github.com/metacubex/quic-go/http3" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/auth" "github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/bufio" E "github.com/metacubex/sing/common/exceptions" "github.com/metacubex/sing/common/logger" M "github.com/metacubex/sing/common/metadata" "github.com/metacubex/sing/common/network" "github.com/metacubex/tls" ) type Handler interface { network.TCPConnectionHandler network.UDPConnectionHandler } type ICMPHandler interface { NewICMPConnection(ctx context.Context, conn *IcmpConn) } type ServiceOptions struct { Ctx context.Context Logger logger.ContextLogger Handler Handler ICMPHandler ICMPHandler QUICCongestionControl string QUICCwnd int QUICBBRProfile string } type Service struct { ctx context.Context logger logger.ContextLogger users map[string]string handler Handler icmpHandler ICMPHandler quicCongestionControl string quicCwnd int quicBBRProfile string httpServer *http.Server h3Server *http3.Server tcpListener net.Listener tlsListener net.Listener udpConn net.PacketConn } func NewService(options ServiceOptions) *Service { return &Service{ ctx: options.Ctx, logger: options.Logger, handler: options.Handler, icmpHandler: options.ICMPHandler, quicCongestionControl: options.QUICCongestionControl, quicCwnd: options.QUICCwnd, quicBBRProfile: options.QUICBBRProfile, } } func (s *Service) Start(tcpListener net.Listener, udpConn net.PacketConn, tlsConfig *tls.Config) error { if tcpListener != nil { protocols := new(http.Protocols) protocols.SetHTTP1(true) protocols.SetHTTP2(true) protocols.SetUnencryptedHTTP2(true) s.httpServer = &http.Server{ Handler: s, IdleTimeout: DefaultSessionTimeout, BaseContext: func(net.Listener) context.Context { return s.ctx }, Protocols: protocols, } listener := tcpListener s.tcpListener = tcpListener if tlsConfig != nil { listener = tls.NewListener(listener, tlsConfig) s.tlsListener = listener } go func() { sErr := s.httpServer.Serve(listener) if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) { s.logger.ErrorContext(s.ctx, "HTTP server close: ", sErr) } }() } if udpConn != nil { err := s.configHTTP3Server(tlsConfig, udpConn) if err != nil { return err } } return nil } func (s *Service) UpdateUsers(users map[string]string) { s.users = users } func (s *Service) Close() error { var shutdownErr error if s.httpServer != nil { const shutdownTimeout = 5 * time.Second ctx, cancel := context.WithTimeout(s.ctx, shutdownTimeout) shutdownErr = s.httpServer.Shutdown(ctx) cancel() if errors.Is(shutdownErr, http.ErrServerClosed) { shutdownErr = nil } } closeErr := common.Close( common.PtrOrNil(s.httpServer), s.tlsListener, s.tcpListener, common.PtrOrNil(s.h3Server), s.udpConn, ) return E.Errors(shutdownErr, closeErr) } func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) { authorization := request.Header.Get("Proxy-Authorization") username, loaded := s.verify(authorization) if !loaded { writer.WriteHeader(http.StatusProxyAuthRequired) s.badRequest(request.Context(), request, E.New("authorization failed")) return } if request.Method != http.MethodConnect { writer.WriteHeader(http.StatusMethodNotAllowed) s.badRequest(request.Context(), request, E.New("unexpected HTTP method ", request.Method)) return } ctx := request.Context() ctx = auth.ContextWithUser(ctx, username) s.logger.DebugContext(ctx, "[", username, "] ", "request from ", request.RemoteAddr) s.logger.DebugContext(ctx, "[", username, "] ", "request to ", request.Host) switch request.Host { case UDPMagicAddress: writer.WriteHeader(http.StatusOK) flusher, isFlusher := writer.(http.Flusher) if isFlusher { flusher.Flush() } conn := &serverPacketConn{ packetConn: packetConn{ httpConn: httpConn{ writer: writer, flusher: flusher, created: make(chan struct{}), }, }, } httputils.SetAddrFromRequest(&conn.NetAddr, request) conn.setup(request.Body, nil) firstPacket := buf.NewPacket() destination, err := conn.ReadPacket(firstPacket) if err != nil { firstPacket.Release() _ = conn.Close() s.logger.ErrorContext(ctx, E.Cause(err, "read first packet of ", request.RemoteAddr)) return } destination = destination.Unwrap() cachedConn := bufio.NewCachedPacketConn(conn, firstPacket, destination) _ = s.handler.NewPacketConnection(ctx, cachedConn, M.Metadata{ Protocol: "trusttunnel", Source: M.ParseSocksaddr(request.RemoteAddr), Destination: destination, }) case ICMPMagicAddress: flusher, isFlusher := writer.(http.Flusher) if s.icmpHandler == nil { writer.WriteHeader(http.StatusNotImplemented) if isFlusher { flusher.Flush() } _ = request.Body.Close() } else { writer.WriteHeader(http.StatusOK) if isFlusher { flusher.Flush() } conn := &IcmpConn{ httpConn{ writer: writer, flusher: flusher, created: make(chan struct{}), }, } httputils.SetAddrFromRequest(&conn.NetAddr, request) conn.setup(request.Body, nil) s.icmpHandler.NewICMPConnection(ctx, conn) } case HealthCheckMagicAddress: writer.WriteHeader(http.StatusOK) if flusher, isFlusher := writer.(http.Flusher); isFlusher { flusher.Flush() } _ = request.Body.Close() default: writer.WriteHeader(http.StatusOK) flusher, isFlusher := writer.(http.Flusher) if isFlusher { flusher.Flush() } conn := &tcpConn{ httpConn{ writer: writer, flusher: flusher, created: make(chan struct{}), }, } httputils.SetAddrFromRequest(&conn.NetAddr, request) conn.setup(request.Body, nil) _ = s.handler.NewConnection(ctx, N.NewDeadlineConn(conn), M.Metadata{ Protocol: "trusttunnel", Source: M.ParseSocksaddr(request.RemoteAddr), Destination: M.ParseSocksaddr(request.Host).Unwrap(), }) } } func (s *Service) verify(authorization string) (username string, loaded bool) { username, password, loaded := parseBasicAuth(authorization) if !loaded { return "", false } recordedPassword, loaded := s.users[username] if !loaded { return "", false } if password != recordedPassword { return "", false } return username, true } func (s *Service) badRequest(ctx context.Context, request *http.Request, err error) { s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr)) } ================================================ FILE: core/Clash.Meta/transport/tuic/common/congestion.go ================================================ package common import ( "github.com/metacubex/mihomo/transport/tuic/congestion" congestionv2 "github.com/metacubex/mihomo/transport/tuic/congestion_v2" "github.com/metacubex/quic-go" c "github.com/metacubex/quic-go/congestion" ) const ( DefaultStreamReceiveWindow = 15728640 // 15 MB/s DefaultConnectionReceiveWindow = 67108864 // 64 MB/s ) func SetCongestionController(quicConn *quic.Conn, cc string, cwnd int, profile string) { if cwnd == 0 { cwnd = 32 } switch cc { case "cubic": quicConn.SetCongestionControl( congestion.NewCubicSender( congestion.GetInitialPacketSize(quicConn), false, ), ) case "new_reno": quicConn.SetCongestionControl( congestion.NewCubicSender( congestion.GetInitialPacketSize(quicConn), true, ), ) case "bbr_meta_v1": quicConn.SetCongestionControl( congestion.NewBBRSender( congestion.GetInitialPacketSize(quicConn), c.ByteCount(cwnd)*congestion.InitialMaxDatagramSize, congestion.DefaultBBRMaxCongestionWindow*congestion.InitialMaxDatagramSize, ), ) case "bbr_meta_v2": fallthrough case "bbr": quicConn.SetCongestionControl( congestionv2.NewBbrSender( congestionv2.GetInitialPacketSize(quicConn), c.ByteCount(cwnd), congestionv2.Profile(profile), ), ) } } ================================================ FILE: core/Clash.Meta/transport/tuic/common/dial.go ================================================ package common import ( "context" "net" "net/netip" "time" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/quic-go" "github.com/metacubex/tls" ) type PacketDialer interface { ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) } func DialQuic(ctx context.Context, address string, opts []dialer.Option, pDialer PacketDialer, tlsConf *tls.Config, conf *quic.Config, early bool) (net.PacketConn, *quic.Conn, error) { d := dialer.NewDialer( dialer.WithOptions(opts...), dialer.WithNetDialer(dialer.NetDialerFunc(func(ctx context.Context, network, address string) (net.Conn, error) { addrPort, err := netip.ParseAddrPort(address) // the dialer will resolve the domain to ip if err != nil { return nil, err } udpAddr := net.UDPAddrFromAddrPort(addrPort) packetConn, err := pDialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort()) if err != nil { return nil, err } transport := quic.Transport{Conn: packetConn} transport.SetCreatedConn(true) // auto close conn transport.SetSingleUse(true) // auto close transport var quicConn *quic.Conn if early { quicConn, err = transport.DialEarly(ctx, udpAddr, tlsConf, conf) } else { quicConn, err = transport.Dial(ctx, udpAddr, tlsConf, conf) } if err != nil { _ = packetConn.Close() return nil, err } return quicNetConn{Conn: quicConn, pc: packetConn}, nil })), ) c, err := d.DialContext(ctx, "udp", address) if err != nil { return nil, nil, err } nc := c.(quicNetConn) return nc.pc, nc.Conn, nil } type quicNetConn struct { *quic.Conn pc net.PacketConn } func (q quicNetConn) Close() error { err := q.Conn.CloseWithError(0, "") _ = q.pc.Close() // always close the packetConn return err } func (q quicNetConn) Read(b []byte) (n int, err error) { panic("should not call Read on quicNetConn") } func (q quicNetConn) Write(b []byte) (n int, err error) { panic("should not call Write on quicNetConn") } func (q quicNetConn) SetDeadline(t time.Time) error { panic("should not call SetDeadline on quicNetConn") } func (q quicNetConn) SetReadDeadline(t time.Time) error { panic("should not call SetReadDeadline on quicNetConn") } func (q quicNetConn) SetWriteDeadline(t time.Time) error { panic("should not call SetWriteDeadline on quicNetConn") } var _ net.Conn = quicNetConn{} ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/bandwidth.go ================================================ package congestion import ( "math" "time" "github.com/metacubex/quic-go/congestion" ) // Bandwidth of a connection type Bandwidth uint64 const infBandwidth Bandwidth = math.MaxUint64 const ( // BitsPerSecond is 1 bit per second BitsPerSecond Bandwidth = 1 // BytesPerSecond is 1 byte per second BytesPerSecond = 8 * BitsPerSecond ) // BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/bandwidth_sampler.go ================================================ package congestion import ( "math" "time" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" ) var ( InfiniteBandwidth = Bandwidth(math.MaxUint64) ) // SendTimeState is a subset of ConnectionStateOnSentPacket which is returned // to the caller when the packet is acked or lost. type SendTimeState struct { // Whether other states in this object is valid. isValid bool // Whether the sender is app limited at the time the packet was sent. // App limited bandwidth sample might be artificially low because the sender // did not have enough data to send in order to saturate the link. isAppLimited bool // Total number of sent bytes at the time the packet was sent. // Includes the packet itself. totalBytesSent congestion.ByteCount // Total number of acked bytes at the time the packet was sent. totalBytesAcked congestion.ByteCount // Total number of lost bytes at the time the packet was sent. totalBytesLost congestion.ByteCount } // ConnectionStateOnSentPacket represents the information about a sent packet // and the state of the connection at the moment the packet was sent, // specifically the information about the most recently acknowledged packet at // that moment. type ConnectionStateOnSentPacket struct { packetNumber congestion.PacketNumber // Time at which the packet is sent. sendTime monotime.Time // Size of the packet. size congestion.ByteCount // The value of |totalBytesSentAtLastAckedPacket| at the time the // packet was sent. totalBytesSentAtLastAckedPacket congestion.ByteCount // The value of |lastAckedPacketSentTime| at the time the packet was // sent. lastAckedPacketSentTime monotime.Time // The value of |lastAckedPacketAckTime| at the time the packet was // sent. lastAckedPacketAckTime monotime.Time // Send time states that are returned to the congestion controller when the // packet is acked or lost. sendTimeState SendTimeState } // BandwidthSample type BandwidthSample struct { // The bandwidth at that particular sample. Zero if no valid bandwidth sample // is available. bandwidth Bandwidth // The RTT measurement at this particular sample. Zero if no RTT sample is // available. Does not correct for delayed ack time. rtt time.Duration // States captured when the packet was sent. stateAtSend SendTimeState } func NewBandwidthSample() *BandwidthSample { return &BandwidthSample{ // FIXME: the default value of original code is zero. rtt: InfiniteRTT, } } // BandwidthSampler keeps track of sent and acknowledged packets and outputs a // bandwidth sample for every packet acknowledged. The samples are taken for // individual packets, and are not filtered; the consumer has to filter the // bandwidth samples itself. In certain cases, the sampler will locally severely // underestimate the bandwidth, hence a maximum filter with a size of at least // one RTT is recommended. // // This class bases its samples on the slope of two curves: the number of bytes // sent over time, and the number of bytes acknowledged as received over time. // It produces a sample of both slopes for every packet that gets acknowledged, // based on a slope between two points on each of the corresponding curves. Note // that due to the packet loss, the number of bytes on each curve might get // further and further away from each other, meaning that it is not feasible to // compare byte values coming from different curves with each other. // // The obvious points for measuring slope sample are the ones corresponding to // the packet that was just acknowledged. Let us denote them as S_1 (point at // which the current packet was sent) and A_1 (point at which the current packet // was acknowledged). However, taking a slope requires two points on each line, // so estimating bandwidth requires picking a packet in the past with respect to // which the slope is measured. // // For that purpose, BandwidthSampler always keeps track of the most recently // acknowledged packet, and records it together with every outgoing packet. // When a packet gets acknowledged (A_1), it has not only information about when // it itself was sent (S_1), but also the information about the latest // acknowledged packet right before it was sent (S_0 and A_0). // // Based on that data, send and ack rate are estimated as: // // send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) // ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) // // Here, the ack rate is intuitively the rate we want to treat as bandwidth. // However, in certain cases (e.g. ack compression) the ack rate at a point may // end up higher than the rate at which the data was originally sent, which is // not indicative of the real bandwidth. Hence, we use the send rate as an upper // bound, and the sample value is // // rate_sample = min(send_rate, ack_rate) // // An important edge case handled by the sampler is tracking the app-limited // samples. There are multiple meaning of "app-limited" used interchangeably, // hence it is important to understand and to be able to distinguish between // them. // // Meaning 1: connection state. The connection is said to be app-limited when // there is no outstanding data to send. This means that certain bandwidth // samples in the future would not be an accurate indication of the link // capacity, and it is important to inform consumer about that. Whenever // connection becomes app-limited, the sampler is notified via OnAppLimited() // method. // // Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth // sampler becomes notified about the connection being app-limited, it enters // app-limited phase. In that phase, all *sent* packets are marked as // app-limited. Note that the connection itself does not have to be // app-limited during the app-limited phase, and in fact it will not be // (otherwise how would it send packets?). The boolean flag below indicates // whether the sampler is in that phase. // // Meaning 3: a flag on the sent packet and on the sample. If a sent packet is // sent during the app-limited phase, the resulting sample related to the // packet will be marked as app-limited. // // With the terminology issue out of the way, let us consider the question of // what kind of situation it addresses. // // Consider a scenario where we first send packets 1 to 20 at a regular // bandwidth, and then immediately run out of data. After a few seconds, we send // packets 21 to 60, and only receive ack for 21 between sending packets 40 and // 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 // we use to compute the slope is going to be packet 20, a few seconds apart // from the current packet, hence the resulting estimate would be extremely low // and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, // meaning that the bandwidth sample would exclude the quiescence. // // Based on the analysis of that scenario, we implement the following rule: once // OnAppLimited() is called, all sent packets will produce app-limited samples // up until an ack for a packet that was sent after OnAppLimited() was called. // Note that while the scenario above is not the only scenario when the // connection is app-limited, the approach works in other cases too. type BandwidthSampler struct { // The total number of congestion controlled bytes sent during the connection. totalBytesSent congestion.ByteCount // The total number of congestion controlled bytes which were acknowledged. totalBytesAcked congestion.ByteCount // The total number of congestion controlled bytes which were lost. totalBytesLost congestion.ByteCount // The value of |totalBytesSent| at the time the last acknowledged packet // was sent. Valid only when |lastAckedPacketSentTime| is valid. totalBytesSentAtLastAckedPacket congestion.ByteCount // The time at which the last acknowledged packet was sent. Set to // QuicTime::Zero() if no valid timestamp is available. lastAckedPacketSentTime monotime.Time // The time at which the most recent packet was acknowledged. lastAckedPacketAckTime monotime.Time // The most recently sent packet. lastSendPacket congestion.PacketNumber // Indicates whether the bandwidth sampler is currently in an app-limited // phase. isAppLimited bool // The packet that will be acknowledged after this one will cause the sampler // to exit the app-limited phase. endOfAppLimitedPhase congestion.PacketNumber // Record of the connection state at the point where each packet in flight was // sent, indexed by the packet number. connectionStats *ConnectionStates } func NewBandwidthSampler() *BandwidthSampler { return &BandwidthSampler{ connectionStats: &ConnectionStates{ stats: make(map[congestion.PacketNumber]*ConnectionStateOnSentPacket), }, } } // OnPacketSent Inputs the sent packet information into the sampler. Assumes that all // packets are sent in order. The information about the packet will not be // released from the sampler until it the packet is either acknowledged or // declared lost. func (s *BandwidthSampler) OnPacketSent(sentTime monotime.Time, lastSentPacket congestion.PacketNumber, sentBytes, bytesInFlight congestion.ByteCount, hasRetransmittableData bool) { s.lastSendPacket = lastSentPacket if !hasRetransmittableData { return } s.totalBytesSent += sentBytes // If there are no packets in flight, the time at which the new transmission // opens can be treated as the A_0 point for the purpose of bandwidth // sampling. This underestimates bandwidth to some extent, and produces some // artificially low samples for most packets in flight, but it provides with // samples at important points where we would not have them otherwise, most // importantly at the beginning of the connection. if bytesInFlight == 0 { s.lastAckedPacketAckTime = sentTime s.totalBytesSentAtLastAckedPacket = s.totalBytesSent // In this situation ack compression is not a concern, set send rate to // effectively infinite. s.lastAckedPacketSentTime = sentTime } s.connectionStats.Insert(lastSentPacket, sentTime, sentBytes, s) } // OnPacketAcked Notifies the sampler that the |lastAckedPacket| is acknowledged. Returns a // bandwidth sample. If no bandwidth sample is available, // QuicBandwidth::Zero() is returned. func (s *BandwidthSampler) OnPacketAcked(ackTime monotime.Time, lastAckedPacket congestion.PacketNumber) *BandwidthSample { sentPacketState := s.connectionStats.Get(lastAckedPacket) if sentPacketState == nil { return NewBandwidthSample() } sample := s.onPacketAckedInner(ackTime, lastAckedPacket, sentPacketState) s.connectionStats.Remove(lastAckedPacket) return sample } // onPacketAckedInner Handles the actual bandwidth calculations, whereas the outer method handles // retrieving and removing |sentPacket|. func (s *BandwidthSampler) onPacketAckedInner(ackTime monotime.Time, lastAckedPacket congestion.PacketNumber, sentPacket *ConnectionStateOnSentPacket) *BandwidthSample { s.totalBytesAcked += sentPacket.size s.totalBytesSentAtLastAckedPacket = sentPacket.sendTimeState.totalBytesSent s.lastAckedPacketSentTime = sentPacket.sendTime s.lastAckedPacketAckTime = ackTime // Exit app-limited phase once a packet that was sent while the connection is // not app-limited is acknowledged. if s.isAppLimited && lastAckedPacket > s.endOfAppLimitedPhase { s.isAppLimited = false } // There might have been no packets acknowledged at the moment when the // current packet was sent. In that case, there is no bandwidth sample to // make. if sentPacket.lastAckedPacketSentTime.IsZero() { return NewBandwidthSample() } // Infinite rate indicates that the sampler is supposed to discard the // current send rate sample and use only the ack rate. sendRate := InfiniteBandwidth if sentPacket.sendTime.After(sentPacket.lastAckedPacketSentTime) { sendRate = BandwidthFromDelta(sentPacket.sendTimeState.totalBytesSent-sentPacket.totalBytesSentAtLastAckedPacket, sentPacket.sendTime.Sub(sentPacket.lastAckedPacketSentTime)) } // During the slope calculation, ensure that ack time of the current packet is // always larger than the time of the previous packet, otherwise division by // zero or integer underflow can occur. if !ackTime.After(sentPacket.lastAckedPacketAckTime) { // TODO(wub): Compare this code count before and after fixing clock jitter // issue. // if sentPacket.lastAckedPacketAckTime.Equal(sentPacket.sendTime) { // This is the 1st packet after quiescense. // QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 1, 2); // } else { // QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 2, 2); // } return NewBandwidthSample() } ackRate := BandwidthFromDelta(s.totalBytesAcked-sentPacket.sendTimeState.totalBytesAcked, ackTime.Sub(sentPacket.lastAckedPacketAckTime)) // Note: this sample does not account for delayed acknowledgement time. This // means that the RTT measurements here can be artificially high, especially // on low bandwidth connections. sample := &BandwidthSample{ bandwidth: minBandwidth(sendRate, ackRate), rtt: ackTime.Sub(sentPacket.sendTime), } SentPacketToSendTimeState(sentPacket, &sample.stateAtSend) return sample } // OnCongestionEvent Informs the sampler that a packet is considered lost and it should no // longer keep track of it. func (s *BandwidthSampler) OnCongestionEvent(packetNumber congestion.PacketNumber) SendTimeState { ok, sentPacket := s.connectionStats.Remove(packetNumber) sendTimeState := SendTimeState{ isValid: ok, } if sentPacket != nil { s.totalBytesLost += sentPacket.size SentPacketToSendTimeState(sentPacket, &sendTimeState) } return sendTimeState } // OnAppLimited Informs the sampler that the connection is currently app-limited, causing // the sampler to enter the app-limited phase. The phase will expire by // itself. func (s *BandwidthSampler) OnAppLimited() { s.isAppLimited = true s.endOfAppLimitedPhase = s.lastSendPacket } // SentPacketToSendTimeState Copy a subset of the (private) ConnectionStateOnSentPacket to the (public) // SendTimeState. Always set send_time_state->is_valid to true. func SentPacketToSendTimeState(sentPacket *ConnectionStateOnSentPacket, sendTimeState *SendTimeState) { sendTimeState.isAppLimited = sentPacket.sendTimeState.isAppLimited sendTimeState.totalBytesSent = sentPacket.sendTimeState.totalBytesSent sendTimeState.totalBytesAcked = sentPacket.sendTimeState.totalBytesAcked sendTimeState.totalBytesLost = sentPacket.sendTimeState.totalBytesLost sendTimeState.isValid = true } // ConnectionStates Record of the connection state at the point where each packet in flight was // sent, indexed by the packet number. // FIXME: using LinkedList replace map to fast remove all the packets lower than the specified packet number. type ConnectionStates struct { stats map[congestion.PacketNumber]*ConnectionStateOnSentPacket } func (s *ConnectionStates) Insert(packetNumber congestion.PacketNumber, sentTime monotime.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) bool { if _, ok := s.stats[packetNumber]; ok { return false } s.stats[packetNumber] = NewConnectionStateOnSentPacket(packetNumber, sentTime, bytes, sampler) return true } func (s *ConnectionStates) Get(packetNumber congestion.PacketNumber) *ConnectionStateOnSentPacket { return s.stats[packetNumber] } func (s *ConnectionStates) Remove(packetNumber congestion.PacketNumber) (bool, *ConnectionStateOnSentPacket) { state, ok := s.stats[packetNumber] if ok { delete(s.stats, packetNumber) } return ok, state } func NewConnectionStateOnSentPacket(packetNumber congestion.PacketNumber, sentTime monotime.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) *ConnectionStateOnSentPacket { return &ConnectionStateOnSentPacket{ packetNumber: packetNumber, sendTime: sentTime, size: bytes, lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, sendTimeState: SendTimeState{ isValid: true, isAppLimited: sampler.isAppLimited, totalBytesSent: sampler.totalBytesSent, totalBytesAcked: sampler.totalBytesAcked, totalBytesLost: sampler.totalBytesLost, }, } } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/bbr_sender.go ================================================ package congestion // src from https://quiche.googlesource.com/quiche.git/+/66dea072431f94095dfc3dd2743cb94ef365f7ef/quic/core/congestion_control/bbr_sender.cc import ( "fmt" "math" "time" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" "github.com/metacubex/randv2" ) const ( // InitialMaxDatagramSize is the default maximum packet size used in QUIC for congestion window computations in bytes. InitialMaxDatagramSize = 1280 InitialPacketSize = 1280 InitialCongestionWindow = 32 DefaultBBRMaxCongestionWindow = 10000 ) func GetInitialPacketSize(quicConn *quic.Conn) congestion.ByteCount { return congestion.ByteCount(quicConn.Config().InitialPacketSize) } var ( // Default initial rtt used before any samples are received. InitialRtt = 100 * time.Millisecond // The gain used for the STARTUP, equal to 4*ln(2). DefaultHighGain = 2.77 // The gain used in STARTUP after loss has been detected. // 1.5 is enough to allow for 25% exogenous loss and still observe a 25% growth // in measured bandwidth. StartupAfterLossGain = 1.5 // The cycle of gains used during the PROBE_BW stage. PacingGain = []float64{1.25, 0.75, 1, 1, 1, 1, 1, 1} // The length of the gain cycle. GainCycleLength = len(PacingGain) // The size of the bandwidth filter window, in round-trips. BandwidthWindowSize = GainCycleLength + 2 // The time after which the current min_rtt value expires. MinRttExpiry = 10 * time.Second // The minimum time the connection can spend in PROBE_RTT mode. ProbeRttTime = time.Millisecond * 200 // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection // will exit the STARTUP mode. StartupGrowthTarget = 1.25 RoundTripsWithoutGrowthBeforeExitingStartup = int64(3) // Coefficient of target congestion window to use when basing PROBE_RTT on BDP. ModerateProbeRttMultiplier = 0.75 // Coefficient to determine if a new RTT is sufficiently similar to min_rtt that // we don't need to enter PROBE_RTT. SimilarMinRttThreshold = 1.125 // Congestion window gain for QUIC BBR during PROBE_BW phase. DefaultCongestionWindowGainConst = 2.0 ) type bbrMode int const ( // Startup phase of the connection. STARTUP = iota // After achieving the highest possible bandwidth during the startup, lower // the pacing rate in order to drain the queue. DRAIN // Cruising mode. PROBE_BW // Temporarily slow down sending in order to empty the buffer and measure // the real minimum RTT. PROBE_RTT ) type bbrRecoveryState int const ( // Do not limit. NOT_IN_RECOVERY = iota // Allow an extra outstanding byte for each byte acknowledged. CONSERVATION // Allow two extra outstanding bytes for each byte acknowledged (slow // start). GROWTH ) type bbrSender struct { mode bbrMode rttStats congestion.RTTStatsProvider bytesInFlight congestion.ByteCount // return total bytes of unacked packets. //GetBytesInFlight func() congestion.ByteCount // Bandwidth sampler provides BBR with the bandwidth measurements at // individual points. sampler *BandwidthSampler // The number of the round trips that have occurred during the connection. roundTripCount int64 // The packet number of the most recently sent packet. lastSendPacket congestion.PacketNumber // Acknowledgement of any packet after |current_round_trip_end_| will cause // the round trip counter to advance. currentRoundTripEnd congestion.PacketNumber // The filter that tracks the maximum bandwidth over the multiple recent // round-trips. maxBandwidth *WindowedFilter // Tracks the maximum number of bytes acked faster than the sending rate. maxAckHeight *WindowedFilter // The time this aggregation started and the number of bytes acked during it. aggregationEpochStartTime monotime.Time aggregationEpochBytes congestion.ByteCount // Minimum RTT estimate. Automatically expires within 10 seconds (and // triggers PROBE_RTT mode) if no new value is sampled during that period. minRtt time.Duration // The time at which the current value of |min_rtt_| was assigned. minRttTimestamp monotime.Time // The maximum allowed number of bytes in flight. congestionWindow congestion.ByteCount // The initial value of the |congestion_window_|. initialCongestionWindow congestion.ByteCount // The largest value the |congestion_window_| can achieve. initialMaxCongestionWindow congestion.ByteCount // The smallest value the |congestion_window_| can achieve. //minCongestionWindow congestion.ByteCount // The pacing gain applied during the STARTUP phase. highGain float64 // The CWND gain applied during the STARTUP phase. highCwndGain float64 // The pacing gain applied during the DRAIN phase. drainGain float64 // The current pacing rate of the connection. pacingRate Bandwidth // The gain currently applied to the pacing rate. pacingGain float64 // The gain currently applied to the congestion window. congestionWindowGain float64 // The gain used for the congestion window during PROBE_BW. Latched from // quic_bbr_cwnd_gain flag. congestionWindowGainConst float64 // The number of RTTs to stay in STARTUP mode. Defaults to 3. numStartupRtts int64 // If true, exit startup if 1RTT has passed with no bandwidth increase and // the connection is in recovery. exitStartupOnLoss bool // Number of round-trips in PROBE_BW mode, used for determining the current // pacing gain cycle. cycleCurrentOffset int // The time at which the last pacing gain cycle was started. lastCycleStart monotime.Time // Indicates whether the connection has reached the full bandwidth mode. isAtFullBandwidth bool // Number of rounds during which there was no significant bandwidth increase. roundsWithoutBandwidthGain int64 // The bandwidth compared to which the increase is measured. bandwidthAtLastRound Bandwidth // Set to true upon exiting quiescence. exitingQuiescence bool // Time at which PROBE_RTT has to be exited. Setting it to zero indicates // that the time is yet unknown as the number of packets in flight has not // reached the required value. exitProbeRttAt monotime.Time // Indicates whether a round-trip has passed since PROBE_RTT became active. probeRttRoundPassed bool // Indicates whether the most recent bandwidth sample was marked as // app-limited. lastSampleIsAppLimited bool // Indicates whether any non app-limited samples have been recorded. hasNoAppLimitedSample bool // Indicates app-limited calls should be ignored as long as there's // enough data inflight to see more bandwidth when necessary. flexibleAppLimited bool // Current state of recovery. recoveryState bbrRecoveryState // Receiving acknowledgement of a packet after |end_recovery_at_| will cause // BBR to exit the recovery mode. A value above zero indicates at least one // loss has been detected, so it must not be set back to zero. endRecoveryAt congestion.PacketNumber // A window used to limit the number of bytes in flight during loss recovery. recoveryWindow congestion.ByteCount // If true, consider all samples in recovery app-limited. isAppLimitedRecovery bool // When true, pace at 1.5x and disable packet conservation in STARTUP. slowerStartup bool // When true, disables packet conservation in STARTUP. rateBasedStartup bool // When non-zero, decreases the rate in STARTUP by the total number of bytes // lost in STARTUP divided by CWND. startupRateReductionMultiplier int64 // Sum of bytes lost in STARTUP. startupBytesLost congestion.ByteCount // When true, add the most recent ack aggregation measurement during STARTUP. enableAckAggregationDuringStartup bool // When true, expire the windowed ack aggregation values in STARTUP when // bandwidth increases more than 25%. expireAckAggregationInStartup bool // If true, will not exit low gain mode until bytes_in_flight drops below BDP // or it's time for high gain mode. drainToTarget bool // If true, use a CWND of 0.75*BDP during probe_rtt instead of 4 packets. probeRttBasedOnBdp bool // If true, skip probe_rtt and update the timestamp of the existing min_rtt to // now if min_rtt over the last cycle is within 12.5% of the current min_rtt. // Even if the min_rtt is 12.5% too low, the 25% gain cycling and 2x CWND gain // should overcome an overly small min_rtt. probeRttSkippedIfSimilarRtt bool // If true, disable PROBE_RTT entirely as long as the connection was recently // app limited. probeRttDisabledIfAppLimited bool appLimitedSinceLastProbeRtt bool minRttSinceLastProbeRtt time.Duration // Latched value of --quic_always_get_bw_sample_when_acked. alwaysGetBwSampleWhenAcked bool pacer *pacer maxDatagramSize congestion.ByteCount } func NewBBRSender( initialMaxDatagramSize, initialCongestionWindow, initialMaxCongestionWindow congestion.ByteCount, ) *bbrSender { b := &bbrSender{ mode: STARTUP, sampler: NewBandwidthSampler(), maxBandwidth: NewWindowedFilter(int64(BandwidthWindowSize), MaxFilter), maxAckHeight: NewWindowedFilter(int64(BandwidthWindowSize), MaxFilter), congestionWindow: initialCongestionWindow, initialCongestionWindow: initialCongestionWindow, highGain: DefaultHighGain, highCwndGain: DefaultHighGain, drainGain: 1.0 / DefaultHighGain, pacingGain: 1.0, congestionWindowGain: 1.0, congestionWindowGainConst: DefaultCongestionWindowGainConst, numStartupRtts: RoundTripsWithoutGrowthBeforeExitingStartup, recoveryState: NOT_IN_RECOVERY, recoveryWindow: initialMaxCongestionWindow, minRttSinceLastProbeRtt: InfiniteRTT, maxDatagramSize: initialMaxDatagramSize, } b.pacer = newPacer(b.BandwidthEstimate) return b } func (b *bbrSender) maxCongestionWindow() congestion.ByteCount { return b.maxDatagramSize * DefaultBBRMaxCongestionWindow } func (b *bbrSender) minCongestionWindow() congestion.ByteCount { return b.maxDatagramSize * b.initialCongestionWindow } func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { b.rttStats = provider } func (b *bbrSender) GetBytesInFlight() congestion.ByteCount { return b.bytesInFlight } // TimeUntilSend returns when the next packet should be sent. func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time { b.bytesInFlight = bytesInFlight return b.pacer.TimeUntilSend() } func (b *bbrSender) HasPacingBudget(now monotime.Time) bool { return b.pacer.Budget(now) >= b.maxDatagramSize } func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { if s < b.maxDatagramSize { panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) } cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow() b.maxDatagramSize = s if cwndIsMinCwnd { b.congestionWindow = b.minCongestionWindow() } b.pacer.SetMaxDatagramSize(s) } func (b *bbrSender) OnPacketSent(sentTime monotime.Time, bytesInFlight congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool) { b.pacer.SentPacket(sentTime, bytes) b.lastSendPacket = packetNumber b.bytesInFlight = bytesInFlight if bytesInFlight == 0 && b.sampler.isAppLimited { b.exitingQuiescence = true } if b.aggregationEpochStartTime.IsZero() { b.aggregationEpochStartTime = sentTime } b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) } func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { b.bytesInFlight = bytesInFlight return bytesInFlight < b.GetCongestionWindow() } func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { if b.mode == PROBE_RTT { return b.ProbeRttCongestionWindow() } if b.InRecovery() && !(b.rateBasedStartup && b.mode == STARTUP) { return minByteCount(b.congestionWindow, b.recoveryWindow) } return b.congestionWindow } func (b *bbrSender) MaybeExitSlowStart() { } func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, priorInFlight congestion.ByteCount, eventTime monotime.Time) { // Stub } func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, priorInFlight congestion.ByteCount) { // Stub } func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { totalBytesAckedBefore := b.sampler.totalBytesAcked isRoundStart, minRttExpired := false, false if lostPackets != nil { b.DiscardLostPackets(lostPackets) } // Input the new data into the BBR model of the connection. var excessAcked congestion.ByteCount if len(ackedPackets) > 0 { lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber isRoundStart = b.UpdateRoundTripCounter(lastAckedPacket) minRttExpired = b.UpdateBandwidthAndMinRtt(eventTime, ackedPackets) b.UpdateRecoveryState(len(lostPackets) > 0, isRoundStart) bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore excessAcked = b.UpdateAckAggregationBytes(eventTime, bytesAcked) } // Handle logic specific to PROBE_BW mode. if b.mode == PROBE_BW { b.UpdateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) > 0) } // Handle logic specific to STARTUP and DRAIN modes. if isRoundStart && !b.isAtFullBandwidth { b.CheckIfFullBandwidthReached() } b.MaybeExitStartupOrDrain(eventTime) // Handle logic specific to PROBE_RTT. b.MaybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) // Calculate number of packets acked and lost. bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore bytesLost := congestion.ByteCount(0) for _, packet := range lostPackets { bytesLost += packet.BytesLost } // After the model is updated, recalculate the pacing rate and congestion // window. b.CalculatePacingRate() b.CalculateCongestionWindow(bytesAcked, excessAcked) b.CalculateRecoveryWindow(bytesAcked, bytesLost) } //func (b *bbrSender) SetNumEmulatedConnections(n int) { // //} func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { } //func (b *bbrSender) OnConnectionMigration() { // //} //// Experiments //func (b *bbrSender) SetSlowStartLargeReduction(enabled bool) { // //} //func (b *bbrSender) BandwidthEstimate() Bandwidth { // return Bandwidth(b.maxBandwidth.GetBest()) //} // BandwidthEstimate returns the current bandwidth estimate func (b *bbrSender) BandwidthEstimate() Bandwidth { if b.rttStats == nil { return infBandwidth } srtt := b.rttStats.SmoothedRTT() if srtt == 0 { // If we haven't measured an rtt, the bandwidth estimate is unknown. return infBandwidth } return BandwidthFromDelta(b.GetCongestionWindow(), srtt) } //func (b *bbrSender) HybridSlowStart() *HybridSlowStart { // return nil //} //func (b *bbrSender) SlowstartThreshold() congestion.ByteCount { // return 0 //} //func (b *bbrSender) RenoBeta() float32 { // return 0.0 //} func (b *bbrSender) InRecovery() bool { return b.recoveryState != NOT_IN_RECOVERY } func (b *bbrSender) InSlowStart() bool { return b.mode == STARTUP } //func (b *bbrSender) ShouldSendProbingPacket() bool { // if b.pacingGain <= 1 { // return false // } // // TODO(b/77975811): If the pipe is highly under-utilized, consider not // // sending a probing transmission, because the extra bandwidth is not needed. // // If flexible_app_limited is enabled, check if the pipe is sufficiently full. // if b.flexibleAppLimited { // return !b.IsPipeSufficientlyFull() // } else { // return true // } //} //func (b *bbrSender) IsPipeSufficientlyFull() bool { // // See if we need more bytes in flight to see more bandwidth. // if b.mode == STARTUP { // // STARTUP exits if it doesn't observe a 25% bandwidth increase, so the CWND // // must be more than 25% above the target. // return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(1.5) // } // if b.pacingGain > 1 { // // Super-unity PROBE_BW doesn't exit until 1.25 * BDP is achieved. // return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(b.pacingGain) // } // // If bytes_in_flight are above the target congestion window, it should be // // possible to observe the same or more bandwidth if it's available. // return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(1.1) //} //func (b *bbrSender) SetFromConfig() { // // TODO: not impl. //} func (b *bbrSender) UpdateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { if b.currentRoundTripEnd == 0 || lastAckedPacket > b.currentRoundTripEnd { b.currentRoundTripEnd = lastAckedPacket b.roundTripCount++ // if b.rttStats != nil && b.InSlowStart() { // TODO: ++stats_->slowstart_num_rtts; // } return true } return false } func (b *bbrSender) UpdateBandwidthAndMinRtt(now monotime.Time, ackedPackets []congestion.AckedPacketInfo) bool { sampleMinRtt := InfiniteRTT for _, packet := range ackedPackets { if !b.alwaysGetBwSampleWhenAcked && packet.BytesAcked == 0 { // Skip acked packets with 0 in flight bytes when updating bandwidth. return false } bandwidthSample := b.sampler.OnPacketAcked(now, packet.PacketNumber) if b.alwaysGetBwSampleWhenAcked && !bandwidthSample.stateAtSend.isValid { // From the sampler's perspective, the packet has never been sent, or the // packet has been acked or marked as lost previously. return false } b.lastSampleIsAppLimited = bandwidthSample.stateAtSend.isAppLimited // has_non_app_limited_sample_ |= // !bandwidth_sample.state_at_send.is_app_limited; if !bandwidthSample.stateAtSend.isAppLimited { b.hasNoAppLimitedSample = true } if bandwidthSample.rtt > 0 { sampleMinRtt = minRtt(sampleMinRtt, bandwidthSample.rtt) } if !bandwidthSample.stateAtSend.isAppLimited || bandwidthSample.bandwidth > b.BandwidthEstimate() { b.maxBandwidth.Update(int64(bandwidthSample.bandwidth), b.roundTripCount) } } // If none of the RTT samples are valid, return immediately. if sampleMinRtt == InfiniteRTT { return false } b.minRttSinceLastProbeRtt = minRtt(b.minRttSinceLastProbeRtt, sampleMinRtt) // Do not expire min_rtt if none was ever available. minRttExpired := b.minRtt > 0 && (now.After(b.minRttTimestamp.Add(MinRttExpiry))) if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { if minRttExpired && b.ShouldExtendMinRttExpiry() { minRttExpired = false } else { b.minRtt = sampleMinRtt } b.minRttTimestamp = now // Reset since_last_probe_rtt fields. b.minRttSinceLastProbeRtt = InfiniteRTT b.appLimitedSinceLastProbeRtt = false } return minRttExpired } func (b *bbrSender) ShouldExtendMinRttExpiry() bool { if b.probeRttDisabledIfAppLimited && b.appLimitedSinceLastProbeRtt { // Extend the current min_rtt if we've been app limited recently. return true } minRttIncreasedSinceLastProbe := b.minRttSinceLastProbeRtt > time.Duration(float64(b.minRtt)*SimilarMinRttThreshold) if b.probeRttSkippedIfSimilarRtt && b.appLimitedSinceLastProbeRtt && !minRttIncreasedSinceLastProbe { // Extend the current min_rtt if we've been app limited recently and an rtt // has been measured in that time that's less than 12.5% more than the // current min_rtt. return true } return false } func (b *bbrSender) DiscardLostPackets(lostPackets []congestion.LostPacketInfo) { for _, packet := range lostPackets { b.sampler.OnCongestionEvent(packet.PacketNumber) if b.mode == STARTUP { // if b.rttStats != nil { // TODO: slow start. // } if b.startupRateReductionMultiplier != 0 { b.startupBytesLost += packet.BytesLost } } } } func (b *bbrSender) UpdateRecoveryState(hasLosses, isRoundStart bool) { // Exit recovery when there are no losses for a round. if !hasLosses { b.endRecoveryAt = b.lastSendPacket } switch b.recoveryState { case NOT_IN_RECOVERY: // Enter conservation on the first loss. if hasLosses { b.recoveryState = CONSERVATION // This will cause the |recovery_window_| to be set to the correct // value in CalculateRecoveryWindow(). b.recoveryWindow = 0 // Since the conservation phase is meant to be lasting for a whole // round, extend the current round as if it were started right now. b.currentRoundTripEnd = b.lastSendPacket if false && b.lastSampleIsAppLimited { b.isAppLimitedRecovery = true } } case CONSERVATION: if isRoundStart { b.recoveryState = GROWTH } fallthrough case GROWTH: // Exit recovery if appropriate. if !hasLosses && b.lastSendPacket > b.endRecoveryAt { b.recoveryState = NOT_IN_RECOVERY b.isAppLimitedRecovery = false } } if b.recoveryState != NOT_IN_RECOVERY && b.isAppLimitedRecovery { b.sampler.OnAppLimited() } } func (b *bbrSender) UpdateAckAggregationBytes(ackTime monotime.Time, ackedBytes congestion.ByteCount) congestion.ByteCount { // Compute how many bytes are expected to be delivered, assuming max bandwidth // is correct. expectedAckedBytes := congestion.ByteCount(b.maxBandwidth.GetBest()) * congestion.ByteCount((ackTime.Sub(b.aggregationEpochStartTime))) // Reset the current aggregation epoch as soon as the ack arrival rate is less // than or equal to the max bandwidth. if b.aggregationEpochBytes <= expectedAckedBytes { // Reset to start measuring a new aggregation epoch. b.aggregationEpochBytes = ackedBytes b.aggregationEpochStartTime = ackTime return 0 } // Compute how many extra bytes were delivered vs max bandwidth. // Include the bytes most recently acknowledged to account for stretch acks. b.aggregationEpochBytes += ackedBytes b.maxAckHeight.Update(int64(b.aggregationEpochBytes-expectedAckedBytes), b.roundTripCount) return b.aggregationEpochBytes - expectedAckedBytes } func (b *bbrSender) UpdateGainCyclePhase(now monotime.Time, priorInFlight congestion.ByteCount, hasLosses bool) { bytesInFlight := b.GetBytesInFlight() // In most cases, the cycle is advanced after an RTT passes. shouldAdvanceGainCycling := now.Sub(b.lastCycleStart) > b.GetMinRtt() // If the pacing gain is above 1.0, the connection is trying to probe the // bandwidth by increasing the number of bytes in flight to at least // pacing_gain * BDP. Make sure that it actually reaches the target, as long // as there are no losses suggesting that the buffers are not able to hold // that much. if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.GetTargetCongestionWindow(b.pacingGain) { shouldAdvanceGainCycling = false } // If pacing gain is below 1.0, the connection is trying to drain the extra // queue which could have been incurred by probing prior to it. If the number // of bytes in flight falls down to the estimated BDP value earlier, conclude // that the queue has been successfully drained and exit this cycle early. if b.pacingGain < 1.0 && bytesInFlight <= b.GetTargetCongestionWindow(1.0) { shouldAdvanceGainCycling = true } if shouldAdvanceGainCycling { b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % GainCycleLength b.lastCycleStart = now // Stay in low gain mode until the target BDP is hit. // Low gain mode will be exited immediately when the target BDP is achieved. if b.drainToTarget && b.pacingGain < 1.0 && PacingGain[b.cycleCurrentOffset] == 1.0 && bytesInFlight > b.GetTargetCongestionWindow(1.0) { return } b.pacingGain = PacingGain[b.cycleCurrentOffset] } } func (b *bbrSender) GetTargetCongestionWindow(gain float64) congestion.ByteCount { bdp := congestion.ByteCount(b.GetMinRtt()) * congestion.ByteCount(b.BandwidthEstimate()) congestionWindow := congestion.ByteCount(gain * float64(bdp)) // BDP estimate will be zero if no bandwidth samples are available yet. if congestionWindow == 0 { congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) } return maxByteCount(congestionWindow, b.minCongestionWindow()) } func (b *bbrSender) CheckIfFullBandwidthReached() { if b.lastSampleIsAppLimited { return } target := Bandwidth(float64(b.bandwidthAtLastRound) * StartupGrowthTarget) if b.BandwidthEstimate() >= target { b.bandwidthAtLastRound = b.BandwidthEstimate() b.roundsWithoutBandwidthGain = 0 if b.expireAckAggregationInStartup { // Expire old excess delivery measurements now that bandwidth increased. b.maxAckHeight.Reset(0, b.roundTripCount) } return } b.roundsWithoutBandwidthGain++ if b.roundsWithoutBandwidthGain >= b.numStartupRtts || (b.exitStartupOnLoss && b.InRecovery()) { b.isAtFullBandwidth = true } } func (b *bbrSender) MaybeExitStartupOrDrain(now monotime.Time) { if b.mode == STARTUP && b.isAtFullBandwidth { b.OnExitStartup(now) b.mode = DRAIN b.pacingGain = b.drainGain b.congestionWindowGain = b.highCwndGain } if b.mode == DRAIN && b.GetBytesInFlight() <= b.GetTargetCongestionWindow(1) { b.EnterProbeBandwidthMode(now) } } func (b *bbrSender) EnterProbeBandwidthMode(now monotime.Time) { b.mode = PROBE_BW b.congestionWindowGain = b.congestionWindowGainConst // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is // excluded because in that case increased gain and decreased gain would not // follow each other. b.cycleCurrentOffset = randv2.Int() % (GainCycleLength - 1) if b.cycleCurrentOffset >= 1 { b.cycleCurrentOffset += 1 } b.lastCycleStart = now b.pacingGain = PacingGain[b.cycleCurrentOffset] } func (b *bbrSender) MaybeEnterOrExitProbeRtt(now monotime.Time, isRoundStart, minRttExpired bool) { if minRttExpired && !b.exitingQuiescence && b.mode != PROBE_RTT { if b.InSlowStart() { b.OnExitStartup(now) } b.mode = PROBE_RTT b.pacingGain = 1.0 // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| // is at the target small value. b.exitProbeRttAt = monotime.Time(0) } if b.mode == PROBE_RTT { b.sampler.OnAppLimited() if b.exitProbeRttAt.IsZero() { // If the window has reached the appropriate size, schedule exiting // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but // we allow an extra packet since QUIC checks CWND before sending a // packet. if b.GetBytesInFlight() < b.ProbeRttCongestionWindow()+b.maxDatagramSize { b.exitProbeRttAt = now.Add(ProbeRttTime) b.probeRttRoundPassed = false } } else { if isRoundStart { b.probeRttRoundPassed = true } if !now.Before(b.exitProbeRttAt) && b.probeRttRoundPassed { b.minRttTimestamp = now if !b.isAtFullBandwidth { b.EnterStartupMode(now) } else { b.EnterProbeBandwidthMode(now) } } } } b.exitingQuiescence = false } func (b *bbrSender) ProbeRttCongestionWindow() congestion.ByteCount { if b.probeRttBasedOnBdp { return b.GetTargetCongestionWindow(ModerateProbeRttMultiplier) } else { return b.minCongestionWindow() } } func (b *bbrSender) EnterStartupMode(now monotime.Time) { // if b.rttStats != nil { // TODO: slow start. // } b.mode = STARTUP b.pacingGain = b.highGain b.congestionWindowGain = b.highCwndGain } func (b *bbrSender) OnExitStartup(now monotime.Time) { if b.rttStats == nil { return } // TODO: slow start. } func (b *bbrSender) CalculatePacingRate() { if b.BandwidthEstimate() == 0 { return } targetRate := Bandwidth(b.pacingGain * float64(b.BandwidthEstimate())) if b.isAtFullBandwidth { b.pacingRate = targetRate return } // Pace at the rate of initial_window / RTT as soon as RTT measurements are // available. if b.pacingRate == 0 && b.rttStats.MinRTT() > 0 { b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) return } // Slow the pacing rate in STARTUP once loss has ever been detected. hasEverDetectedLoss := b.endRecoveryAt > 0 if b.slowerStartup && hasEverDetectedLoss && b.hasNoAppLimitedSample { b.pacingRate = Bandwidth(StartupAfterLossGain * float64(b.BandwidthEstimate())) return } // Slow the pacing rate in STARTUP by the bytes_lost / CWND. if b.startupRateReductionMultiplier != 0 && hasEverDetectedLoss && b.hasNoAppLimitedSample { b.pacingRate = Bandwidth((1.0 - (float64(b.startupBytesLost) * float64(b.startupRateReductionMultiplier) / float64(b.congestionWindow))) * float64(targetRate)) // Ensure the pacing rate doesn't drop below the startup growth target times // the bandwidth estimate. b.pacingRate = maxBandwidth(b.pacingRate, Bandwidth(StartupGrowthTarget*float64(b.BandwidthEstimate()))) return } // Do not decrease the pacing rate during startup. b.pacingRate = maxBandwidth(b.pacingRate, targetRate) } func (b *bbrSender) CalculateCongestionWindow(ackedBytes, excessAcked congestion.ByteCount) { if b.mode == PROBE_RTT { return } targetWindow := b.GetTargetCongestionWindow(b.congestionWindowGain) if b.isAtFullBandwidth { // Add the max recently measured ack aggregation to CWND. targetWindow += congestion.ByteCount(b.maxAckHeight.GetBest()) } else if b.enableAckAggregationDuringStartup { // Add the most recent excess acked. Because CWND never decreases in // STARTUP, this will automatically create a very localized max filter. targetWindow += excessAcked } // Instead of immediately setting the target CWND as the new one, BBR grows // the CWND towards |target_window| by only increasing it |bytes_acked| at a // time. addBytesAcked := true || !b.InRecovery() if b.isAtFullBandwidth { b.congestionWindow = minByteCount(targetWindow, b.congestionWindow+ackedBytes) } else if addBytesAcked && (b.congestionWindow < targetWindow || b.sampler.totalBytesAcked < b.initialCongestionWindow) { // If the connection is not yet out of startup phase, do not decrease the // window. b.congestionWindow += ackedBytes } // Enforce the limits on the congestion window. b.congestionWindow = maxByteCount(b.congestionWindow, b.minCongestionWindow()) b.congestionWindow = minByteCount(b.congestionWindow, b.maxCongestionWindow()) } func (b *bbrSender) CalculateRecoveryWindow(ackedBytes, lostBytes congestion.ByteCount) { if b.rateBasedStartup && b.mode == STARTUP { return } if b.recoveryState == NOT_IN_RECOVERY { return } // Set up the initial recovery window. if b.recoveryWindow == 0 { b.recoveryWindow = maxByteCount(b.GetBytesInFlight()+ackedBytes, b.minCongestionWindow()) return } // Remove losses from the recovery window, while accounting for a potential // integer underflow. if b.recoveryWindow >= lostBytes { b.recoveryWindow -= lostBytes } else { b.recoveryWindow = congestion.ByteCount(b.maxDatagramSize) } // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, // release additional |bytes_acked| to achieve a slow-start-like behavior. if b.recoveryState == GROWTH { b.recoveryWindow += ackedBytes } // Sanity checks. Ensure that we always allow to send at least an MSS or // |bytes_acked| in response, whichever is larger. b.recoveryWindow = maxByteCount(b.recoveryWindow, b.GetBytesInFlight()+ackedBytes) b.recoveryWindow = maxByteCount(b.recoveryWindow, b.minCongestionWindow()) } var _ congestion.CongestionControl = (*bbrSender)(nil) func (b *bbrSender) GetMinRtt() time.Duration { if b.minRtt > 0 { return b.minRtt } else { return InitialRtt } } func minRtt(a, b time.Duration) time.Duration { if a < b { return a } else { return b } } func minBandwidth(a, b Bandwidth) Bandwidth { if a < b { return a } else { return b } } func maxBandwidth(a, b Bandwidth) Bandwidth { if a > b { return a } else { return b } } func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount { if a > b { return a } else { return b } } func minByteCount(a, b congestion.ByteCount) congestion.ByteCount { if a < b { return a } else { return b } } var ( InfiniteRTT = time.Duration(math.MaxInt64) ) ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/cubic.go ================================================ package congestion import ( "math" "time" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" ) // This cubic implementation is based on the one found in Chromiums's QUIC // implementation, in the files net/quic/congestion_control/cubic.{hh,cc}. // Constants based on TCP defaults. // The following constants are in 2^10 fractions of a second instead of ms to // allow a 10 shift right to divide. // 1024*1024^3 (first 1024 is from 0.100^3) // where 0.100 is 100 ms which is the scaling round trip time. const ( cubeScale = 40 cubeCongestionWindowScale = 410 cubeFactor congestion.ByteCount = 1 << cubeScale / cubeCongestionWindowScale / maxDatagramSize // TODO: when re-enabling cubic, make sure to use the actual packet size here maxDatagramSize = congestion.ByteCount(InitialPacketSize) ) const defaultNumConnections = 1 // Default Cubic backoff factor const beta float32 = 0.7 // Additional backoff factor when loss occurs in the concave part of the Cubic // curve. This additional backoff factor is expected to give up bandwidth to // new concurrent flows and speed up convergence. const betaLastMax float32 = 0.85 // Cubic implements the cubic algorithm from TCP type Cubic struct { // Number of connections to simulate. numConnections int // Time when this cycle started, after last loss event. epoch monotime.Time // Max congestion window used just before last loss event. // Note: to improve fairness to other streams an additional back off is // applied to this value if the new value is below our latest value. lastMaxCongestionWindow congestion.ByteCount // Number of acked bytes since the cycle started (epoch). ackedBytesCount congestion.ByteCount // TCP Reno equivalent congestion window in packets. estimatedTCPcongestionWindow congestion.ByteCount // Origin point of cubic function. originPointCongestionWindow congestion.ByteCount // Time to origin point of cubic function in 2^10 fractions of a second. timeToOriginPoint uint32 // Last congestion window in packets computed by cubic function. lastTargetCongestionWindow congestion.ByteCount } // NewCubic returns a new Cubic instance func NewCubic() *Cubic { c := &Cubic{ numConnections: defaultNumConnections, } c.Reset() return c } // Reset is called after a timeout to reset the cubic state func (c *Cubic) Reset() { c.epoch = monotime.Time(0) c.lastMaxCongestionWindow = 0 c.ackedBytesCount = 0 c.estimatedTCPcongestionWindow = 0 c.originPointCongestionWindow = 0 c.timeToOriginPoint = 0 c.lastTargetCongestionWindow = 0 } func (c *Cubic) alpha() float32 { // TCPFriendly alpha is described in Section 3.3 of the CUBIC paper. Note that // beta here is a cwnd multiplier, and is equal to 1-beta from the paper. // We derive the equivalent alpha for an N-connection emulation as: b := c.beta() return 3 * float32(c.numConnections) * float32(c.numConnections) * (1 - b) / (1 + b) } func (c *Cubic) beta() float32 { // kNConnectionBeta is the backoff factor after loss for our N-connection // emulation, which emulates the effective backoff of an ensemble of N // TCP-Reno connections on a single loss event. The effective multiplier is // computed as: return (float32(c.numConnections) - 1 + beta) / float32(c.numConnections) } func (c *Cubic) betaLastMax() float32 { // betaLastMax is the additional backoff factor after loss for our // N-connection emulation, which emulates the additional backoff of // an ensemble of N TCP-Reno connections on a single loss event. The // effective multiplier is computed as: return (float32(c.numConnections) - 1 + betaLastMax) / float32(c.numConnections) } // OnApplicationLimited is called on ack arrival when sender is unable to use // the available congestion window. Resets Cubic state during quiescence. func (c *Cubic) OnApplicationLimited() { // When sender is not using the available congestion window, the window does // not grow. But to be RTT-independent, Cubic assumes that the sender has been // using the entire window during the time since the beginning of the current // "epoch" (the end of the last loss recovery period). Since // application-limited periods break this assumption, we reset the epoch when // in such a period. This reset effectively freezes congestion window growth // through application-limited periods and allows Cubic growth to continue // when the entire window is being used. c.epoch = monotime.Time(0) } // CongestionWindowAfterPacketLoss computes a new congestion window to use after // a loss event. Returns the new congestion window in packets. The new // congestion window is a multiplicative decrease of our current window. func (c *Cubic) CongestionWindowAfterPacketLoss(currentCongestionWindow congestion.ByteCount) congestion.ByteCount { if currentCongestionWindow+maxDatagramSize < c.lastMaxCongestionWindow { // We never reached the old max, so assume we are competing with another // flow. Use our extra back off factor to allow the other flow to go up. c.lastMaxCongestionWindow = congestion.ByteCount(c.betaLastMax() * float32(currentCongestionWindow)) } else { c.lastMaxCongestionWindow = currentCongestionWindow } c.epoch = monotime.Time(0) // Reset time. return congestion.ByteCount(float32(currentCongestionWindow) * c.beta()) } // CongestionWindowAfterAck computes a new congestion window to use after a received ACK. // Returns the new congestion window in packets. The new congestion window // follows a cubic function that depends on the time passed since last // packet loss. func (c *Cubic) CongestionWindowAfterAck( ackedBytes congestion.ByteCount, currentCongestionWindow congestion.ByteCount, delayMin time.Duration, eventTime monotime.Time, ) congestion.ByteCount { c.ackedBytesCount += ackedBytes if c.epoch.IsZero() { // First ACK after a loss event. c.epoch = eventTime // Start of epoch. c.ackedBytesCount = ackedBytes // Reset count. // Reset estimated_tcp_congestion_window_ to be in sync with cubic. c.estimatedTCPcongestionWindow = currentCongestionWindow if c.lastMaxCongestionWindow <= currentCongestionWindow { c.timeToOriginPoint = 0 c.originPointCongestionWindow = currentCongestionWindow } else { c.timeToOriginPoint = uint32(math.Cbrt(float64(cubeFactor * (c.lastMaxCongestionWindow - currentCongestionWindow)))) c.originPointCongestionWindow = c.lastMaxCongestionWindow } } // Change the time unit from microseconds to 2^10 fractions per second. Take // the round trip time in account. This is done to allow us to use shift as a // divide operator. elapsedTime := int64(eventTime.Add(delayMin).Sub(c.epoch)/time.Microsecond) << 10 / (1000 * 1000) // Right-shifts of negative, signed numbers have implementation-dependent // behavior, so force the offset to be positive, as is done in the kernel. offset := int64(c.timeToOriginPoint) - elapsedTime if offset < 0 { offset = -offset } deltaCongestionWindow := congestion.ByteCount(cubeCongestionWindowScale*offset*offset*offset) * maxDatagramSize >> cubeScale var targetCongestionWindow congestion.ByteCount if elapsedTime > int64(c.timeToOriginPoint) { targetCongestionWindow = c.originPointCongestionWindow + deltaCongestionWindow } else { targetCongestionWindow = c.originPointCongestionWindow - deltaCongestionWindow } // Limit the CWND increase to half the acked bytes. targetCongestionWindow = Min(targetCongestionWindow, currentCongestionWindow+c.ackedBytesCount/2) // Increase the window by approximately Alpha * 1 MSS of bytes every // time we ack an estimated tcp window of bytes. For small // congestion windows (less than 25), the formula below will // increase slightly slower than linearly per estimated tcp window // of bytes. c.estimatedTCPcongestionWindow += congestion.ByteCount(float32(c.ackedBytesCount) * c.alpha() * float32(maxDatagramSize) / float32(c.estimatedTCPcongestionWindow)) c.ackedBytesCount = 0 // We have a new cubic congestion window. c.lastTargetCongestionWindow = targetCongestionWindow // Compute target congestion_window based on cubic target and estimated TCP // congestion_window, use highest (fastest). if targetCongestionWindow < c.estimatedTCPcongestionWindow { targetCongestionWindow = c.estimatedTCPcongestionWindow } return targetCongestionWindow } // SetNumConnections sets the number of emulated connections func (c *Cubic) SetNumConnections(n int) { c.numConnections = n } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/cubic_sender.go ================================================ package congestion import ( "fmt" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" ) const ( maxBurstPackets = 3 renoBeta = 0.7 // Reno backoff factor. minCongestionWindowPackets = 2 initialCongestionWindow = 32 ) const InvalidPacketNumber congestion.PacketNumber = -1 const MaxCongestionWindowPackets = 20000 const MaxByteCount = congestion.ByteCount(1<<62 - 1) type cubicSender struct { hybridSlowStart HybridSlowStart rttStats congestion.RTTStatsProvider cubic *Cubic pacer *pacer reno bool // Track the largest packet that has been sent. largestSentPacketNumber congestion.PacketNumber // Track the largest packet that has been acked. largestAckedPacketNumber congestion.PacketNumber // Track the largest packet number outstanding when a CWND cutback occurs. largestSentAtLastCutback congestion.PacketNumber // Whether the last loss event caused us to exit slowstart. // Used for stats collection of slowstartPacketsLost lastCutbackExitedSlowstart bool // Congestion window in bytes. congestionWindow congestion.ByteCount // Slow start congestion window in bytes, aka ssthresh. slowStartThreshold congestion.ByteCount // ACK counter for the Reno implementation. numAckedPackets uint64 initialCongestionWindow congestion.ByteCount initialMaxCongestionWindow congestion.ByteCount maxDatagramSize congestion.ByteCount } var ( _ congestion.CongestionControl = &cubicSender{} ) // NewCubicSender makes a new cubic sender func NewCubicSender( initialMaxDatagramSize congestion.ByteCount, reno bool, ) *cubicSender { return newCubicSender( reno, initialMaxDatagramSize, initialCongestionWindow*initialMaxDatagramSize, MaxCongestionWindowPackets*initialMaxDatagramSize, ) } func newCubicSender( reno bool, initialMaxDatagramSize, initialCongestionWindow, initialMaxCongestionWindow congestion.ByteCount, ) *cubicSender { c := &cubicSender{ largestSentPacketNumber: InvalidPacketNumber, largestAckedPacketNumber: InvalidPacketNumber, largestSentAtLastCutback: InvalidPacketNumber, initialCongestionWindow: initialCongestionWindow, initialMaxCongestionWindow: initialMaxCongestionWindow, congestionWindow: initialCongestionWindow, slowStartThreshold: MaxByteCount, cubic: NewCubic(), reno: reno, maxDatagramSize: initialMaxDatagramSize, } c.pacer = newPacer(c.BandwidthEstimate) return c } func (c *cubicSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { c.rttStats = provider } // TimeUntilSend returns when the next packet should be sent. func (c *cubicSender) TimeUntilSend(_ congestion.ByteCount) monotime.Time { return c.pacer.TimeUntilSend() } func (c *cubicSender) HasPacingBudget(now monotime.Time) bool { return c.pacer.Budget(now) >= c.maxDatagramSize } func (c *cubicSender) maxCongestionWindow() congestion.ByteCount { return c.maxDatagramSize * MaxCongestionWindowPackets } func (c *cubicSender) minCongestionWindow() congestion.ByteCount { return c.maxDatagramSize * minCongestionWindowPackets } func (c *cubicSender) OnPacketSent( sentTime monotime.Time, _ congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, ) { c.pacer.SentPacket(sentTime, bytes) if !isRetransmittable { return } c.largestSentPacketNumber = packetNumber c.hybridSlowStart.OnPacketSent(packetNumber) } func (c *cubicSender) CanSend(bytesInFlight congestion.ByteCount) bool { return bytesInFlight < c.GetCongestionWindow() } func (c *cubicSender) InRecovery() bool { return c.largestAckedPacketNumber != InvalidPacketNumber && c.largestAckedPacketNumber <= c.largestSentAtLastCutback } func (c *cubicSender) InSlowStart() bool { return c.GetCongestionWindow() < c.slowStartThreshold } func (c *cubicSender) GetCongestionWindow() congestion.ByteCount { return c.congestionWindow } func (c *cubicSender) MaybeExitSlowStart() { if c.InSlowStart() && c.hybridSlowStart.ShouldExitSlowStart(c.rttStats.LatestRTT(), c.rttStats.MinRTT(), c.GetCongestionWindow()/c.maxDatagramSize) { // exit slow start c.slowStartThreshold = c.congestionWindow } } func (c *cubicSender) OnPacketAcked( ackedPacketNumber congestion.PacketNumber, ackedBytes congestion.ByteCount, priorInFlight congestion.ByteCount, eventTime monotime.Time, ) { c.largestAckedPacketNumber = Max(ackedPacketNumber, c.largestAckedPacketNumber) if c.InRecovery() { return } c.maybeIncreaseCwnd(ackedPacketNumber, ackedBytes, priorInFlight, eventTime) if c.InSlowStart() { c.hybridSlowStart.OnPacketAcked(ackedPacketNumber) } } func (c *cubicSender) OnCongestionEvent(packetNumber congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { // TCP NewReno (RFC6582) says that once a loss occurs, any losses in packets // already sent should be treated as a single loss event, since it's expected. if packetNumber <= c.largestSentAtLastCutback { return } c.lastCutbackExitedSlowstart = c.InSlowStart() if c.reno { c.congestionWindow = congestion.ByteCount(float64(c.congestionWindow) * renoBeta) } else { c.congestionWindow = c.cubic.CongestionWindowAfterPacketLoss(c.congestionWindow) } if minCwnd := c.minCongestionWindow(); c.congestionWindow < minCwnd { c.congestionWindow = minCwnd } c.slowStartThreshold = c.congestionWindow c.largestSentAtLastCutback = c.largestSentPacketNumber // reset packet count from congestion avoidance mode. We start // counting again when we're out of recovery. c.numAckedPackets = 0 } func (b *cubicSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { // Stub } // Called when we receive an ack. Normal TCP tracks how many packets one ack // represents, but quic has a separate ack for each packet. func (c *cubicSender) maybeIncreaseCwnd( _ congestion.PacketNumber, ackedBytes congestion.ByteCount, priorInFlight congestion.ByteCount, eventTime monotime.Time, ) { // Do not increase the congestion window unless the sender is close to using // the current window. if !c.isCwndLimited(priorInFlight) { c.cubic.OnApplicationLimited() return } if c.congestionWindow >= c.maxCongestionWindow() { return } if c.InSlowStart() { // TCP slow start, exponential growth, increase by one for each ACK. c.congestionWindow += c.maxDatagramSize return } // Congestion avoidance if c.reno { // Classic Reno congestion avoidance. c.numAckedPackets++ if c.numAckedPackets >= uint64(c.congestionWindow/c.maxDatagramSize) { c.congestionWindow += c.maxDatagramSize c.numAckedPackets = 0 } } else { c.congestionWindow = Min(c.maxCongestionWindow(), c.cubic.CongestionWindowAfterAck(ackedBytes, c.congestionWindow, c.rttStats.MinRTT(), eventTime)) } } func (c *cubicSender) isCwndLimited(bytesInFlight congestion.ByteCount) bool { congestionWindow := c.GetCongestionWindow() if bytesInFlight >= congestionWindow { return true } availableBytes := congestionWindow - bytesInFlight slowStartLimited := c.InSlowStart() && bytesInFlight > congestionWindow/2 return slowStartLimited || availableBytes <= maxBurstPackets*c.maxDatagramSize } // BandwidthEstimate returns the current bandwidth estimate func (c *cubicSender) BandwidthEstimate() Bandwidth { if c.rttStats == nil { return infBandwidth } srtt := c.rttStats.SmoothedRTT() if srtt == 0 { // If we haven't measured an rtt, the bandwidth estimate is unknown. return infBandwidth } return BandwidthFromDelta(c.GetCongestionWindow(), srtt) } // OnRetransmissionTimeout is called on an retransmission timeout func (c *cubicSender) OnRetransmissionTimeout(packetsRetransmitted bool) { c.largestSentAtLastCutback = InvalidPacketNumber if !packetsRetransmitted { return } c.hybridSlowStart.Restart() c.cubic.Reset() c.slowStartThreshold = c.congestionWindow / 2 c.congestionWindow = c.minCongestionWindow() } // OnConnectionMigration is called when the connection is migrated (?) func (c *cubicSender) OnConnectionMigration() { c.hybridSlowStart.Restart() c.largestSentPacketNumber = InvalidPacketNumber c.largestAckedPacketNumber = InvalidPacketNumber c.largestSentAtLastCutback = InvalidPacketNumber c.lastCutbackExitedSlowstart = false c.cubic.Reset() c.numAckedPackets = 0 c.congestionWindow = c.initialCongestionWindow c.slowStartThreshold = c.initialMaxCongestionWindow } func (c *cubicSender) SetMaxDatagramSize(s congestion.ByteCount) { if s < c.maxDatagramSize { panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", c.maxDatagramSize, s)) } cwndIsMinCwnd := c.congestionWindow == c.minCongestionWindow() c.maxDatagramSize = s if cwndIsMinCwnd { c.congestionWindow = c.minCongestionWindow() } c.pacer.SetMaxDatagramSize(s) } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/hybrid_slow_start.go ================================================ package congestion import ( "time" "github.com/metacubex/quic-go/congestion" ) // Note(pwestin): the magic clamping numbers come from the original code in // tcp_cubic.c. const hybridStartLowWindow = congestion.ByteCount(16) // Number of delay samples for detecting the increase of delay. const hybridStartMinSamples = uint32(8) // Exit slow start if the min rtt has increased by more than 1/8th. const hybridStartDelayFactorExp = 3 // 2^3 = 8 // The original paper specifies 2 and 8ms, but those have changed over time. const ( hybridStartDelayMinThresholdUs = int64(4000) hybridStartDelayMaxThresholdUs = int64(16000) ) // HybridSlowStart implements the TCP hybrid slow start algorithm type HybridSlowStart struct { endPacketNumber congestion.PacketNumber lastSentPacketNumber congestion.PacketNumber started bool currentMinRTT time.Duration rttSampleCount uint32 hystartFound bool } // StartReceiveRound is called for the start of each receive round (burst) in the slow start phase. func (s *HybridSlowStart) StartReceiveRound(lastSent congestion.PacketNumber) { s.endPacketNumber = lastSent s.currentMinRTT = 0 s.rttSampleCount = 0 s.started = true } // IsEndOfRound returns true if this ack is the last packet number of our current slow start round. func (s *HybridSlowStart) IsEndOfRound(ack congestion.PacketNumber) bool { return s.endPacketNumber < ack } // ShouldExitSlowStart should be called on every new ack frame, since a new // RTT measurement can be made then. // rtt: the RTT for this ack packet. // minRTT: is the lowest delay (RTT) we have seen during the session. // congestionWindow: the congestion window in packets. func (s *HybridSlowStart) ShouldExitSlowStart(latestRTT time.Duration, minRTT time.Duration, congestionWindow congestion.ByteCount) bool { if !s.started { // Time to start the hybrid slow start. s.StartReceiveRound(s.lastSentPacketNumber) } if s.hystartFound { return true } // Second detection parameter - delay increase detection. // Compare the minimum delay (s.currentMinRTT) of the current // burst of packets relative to the minimum delay during the session. // Note: we only look at the first few(8) packets in each burst, since we // only want to compare the lowest RTT of the burst relative to previous // bursts. s.rttSampleCount++ if s.rttSampleCount <= hybridStartMinSamples { if s.currentMinRTT == 0 || s.currentMinRTT > latestRTT { s.currentMinRTT = latestRTT } } // We only need to check this once per round. if s.rttSampleCount == hybridStartMinSamples { // Divide minRTT by 8 to get a rtt increase threshold for exiting. minRTTincreaseThresholdUs := int64(minRTT / time.Microsecond >> hybridStartDelayFactorExp) // Ensure the rtt threshold is never less than 2ms or more than 16ms. minRTTincreaseThresholdUs = Min(minRTTincreaseThresholdUs, hybridStartDelayMaxThresholdUs) minRTTincreaseThreshold := time.Duration(Max(minRTTincreaseThresholdUs, hybridStartDelayMinThresholdUs)) * time.Microsecond if s.currentMinRTT > (minRTT + minRTTincreaseThreshold) { s.hystartFound = true } } // Exit from slow start if the cwnd is greater than 16 and // increasing delay is found. return congestionWindow >= hybridStartLowWindow && s.hystartFound } // OnPacketSent is called when a packet was sent func (s *HybridSlowStart) OnPacketSent(packetNumber congestion.PacketNumber) { s.lastSentPacketNumber = packetNumber } // OnPacketAcked gets invoked after ShouldExitSlowStart, so it's best to end // the round when the final packet of the burst is received and start it on // the next incoming ack. func (s *HybridSlowStart) OnPacketAcked(ackedPacketNumber congestion.PacketNumber) { if s.IsEndOfRound(ackedPacketNumber) { s.started = false } } // Started returns true if started func (s *HybridSlowStart) Started() bool { return s.started } // Restart the slow start phase func (s *HybridSlowStart) Restart() { s.started = false s.hystartFound = false } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/minmax.go ================================================ package congestion import ( "math" "time" ) // InfDuration is a duration of infinite length const InfDuration = time.Duration(math.MaxInt64) // MinNonZeroDuration return the minimum duration that's not zero. func MinNonZeroDuration(a, b time.Duration) time.Duration { if a == 0 { return b } if b == 0 { return a } return Min(a, b) } // AbsDuration returns the absolute value of a time duration func AbsDuration(d time.Duration) time.Duration { if d >= 0 { return d } return -d } // MinTime returns the earlier time func MinTime(a, b time.Time) time.Time { if a.After(b) { return b } return a } // MinNonZeroTime returns the earlist time that is not time.Time{} // If both a and b are time.Time{}, it returns time.Time{} func MinNonZeroTime(a, b time.Time) time.Time { if a.IsZero() { return b } if b.IsZero() { return a } return MinTime(a, b) } // MaxTime returns the later time func MaxTime(a, b time.Time) time.Time { if a.After(b) { return a } return b } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/minmax_go120.go ================================================ //go:build !go1.21 package congestion import "golang.org/x/exp/constraints" func Max[T constraints.Ordered](a, b T) T { if a < b { return b } return a } func Min[T constraints.Ordered](a, b T) T { if a < b { return a } return b } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/minmax_go121.go ================================================ //go:build go1.21 package congestion import "cmp" func Max[T cmp.Ordered](a, b T) T { return max(a, b) } func Min[T cmp.Ordered](a, b T) T { return min(a, b) } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/pacer.go ================================================ package congestion import ( "math" "time" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" ) const initialMaxDatagramSize = congestion.ByteCount(1252) const MinPacingDelay = time.Millisecond const TimerGranularity = time.Millisecond const maxBurstSizePackets = 10 // The pacer implements a token bucket pacing algorithm. type pacer struct { budgetAtLastSent congestion.ByteCount maxDatagramSize congestion.ByteCount lastSentTime monotime.Time getAdjustedBandwidth func() uint64 // in bytes/s } func newPacer(getBandwidth func() Bandwidth) *pacer { p := &pacer{ maxDatagramSize: initialMaxDatagramSize, getAdjustedBandwidth: func() uint64 { // Bandwidth is in bits/s. We need the value in bytes/s. bw := uint64(getBandwidth() / BytesPerSecond) // Use a slightly higher value than the actual measured bandwidth. // RTT variations then won't result in under-utilization of the congestion window. // Ultimately, this will result in sending packets as acknowledgments are received rather than when timers fire, // provided the congestion window is fully utilized and acknowledgments arrive at regular intervals. return bw * 5 / 4 }, } p.budgetAtLastSent = p.maxBurstSize() return p } func (p *pacer) SentPacket(sendTime monotime.Time, size congestion.ByteCount) { budget := p.Budget(sendTime) if size > budget { p.budgetAtLastSent = 0 } else { p.budgetAtLastSent = budget - size } p.lastSentTime = sendTime } func (p *pacer) Budget(now monotime.Time) congestion.ByteCount { if p.lastSentTime.IsZero() { return p.maxBurstSize() } budget := p.budgetAtLastSent + (congestion.ByteCount(p.getAdjustedBandwidth())*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 return Min(p.maxBurstSize(), budget) } func (p *pacer) maxBurstSize() congestion.ByteCount { return Max( congestion.ByteCount(uint64((MinPacingDelay+TimerGranularity).Nanoseconds())*p.getAdjustedBandwidth())/1e9, maxBurstSizePackets*p.maxDatagramSize, ) } // TimeUntilSend returns when the next packet should be sent. // It returns the zero value of monotime.Time if a packet can be sent immediately. func (p *pacer) TimeUntilSend() monotime.Time { if p.budgetAtLastSent >= p.maxDatagramSize { return monotime.Time(0) } return p.lastSentTime.Add(Max( MinPacingDelay, time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/float64(p.getAdjustedBandwidth())))*time.Nanosecond, )) } func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) { p.maxDatagramSize = s } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion/windowed_filter.go ================================================ package congestion // WindowedFilter Use the following to construct a windowed filter object of type T. // For example, a min filter using QuicTime as the time type: // // WindowedFilter, QuicTime, QuicTime::Delta> ObjectName; // // A max filter using 64-bit integers as the time type: // // WindowedFilter, uint64_t, int64_t> ObjectName; // // Specifically, this template takes four arguments: // 1. T -- type of the measurement that is being filtered. // 2. Compare -- MinFilter or MaxFilter, depending on the type of filter // desired. // 3. TimeT -- the type used to represent timestamps. // 4. TimeDeltaT -- the type used to represent continuous time intervals between // two timestamps. Has to be the type of (a - b) if both |a| and |b| are // of type TimeT. type WindowedFilter struct { // Time length of window. windowLength int64 estimates []Sample comparator func(int64, int64) bool } type Sample struct { sample int64 time int64 } // Compares two values and returns true if the first is greater than or equal // to the second. func MaxFilter(a, b int64) bool { return a >= b } // Compares two values and returns true if the first is less than or equal // to the second. func MinFilter(a, b int64) bool { return a <= b } func NewWindowedFilter(windowLength int64, comparator func(int64, int64) bool) *WindowedFilter { return &WindowedFilter{ windowLength: windowLength, estimates: make([]Sample, 3), comparator: comparator, } } // Changes the window length. Does not update any current samples. func (f *WindowedFilter) SetWindowLength(windowLength int64) { f.windowLength = windowLength } func (f *WindowedFilter) GetBest() int64 { return f.estimates[0].sample } func (f *WindowedFilter) GetSecondBest() int64 { return f.estimates[1].sample } func (f *WindowedFilter) GetThirdBest() int64 { return f.estimates[2].sample } func (f *WindowedFilter) Update(sample int64, time int64) { if f.estimates[0].time == 0 || f.comparator(sample, f.estimates[0].sample) || (time-f.estimates[2].time) > f.windowLength { f.Reset(sample, time) return } if f.comparator(sample, f.estimates[1].sample) { f.estimates[1].sample = sample f.estimates[1].time = time f.estimates[2].sample = sample f.estimates[2].time = time } else if f.comparator(sample, f.estimates[2].sample) { f.estimates[2].sample = sample f.estimates[2].time = time } // Expire and update estimates as necessary. if time-f.estimates[0].time > f.windowLength { // The best estimate hasn't been updated for an entire window, so promote // second and third best estimates. f.estimates[0].sample = f.estimates[1].sample f.estimates[0].time = f.estimates[1].time f.estimates[1].sample = f.estimates[2].sample f.estimates[1].time = f.estimates[2].time f.estimates[2].sample = sample f.estimates[2].time = time // Need to iterate one more time. Check if the new best estimate is // outside the window as well, since it may also have been recorded a // long time ago. Don't need to iterate once more since we cover that // case at the beginning of the method. if time-f.estimates[0].time > f.windowLength { f.estimates[0].sample = f.estimates[1].sample f.estimates[0].time = f.estimates[1].time f.estimates[1].sample = f.estimates[2].sample f.estimates[1].time = f.estimates[2].time } return } if f.estimates[1].sample == f.estimates[0].sample && time-f.estimates[1].time > f.windowLength>>2 { // A quarter of the window has passed without a better sample, so the // second-best estimate is taken from the second quarter of the window. f.estimates[1].sample = sample f.estimates[1].time = time f.estimates[2].sample = sample f.estimates[2].time = time return } if f.estimates[2].sample == f.estimates[1].sample && time-f.estimates[2].time > f.windowLength>>1 { // We've passed a half of the window without a better estimate, so take // a third-best estimate from the second half of the window. f.estimates[2].sample = sample f.estimates[2].time = time } } func (f *WindowedFilter) Reset(newSample int64, newTime int64) { f.estimates[0].sample = newSample f.estimates[0].time = newTime f.estimates[1].sample = newSample f.estimates[1].time = newTime f.estimates[2].sample = newSample f.estimates[2].time = newTime } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/bandwidth.go ================================================ package congestion import ( "math" "time" "github.com/metacubex/quic-go/congestion" ) const ( infBandwidth = Bandwidth(math.MaxUint64) ) // Bandwidth of a connection type Bandwidth uint64 const ( // BitsPerSecond is 1 bit per second BitsPerSecond Bandwidth = 1 // BytesPerSecond is 1 byte per second BytesPerSecond = 8 * BitsPerSecond ) // BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/bandwidth_sampler.go ================================================ package congestion import ( "math" "time" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" ) const ( infRTT = time.Duration(math.MaxInt64) defaultConnectionStateMapQueueSize = 256 defaultCandidatesBufferSize = 256 ) type roundTripCount uint64 // SendTimeState is a subset of ConnectionStateOnSentPacket which is returned // to the caller when the packet is acked or lost. type sendTimeState struct { // Whether other states in this object is valid. isValid bool // Whether the sender is app limited at the time the packet was sent. // App limited bandwidth sample might be artificially low because the sender // did not have enough data to send in order to saturate the link. isAppLimited bool // Total number of sent bytes at the time the packet was sent. // Includes the packet itself. totalBytesSent congestion.ByteCount // Total number of acked bytes at the time the packet was sent. totalBytesAcked congestion.ByteCount // Total number of lost bytes at the time the packet was sent. totalBytesLost congestion.ByteCount // Total number of inflight bytes at the time the packet was sent. // Includes the packet itself. // It should be equal to |total_bytes_sent| minus the sum of // |total_bytes_acked|, |total_bytes_lost| and total neutered bytes. bytesInFlight congestion.ByteCount } func newSendTimeState( isAppLimited bool, totalBytesSent congestion.ByteCount, totalBytesAcked congestion.ByteCount, totalBytesLost congestion.ByteCount, bytesInFlight congestion.ByteCount, ) *sendTimeState { return &sendTimeState{ isValid: true, isAppLimited: isAppLimited, totalBytesSent: totalBytesSent, totalBytesAcked: totalBytesAcked, totalBytesLost: totalBytesLost, bytesInFlight: bytesInFlight, } } type extraAckedEvent struct { // The excess bytes acknowlwedged in the time delta for this event. extraAcked congestion.ByteCount // The bytes acknowledged and time delta from the event. bytesAcked congestion.ByteCount timeDelta time.Duration // The round trip of the event. round roundTripCount } func maxExtraAckedEventFunc(a, b extraAckedEvent) int { if a.extraAcked > b.extraAcked { return 1 } else if a.extraAcked < b.extraAcked { return -1 } return 0 } // BandwidthSample type bandwidthSample struct { // The bandwidth at that particular sample. Zero if no valid bandwidth sample // is available. bandwidth Bandwidth // The RTT measurement at this particular sample. Zero if no RTT sample is // available. Does not correct for delayed ack time. rtt time.Duration // |send_rate| is computed from the current packet being acked('P') and an // earlier packet that is acked before P was sent. sendRate Bandwidth // States captured when the packet was sent. stateAtSend sendTimeState } func newBandwidthSample() *bandwidthSample { return &bandwidthSample{ sendRate: infBandwidth, } } // MaxAckHeightTracker is part of the BandwidthSampler. It is called after every // ack event to keep track the degree of ack aggregation(a.k.a "ack height"). type maxAckHeightTracker struct { // Tracks the maximum number of bytes acked faster than the estimated // bandwidth. maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount] // The time this aggregation started and the number of bytes acked during it. aggregationEpochStartTime monotime.Time aggregationEpochBytes congestion.ByteCount // The last sent packet number before the current aggregation epoch started. lastSentPacketNumberBeforeEpoch congestion.PacketNumber // The number of ack aggregation epochs ever started, including the ongoing // one. Stats only. numAckAggregationEpochs uint64 ackAggregationBandwidthThreshold float64 startNewAggregationEpochAfterFullRound bool reduceExtraAckedOnBandwidthIncrease bool } func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker { return &maxAckHeightTracker{ maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc), lastSentPacketNumberBeforeEpoch: invalidPacketNumber, ackAggregationBandwidthThreshold: 1.0, } } func (m *maxAckHeightTracker) Get() congestion.ByteCount { return m.maxAckHeightFilter.GetBest().extraAcked } func (m *maxAckHeightTracker) Update( bandwidthEstimate Bandwidth, isNewMaxBandwidth bool, roundTripCount roundTripCount, lastSentPacketNumber congestion.PacketNumber, lastAckedPacketNumber congestion.PacketNumber, ackTime monotime.Time, bytesAcked congestion.ByteCount, ) congestion.ByteCount { forceNewEpoch := false if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth { // Save and clear existing entries. best := m.maxAckHeightFilter.GetBest() secondBest := m.maxAckHeightFilter.GetSecondBest() thirdBest := m.maxAckHeightFilter.GetThirdBest() m.maxAckHeightFilter.Clear() // Reinsert the heights into the filter after recalculating. expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta) if expectedBytesAcked < best.bytesAcked { best.extraAcked = best.bytesAcked - expectedBytesAcked m.maxAckHeightFilter.Update(best, best.round) } expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta) if expectedBytesAcked < secondBest.bytesAcked { secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked m.maxAckHeightFilter.Update(secondBest, secondBest.round) } expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta) if expectedBytesAcked < thirdBest.bytesAcked { thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked m.maxAckHeightFilter.Update(thirdBest, thirdBest.round) } } // If any packet sent after the start of the epoch has been acked, start a new // epoch. if m.startNewAggregationEpochAfterFullRound && m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber && lastAckedPacketNumber != invalidPacketNumber && lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch { forceNewEpoch = true } if m.aggregationEpochStartTime.IsZero() || forceNewEpoch { m.aggregationEpochBytes = bytesAcked m.aggregationEpochStartTime = ackTime m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber m.numAckAggregationEpochs++ return 0 } // Compute how many bytes are expected to be delivered, assuming max bandwidth // is correct. aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime) expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta) // Reset the current aggregation epoch as soon as the ack arrival rate is less // than or equal to the max bandwidth. if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) { // Reset to start measuring a new aggregation epoch. m.aggregationEpochBytes = bytesAcked m.aggregationEpochStartTime = ackTime m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber m.numAckAggregationEpochs++ return 0 } m.aggregationEpochBytes += bytesAcked // Compute how many extra bytes were delivered vs max bandwidth. extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked newEvent := extraAckedEvent{ extraAcked: extraBytesAcked, bytesAcked: m.aggregationEpochBytes, timeDelta: aggregationDelta, } m.maxAckHeightFilter.Update(newEvent, roundTripCount) return extraBytesAcked } func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) { m.maxAckHeightFilter.SetWindowLength(length) } func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) { newEvent := extraAckedEvent{ extraAcked: newHeight, round: newTime, } m.maxAckHeightFilter.Reset(newEvent, newTime) } func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) { m.ackAggregationBandwidthThreshold = threshold } func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) { m.startNewAggregationEpochAfterFullRound = value } func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) { m.reduceExtraAckedOnBandwidthIncrease = value } func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 { return m.ackAggregationBandwidthThreshold } func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 { return m.numAckAggregationEpochs } // AckPoint represents a point on the ack line. type ackPoint struct { ackTime monotime.Time totalBytesAcked congestion.ByteCount } // RecentAckPoints maintains the most recent 2 ack points at distinct times. type recentAckPoints struct { ackPoints [2]ackPoint } func (r *recentAckPoints) Update(ackTime monotime.Time, totalBytesAcked congestion.ByteCount) { if ackTime.Before(r.ackPoints[1].ackTime) { r.ackPoints[1].ackTime = ackTime } else if ackTime.After(r.ackPoints[1].ackTime) { r.ackPoints[0] = r.ackPoints[1] r.ackPoints[1].ackTime = ackTime } r.ackPoints[1].totalBytesAcked = totalBytesAcked } func (r *recentAckPoints) Clear() { r.ackPoints[0] = ackPoint{} r.ackPoints[1] = ackPoint{} } func (r *recentAckPoints) MostRecentPoint() *ackPoint { return &r.ackPoints[1] } func (r *recentAckPoints) LessRecentPoint() *ackPoint { if r.ackPoints[0].totalBytesAcked != 0 { return &r.ackPoints[0] } return &r.ackPoints[1] } // ConnectionStateOnSentPacket represents the information about a sent packet // and the state of the connection at the moment the packet was sent, // specifically the information about the most recently acknowledged packet at // that moment. type connectionStateOnSentPacket struct { // Time at which the packet is sent. sentTime monotime.Time // Size of the packet. size congestion.ByteCount // The value of |totalBytesSentAtLastAckedPacket| at the time the // packet was sent. totalBytesSentAtLastAckedPacket congestion.ByteCount // The value of |lastAckedPacketSentTime| at the time the packet was // sent. lastAckedPacketSentTime monotime.Time // The value of |lastAckedPacketAckTime| at the time the packet was // sent. lastAckedPacketAckTime monotime.Time // Send time states that are returned to the congestion controller when the // packet is acked or lost. sendTimeState sendTimeState } // Snapshot constructor. Records the current state of the bandwidth // sampler. // |bytes_in_flight| is the bytes in flight right after the packet is sent. func newConnectionStateOnSentPacket( sentTime monotime.Time, size congestion.ByteCount, bytesInFlight congestion.ByteCount, sampler *bandwidthSampler, ) *connectionStateOnSentPacket { return &connectionStateOnSentPacket{ sentTime: sentTime, size: size, totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, sendTimeState: *newSendTimeState( sampler.isAppLimited, sampler.totalBytesSent, sampler.totalBytesAcked, sampler.totalBytesLost, bytesInFlight, ), } } // BandwidthSampler keeps track of sent and acknowledged packets and outputs a // bandwidth sample for every packet acknowledged. The samples are taken for // individual packets, and are not filtered; the consumer has to filter the // bandwidth samples itself. In certain cases, the sampler will locally severely // underestimate the bandwidth, hence a maximum filter with a size of at least // one RTT is recommended. // // This class bases its samples on the slope of two curves: the number of bytes // sent over time, and the number of bytes acknowledged as received over time. // It produces a sample of both slopes for every packet that gets acknowledged, // based on a slope between two points on each of the corresponding curves. Note // that due to the packet loss, the number of bytes on each curve might get // further and further away from each other, meaning that it is not feasible to // compare byte values coming from different curves with each other. // // The obvious points for measuring slope sample are the ones corresponding to // the packet that was just acknowledged. Let us denote them as S_1 (point at // which the current packet was sent) and A_1 (point at which the current packet // was acknowledged). However, taking a slope requires two points on each line, // so estimating bandwidth requires picking a packet in the past with respect to // which the slope is measured. // // For that purpose, BandwidthSampler always keeps track of the most recently // acknowledged packet, and records it together with every outgoing packet. // When a packet gets acknowledged (A_1), it has not only information about when // it itself was sent (S_1), but also the information about the latest // acknowledged packet right before it was sent (S_0 and A_0). // // Based on that data, send and ack rate are estimated as: // // send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) // ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) // // Here, the ack rate is intuitively the rate we want to treat as bandwidth. // However, in certain cases (e.g. ack compression) the ack rate at a point may // end up higher than the rate at which the data was originally sent, which is // not indicative of the real bandwidth. Hence, we use the send rate as an upper // bound, and the sample value is // // rate_sample = Min(send_rate, ack_rate) // // An important edge case handled by the sampler is tracking the app-limited // samples. There are multiple meaning of "app-limited" used interchangeably, // hence it is important to understand and to be able to distinguish between // them. // // Meaning 1: connection state. The connection is said to be app-limited when // there is no outstanding data to send. This means that certain bandwidth // samples in the future would not be an accurate indication of the link // capacity, and it is important to inform consumer about that. Whenever // connection becomes app-limited, the sampler is notified via OnAppLimited() // method. // // Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth // sampler becomes notified about the connection being app-limited, it enters // app-limited phase. In that phase, all *sent* packets are marked as // app-limited. Note that the connection itself does not have to be // app-limited during the app-limited phase, and in fact it will not be // (otherwise how would it send packets?). The boolean flag below indicates // whether the sampler is in that phase. // // Meaning 3: a flag on the sent packet and on the sample. If a sent packet is // sent during the app-limited phase, the resulting sample related to the // packet will be marked as app-limited. // // With the terminology issue out of the way, let us consider the question of // what kind of situation it addresses. // // Consider a scenario where we first send packets 1 to 20 at a regular // bandwidth, and then immediately run out of data. After a few seconds, we send // packets 21 to 60, and only receive ack for 21 between sending packets 40 and // 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 // we use to compute the slope is going to be packet 20, a few seconds apart // from the current packet, hence the resulting estimate would be extremely low // and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, // meaning that the bandwidth sample would exclude the quiescence. // // Based on the analysis of that scenario, we implement the following rule: once // OnAppLimited() is called, all sent packets will produce app-limited samples // up until an ack for a packet that was sent after OnAppLimited() was called. // Note that while the scenario above is not the only scenario when the // connection is app-limited, the approach works in other cases too. type congestionEventSample struct { // The maximum bandwidth sample from all acked packets. // QuicBandwidth::Zero() if no samples are available. sampleMaxBandwidth Bandwidth // Whether |sample_max_bandwidth| is from a app-limited sample. sampleIsAppLimited bool // The minimum rtt sample from all acked packets. // QuicTime::Delta::Infinite() if no samples are available. sampleRtt time.Duration // For each packet p in acked packets, this is the max value of INFLIGHT(p), // where INFLIGHT(p) is the number of bytes acked while p is inflight. sampleMaxInflight congestion.ByteCount // The send state of the largest packet in acked_packets, unless it is // empty. If acked_packets is empty, it's the send state of the largest // packet in lost_packets. lastPacketSendState sendTimeState // The number of extra bytes acked from this ack event, compared to what is // expected from the flow's bandwidth. Larger value means more ack // aggregation. extraAcked congestion.ByteCount } func newCongestionEventSample() *congestionEventSample { return &congestionEventSample{ sampleRtt: infRTT, } } type bandwidthSampler struct { // The total number of congestion controlled bytes sent during the connection. totalBytesSent congestion.ByteCount // The total number of congestion controlled bytes which were acknowledged. totalBytesAcked congestion.ByteCount // The total number of congestion controlled bytes which were lost. totalBytesLost congestion.ByteCount // The total number of congestion controlled bytes which have been neutered. totalBytesNeutered congestion.ByteCount // The value of |total_bytes_sent_| at the time the last acknowledged packet // was sent. Valid only when |last_acked_packet_sent_time_| is valid. totalBytesSentAtLastAckedPacket congestion.ByteCount // The time at which the last acknowledged packet was sent. Set to // QuicTime::Zero() if no valid timestamp is available. lastAckedPacketSentTime monotime.Time // The time at which the most recent packet was acknowledged. lastAckedPacketAckTime monotime.Time // The most recently sent packet. lastSentPacket congestion.PacketNumber // The most recently acked packet. lastAckedPacket congestion.PacketNumber // Indicates whether the bandwidth sampler is currently in an app-limited // phase. isAppLimited bool // The packet that will be acknowledged after this one will cause the sampler // to exit the app-limited phase. endOfAppLimitedPhase congestion.PacketNumber // Record of the connection state at the point where each packet in flight was // sent, indexed by the packet number. connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket] recentAckPoints recentAckPoints a0Candidates RingBuffer[ackPoint] // Maximum number of tracked packets. maxTrackedPackets congestion.ByteCount maxAckHeightTracker *maxAckHeightTracker totalBytesAckedAfterLastAckEvent congestion.ByteCount // True if connection option 'BSAO' is set. overestimateAvoidance bool // True if connection option 'BBRB' is set. limitMaxAckHeightTrackerBySendRate bool } func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler { b := &bandwidthSampler{ maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength), connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize), lastSentPacket: invalidPacketNumber, lastAckedPacket: invalidPacketNumber, endOfAppLimitedPhase: invalidPacketNumber, } b.a0Candidates.Init(defaultCandidatesBufferSize) return b } func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount { return b.maxAckHeightTracker.Get() } func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 { return b.maxAckHeightTracker.NumAckAggregationEpochs() } func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) { b.maxAckHeightTracker.SetFilterWindowLength(length) } func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) { b.maxAckHeightTracker.Reset(newHeight, newTime) } func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) { b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value) } func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) { b.limitMaxAckHeightTrackerBySendRate = value } func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) { b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value) } func (b *bandwidthSampler) EnableOverestimateAvoidance() { if b.overestimateAvoidance { return } b.overestimateAvoidance = true b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0) } func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool { return b.overestimateAvoidance } func (b *bandwidthSampler) OnPacketSent( sentTime monotime.Time, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, bytesInFlight congestion.ByteCount, isRetransmittable bool, ) { b.lastSentPacket = packetNumber if !isRetransmittable { return } b.totalBytesSent += bytes // If there are no packets in flight, the time at which the new transmission // opens can be treated as the A_0 point for the purpose of bandwidth // sampling. This underestimates bandwidth to some extent, and produces some // artificially low samples for most packets in flight, but it provides with // samples at important points where we would not have them otherwise, most // importantly at the beginning of the connection. if bytesInFlight == 0 { b.lastAckedPacketAckTime = sentTime if b.overestimateAvoidance { b.recentAckPoints.Clear() b.recentAckPoints.Update(sentTime, b.totalBytesAcked) b.a0Candidates.Clear() b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint()) } b.totalBytesSentAtLastAckedPacket = b.totalBytesSent // In this situation ack compression is not a concern, set send rate to // effectively infinite. b.lastAckedPacketSentTime = sentTime } b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket( sentTime, bytes, bytesInFlight+bytes, b, )) } func (b *bandwidthSampler) OnCongestionEvent( ackTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo, maxBandwidth Bandwidth, estBandwidthUpperBound Bandwidth, roundTripCount roundTripCount, ) congestionEventSample { eventSample := newCongestionEventSample() var lastLostPacketSendState sendTimeState for _, p := range lostPackets { sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost) if sendState.isValid { lastLostPacketSendState = sendState } } if len(ackedPackets) == 0 { // Only populate send state for a loss-only event. eventSample.lastPacketSendState = lastLostPacketSendState return *eventSample } var lastAckedPacketSendState sendTimeState var maxSendRate Bandwidth for _, p := range ackedPackets { sample := b.onPacketAcknowledged(ackTime, p.PacketNumber) if !sample.stateAtSend.isValid { continue } lastAckedPacketSendState = sample.stateAtSend if sample.rtt != 0 { eventSample.sampleRtt = Min(eventSample.sampleRtt, sample.rtt) } if sample.bandwidth > eventSample.sampleMaxBandwidth { eventSample.sampleMaxBandwidth = sample.bandwidth eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited } if sample.sendRate != infBandwidth { maxSendRate = Max(maxSendRate, sample.sendRate) } inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked if inflightSample > eventSample.sampleMaxInflight { eventSample.sampleMaxInflight = inflightSample } } if !lastLostPacketSendState.isValid { eventSample.lastPacketSendState = lastAckedPacketSendState } else if !lastAckedPacketSendState.isValid { eventSample.lastPacketSendState = lastLostPacketSendState } else { // If two packets are inflight and an alarm is armed to lose a packet and it // wakes up late, then the first of two in flight packets could have been // acknowledged before the wakeup, which re-evaluates loss detection, and // could declare the later of the two lost. if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber { eventSample.lastPacketSendState = lastLostPacketSendState } else { eventSample.lastPacketSendState = lastAckedPacketSendState } } isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth maxBandwidth = Max(maxBandwidth, eventSample.sampleMaxBandwidth) if b.limitMaxAckHeightTrackerBySendRate { maxBandwidth = Max(maxBandwidth, maxSendRate) } eventSample.extraAcked = b.onAckEventEnd(Min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount) return *eventSample } func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) { b.totalBytesLost += bytesLost if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil { sentPacketToSendTimeState(sentPacketPointer, &s) } return s } func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) { b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) { b.totalBytesNeutered += sentPacket.size }) } func (b *bandwidthSampler) OnAppLimited() { b.isAppLimited = true b.endOfAppLimitedPhase = b.lastSentPacket } func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) { // A packet can become obsolete when it is removed from QuicUnackedPacketMap's // view of inflight before it is acked or marked as lost. For example, when // QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet, // the packet is removed from QuicUnackedPacketMap's inflight, but is not // marked as acked or lost in the BandwidthSampler. b.connectionStateMap.RemoveUpTo(leastUnacked) } func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount { return b.totalBytesSent } func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount { return b.totalBytesLost } func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount { return b.totalBytesAcked } func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount { return b.totalBytesNeutered } func (b *bandwidthSampler) IsAppLimited() bool { return b.isAppLimited } func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber { return b.endOfAppLimitedPhase } func (b *bandwidthSampler) max_ack_height() congestion.ByteCount { return b.maxAckHeightTracker.Get() } func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool { if b.a0Candidates.Empty() { return false } if b.a0Candidates.Len() == 1 { *a0 = *b.a0Candidates.Front() return true } for i := 1; i < b.a0Candidates.Len(); i++ { if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked { *a0 = *b.a0Candidates.Offset(i - 1) if i > 1 { for j := 0; j < i-1; j++ { b.a0Candidates.PopFront() } } return true } } *a0 = *b.a0Candidates.Back() for k := 0; k < b.a0Candidates.Len()-1; k++ { b.a0Candidates.PopFront() } return true } func (b *bandwidthSampler) onPacketAcknowledged(ackTime monotime.Time, packetNumber congestion.PacketNumber) bandwidthSample { sample := newBandwidthSample() b.lastAckedPacket = packetNumber sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber) if sentPacketPointer == nil { return *sample } // OnPacketAcknowledgedInner b.totalBytesAcked += sentPacketPointer.size b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent b.lastAckedPacketSentTime = sentPacketPointer.sentTime b.lastAckedPacketAckTime = ackTime if b.overestimateAvoidance { b.recentAckPoints.Update(ackTime, b.totalBytesAcked) } if b.isAppLimited { // Exit app-limited phase in two cases: // (1) end_of_app_limited_phase_ is not initialized, i.e., so far all // packets are sent while there are buffered packets or pending data. // (2) The current acked packet is after the sent packet marked as the end // of the app limit phase. if b.endOfAppLimitedPhase == invalidPacketNumber || packetNumber > b.endOfAppLimitedPhase { b.isAppLimited = false } } // There might have been no packets acknowledged at the moment when the // current packet was sent. In that case, there is no bandwidth sample to // make. if sentPacketPointer.lastAckedPacketSentTime.IsZero() { return *sample } // Infinite rate indicates that the sampler is supposed to discard the // current send rate sample and use only the ack rate. sendRate := infBandwidth if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) { sendRate = BandwidthFromDelta( sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket, sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime)) } var a0 ackPoint if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) { } else { a0.ackTime = sentPacketPointer.lastAckedPacketAckTime a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked } // During the slope calculation, ensure that ack time of the current packet is // always larger than the time of the previous packet, otherwise division by // zero or integer underflow can occur. if ackTime.Sub(a0.ackTime) <= 0 { return *sample } ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime)) sample.bandwidth = Min(sendRate, ackRate) // Note: this sample does not account for delayed acknowledgement time. This // means that the RTT measurements here can be artificially high, especially // on low bandwidth connections. sample.rtt = ackTime.Sub(sentPacketPointer.sentTime) sample.sendRate = sendRate sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend) return *sample } func (b *bandwidthSampler) onAckEventEnd( bandwidthEstimate Bandwidth, isNewMaxBandwidth bool, roundTripCount roundTripCount, ) congestion.ByteCount { newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent if newlyAckedBytes == 0 { return 0 } b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked extraAcked := b.maxAckHeightTracker.Update( bandwidthEstimate, isNewMaxBandwidth, roundTripCount, b.lastSentPacket, b.lastAckedPacket, b.lastAckedPacketAckTime, newlyAckedBytes) // If |extra_acked| is zero, i.e. this ack event marks the start of a new ack // aggregation epoch, save LessRecentPoint, which is the last ack point of the // previous epoch, as a A0 candidate. if b.overestimateAvoidance && extraAcked == 0 { b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint()) } return extraAcked } func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) { *sendTimeState = sentPacket.sendTimeState sendTimeState.isValid = true } // BytesFromBandwidthAndTimeDelta calculates the bytes // from a bandwidth(bits per second) and a time delta func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount { return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) / (congestion.ByteCount(time.Second) * 8) } func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration { return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth) } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/bbr_sender.go ================================================ package congestion // src from https://github.com/google/quiche/blob/e7872fc9e12bb1d46a118949c3d4da36de58aa44/quiche/quic/core/congestion_control/bbr_sender.cc import ( "fmt" "time" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" "github.com/metacubex/randv2" ) // BbrSender implements BBR congestion control algorithm. BBR aims to estimate // the current available Bottleneck Bandwidth and RTT (hence the name), and // regulates the pacing rate and the size of the congestion window based on // those signals. // // BBR relies on pacing in order to function properly. Do not use BBR when // pacing is disabled. // const ( minBps = 65536 // 64 KB/s invalidPacketNumber = -1 initialCongestionWindowPackets = 32 minCongestionWindowPackets = 4 // Constants based on TCP defaults. // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. // Does not inflate the pacing rate. // The gain used for the STARTUP, equal to 2/ln(2). defaultHighGain = 2.885 // The newly derived CWND gain for STARTUP, 2. derivedHighCWNDGain = 2.0 ) // The cycle of gains used during the PROBE_BW stage. var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0} const ( // The length of the gain cycle. gainCycleLength = len(pacingGain) // The size of the bandwidth filter window, in round-trips. bandwidthWindowSize = gainCycleLength + 2 // The time after which the current min_rtt value expires. minRttExpiry = 10 * time.Second // The minimum time the connection can spend in PROBE_RTT mode. probeRttTime = 200 * time.Millisecond // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection // will exit the STARTUP mode. startupGrowthTarget = 1.25 roundTripsWithoutGrowthBeforeExitingStartup = int64(3) // Flag. defaultStartupFullLossCount = 8 quicBbr2DefaultLossThreshold = 0.02 ) type bbrMode int const ( // Startup phase of the connection. bbrModeStartup = iota // After achieving the highest possible bandwidth during the startup, lower // the pacing rate in order to drain the queue. bbrModeDrain // Cruising mode. bbrModeProbeBw // Temporarily slow down sending in order to empty the buffer and measure // the real minimum RTT. bbrModeProbeRtt ) // Indicates how the congestion control limits the amount of bytes in flight. type bbrRecoveryState int const ( // Do not limit. bbrRecoveryStateNotInRecovery = iota // Allow an extra outstanding byte for each byte acknowledged. bbrRecoveryStateConservation // Allow two extra outstanding bytes for each byte acknowledged (slow // start). bbrRecoveryStateGrowth ) type Profile string const ( ProfileConservative Profile = "conservative" ProfileStandard Profile = "standard" ProfileAggressive Profile = "aggressive" ) type profileConfig struct { highGain float64 highCwndGain float64 congestionWindowGainConstant float64 numStartupRtts int64 drainToTarget bool detectOvershooting bool bytesLostMultiplier uint8 enableAckAggregationStartup bool expireAckAggregationStartup bool enableOverestimateAvoidance bool reduceExtraAckedOnBandwidthIncrease bool } func configForProfile(profile Profile) profileConfig { switch profile { case ProfileConservative: return profileConfig{ highGain: 2.25, highCwndGain: 1.75, congestionWindowGainConstant: 1.75, numStartupRtts: 2, drainToTarget: true, detectOvershooting: true, bytesLostMultiplier: 1, enableOverestimateAvoidance: true, reduceExtraAckedOnBandwidthIncrease: true, } case ProfileAggressive: return profileConfig{ highGain: 3.0, highCwndGain: 2.25, congestionWindowGainConstant: 2.5, numStartupRtts: 4, bytesLostMultiplier: 2, enableAckAggregationStartup: true, expireAckAggregationStartup: true, } default: return profileConfig{ highGain: defaultHighGain, highCwndGain: derivedHighCWNDGain, congestionWindowGainConstant: 2.0, numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, bytesLostMultiplier: 2, } } } type bbrSender struct { rttStats congestion.RTTStatsProvider pacer *Pacer mode bbrMode // Bandwidth sampler provides BBR with the bandwidth measurements at // individual points. sampler *bandwidthSampler // The number of the round trips that have occurred during the connection. roundTripCount roundTripCount // The packet number of the most recently sent packet. lastSentPacket congestion.PacketNumber // Acknowledgement of any packet after |current_round_trip_end_| will cause // the round trip counter to advance. currentRoundTripEnd congestion.PacketNumber // Number of congestion events with some losses, in the current round. numLossEventsInRound uint64 // Number of total bytes lost in the current round. bytesLostInRound congestion.ByteCount // The filter that tracks the maximum bandwidth over the multiple recent // round-trips. maxBandwidth *WindowedFilter[Bandwidth, roundTripCount] // Minimum RTT estimate. Automatically expires within 10 seconds (and // triggers PROBE_RTT mode) if no new value is sampled during that period. minRtt time.Duration // The time at which the current value of |min_rtt_| was assigned. minRttTimestamp monotime.Time // The maximum allowed number of bytes in flight. congestionWindow congestion.ByteCount // The initial value of the |congestion_window_|. initialCongestionWindow congestion.ByteCount // The largest value the |congestion_window_| can achieve. maxCongestionWindow congestion.ByteCount // The smallest value the |congestion_window_| can achieve. minCongestionWindow congestion.ByteCount // The BBR profile used by the sender. profile Profile // The pacing gain applied during the STARTUP phase. highGain float64 // The CWND gain applied during the STARTUP phase. highCwndGain float64 // The pacing gain applied during the DRAIN phase. drainGain float64 // The current pacing rate of the connection. pacingRate Bandwidth // The gain currently applied to the pacing rate. pacingGain float64 // The gain currently applied to the congestion window. congestionWindowGain float64 // The gain used for the congestion window during PROBE_BW. Latched from // quic_bbr_cwnd_gain flag. congestionWindowGainConstant float64 // The number of RTTs to stay in STARTUP mode. Defaults to 3. numStartupRtts int64 // Number of round-trips in PROBE_BW mode, used for determining the current // pacing gain cycle. cycleCurrentOffset int // The time at which the last pacing gain cycle was started. lastCycleStart monotime.Time // Indicates whether the connection has reached the full bandwidth mode. isAtFullBandwidth bool // Number of rounds during which there was no significant bandwidth increase. roundsWithoutBandwidthGain int64 // The bandwidth compared to which the increase is measured. bandwidthAtLastRound Bandwidth // Set to true upon exiting quiescence. exitingQuiescence bool // Time at which PROBE_RTT has to be exited. Setting it to zero indicates // that the time is yet unknown as the number of packets in flight has not // reached the required value. exitProbeRttAt monotime.Time // Indicates whether a round-trip has passed since PROBE_RTT became active. probeRttRoundPassed bool // Indicates whether the most recent bandwidth sample was marked as // app-limited. lastSampleIsAppLimited bool // Indicates whether any non app-limited samples have been recorded. hasNoAppLimitedSample bool // Current state of recovery. recoveryState bbrRecoveryState // Receiving acknowledgement of a packet after |end_recovery_at_| will cause // BBR to exit the recovery mode. A value above zero indicates at least one // loss has been detected, so it must not be set back to zero. endRecoveryAt congestion.PacketNumber // A window used to limit the number of bytes in flight during loss recovery. recoveryWindow congestion.ByteCount // If true, consider all samples in recovery app-limited. isAppLimitedRecovery bool // not used // When true, pace at 1.5x and disable packet conservation in STARTUP. slowerStartup bool // not used // When true, disables packet conservation in STARTUP. rateBasedStartup bool // not used // When true, add the most recent ack aggregation measurement during STARTUP. enableAckAggregationDuringStartup bool // When true, expire the windowed ack aggregation values in STARTUP when // bandwidth increases more than 25%. expireAckAggregationInStartup bool // If true, will not exit low gain mode until bytes_in_flight drops below BDP // or it's time for high gain mode. drainToTarget bool // If true, slow down pacing rate in STARTUP when overshooting is detected. detectOvershooting bool // Bytes lost while detect_overshooting_ is true. bytesLostWhileDetectingOvershooting congestion.ByteCount // Slow down pacing rate if // bytes_lost_while_detecting_overshooting_ * // bytes_lost_multiplier_while_detecting_overshooting_ > IW. bytesLostMultiplierWhileDetectingOvershooting uint8 // When overshooting is detected, do not drop pacing_rate_ below this value / // min_rtt. cwndToCalculateMinPacingRate congestion.ByteCount // Max congestion window when adjusting network parameters. maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used // Params. maxDatagramSize congestion.ByteCount // Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()| bytesInFlight congestion.ByteCount } var _ congestion.CongestionControl = &bbrSender{} func NewBbrSender( initialMaxDatagramSize congestion.ByteCount, initialCongestionWindowPackets congestion.ByteCount, profile Profile, ) *bbrSender { return newBbrSender( initialMaxDatagramSize, initialCongestionWindowPackets*initialMaxDatagramSize, congestion.MaxCongestionWindowPackets*initialMaxDatagramSize, profile, ) } func newBbrSender( initialMaxDatagramSize, initialCongestionWindow, initialMaxCongestionWindow congestion.ByteCount, profile Profile, ) *bbrSender { b := &bbrSender{ mode: bbrModeStartup, sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)), lastSentPacket: invalidPacketNumber, currentRoundTripEnd: invalidPacketNumber, maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]), congestionWindow: initialCongestionWindow, initialCongestionWindow: initialCongestionWindow, maxCongestionWindow: initialMaxCongestionWindow, minCongestionWindow: minCongestionWindowForMaxDatagramSize(initialMaxDatagramSize), profile: ProfileStandard, highGain: defaultHighGain, highCwndGain: derivedHighCWNDGain, drainGain: 1.0 / defaultHighGain, pacingGain: 1.0, congestionWindowGain: 1.0, congestionWindowGainConstant: 2.0, numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, recoveryState: bbrRecoveryStateNotInRecovery, endRecoveryAt: invalidPacketNumber, recoveryWindow: initialMaxCongestionWindow, bytesLostMultiplierWhileDetectingOvershooting: 2, cwndToCalculateMinPacingRate: initialCongestionWindow, maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow, maxDatagramSize: initialMaxDatagramSize, } b.pacer = NewPacer(b.bandwidthForPacer) b.applyProfile(profile) b.enterStartupMode() return b } func (b *bbrSender) applyProfile(profile Profile) { cfg := configForProfile(profile) b.profile = profile b.highGain = cfg.highGain b.highCwndGain = cfg.highCwndGain b.drainGain = 1.0 / cfg.highGain b.congestionWindowGainConstant = cfg.congestionWindowGainConstant b.numStartupRtts = cfg.numStartupRtts b.drainToTarget = cfg.drainToTarget b.detectOvershooting = cfg.detectOvershooting b.bytesLostMultiplierWhileDetectingOvershooting = cfg.bytesLostMultiplier b.enableAckAggregationDuringStartup = cfg.enableAckAggregationStartup b.expireAckAggregationInStartup = cfg.expireAckAggregationStartup if cfg.enableOverestimateAvoidance { b.sampler.EnableOverestimateAvoidance() } b.sampler.SetReduceExtraAckedOnBandwidthIncrease(cfg.reduceExtraAckedOnBandwidthIncrease) } func minCongestionWindowForMaxDatagramSize(maxDatagramSize congestion.ByteCount) congestion.ByteCount { return minCongestionWindowPackets * maxDatagramSize } func scaleByteWindowForDatagramSize(window, oldMaxDatagramSize, newMaxDatagramSize congestion.ByteCount) congestion.ByteCount { if oldMaxDatagramSize == newMaxDatagramSize { return window } return congestion.ByteCount(uint64(window) * uint64(newMaxDatagramSize) / uint64(oldMaxDatagramSize)) } func (b *bbrSender) rescalePacketSizedWindows(maxDatagramSize congestion.ByteCount) { oldMaxDatagramSize := b.maxDatagramSize b.maxDatagramSize = maxDatagramSize b.initialCongestionWindow = scaleByteWindowForDatagramSize(b.initialCongestionWindow, oldMaxDatagramSize, maxDatagramSize) b.maxCongestionWindow = scaleByteWindowForDatagramSize(b.maxCongestionWindow, oldMaxDatagramSize, maxDatagramSize) b.minCongestionWindow = minCongestionWindowForMaxDatagramSize(maxDatagramSize) b.cwndToCalculateMinPacingRate = scaleByteWindowForDatagramSize(b.cwndToCalculateMinPacingRate, oldMaxDatagramSize, maxDatagramSize) b.maxCongestionWindowWithNetworkParametersAdjusted = scaleByteWindowForDatagramSize( b.maxCongestionWindowWithNetworkParametersAdjusted, oldMaxDatagramSize, maxDatagramSize, ) } func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { b.rttStats = provider } // TimeUntilSend implements the SendAlgorithm interface. func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time { return b.pacer.TimeUntilSend() } // HasPacingBudget implements the SendAlgorithm interface. func (b *bbrSender) HasPacingBudget(now monotime.Time) bool { return b.pacer.Budget(now) >= b.maxDatagramSize } // OnPacketSent implements the SendAlgorithm interface. func (b *bbrSender) OnPacketSent( sentTime monotime.Time, bytesInFlight congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, ) { b.pacer.SentPacket(sentTime, bytes) b.lastSentPacket = packetNumber b.bytesInFlight = bytesInFlight if bytesInFlight == 0 { b.exitingQuiescence = true } b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) } // CanSend implements the SendAlgorithm interface. func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { return bytesInFlight < b.GetCongestionWindow() } // MaybeExitSlowStart implements the SendAlgorithm interface. func (b *bbrSender) MaybeExitSlowStart() { // Do nothing } // OnPacketAcked implements the SendAlgorithm interface. func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime monotime.Time) { // Do nothing. } // OnPacketLost implements the SendAlgorithm interface. func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { // Do nothing. } // OnRetransmissionTimeout implements the SendAlgorithm interface. func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { // Do nothing. } // SetMaxDatagramSize implements the SendAlgorithm interface. func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { if s < b.maxDatagramSize { panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) } oldMinCongestionWindow := b.minCongestionWindow oldInitialCongestionWindow := b.initialCongestionWindow b.rescalePacketSizedWindows(s) switch b.congestionWindow { case oldMinCongestionWindow: b.congestionWindow = b.minCongestionWindow case oldInitialCongestionWindow: b.congestionWindow = b.initialCongestionWindow default: b.congestionWindow = Min(b.maxCongestionWindow, Max(b.congestionWindow, b.minCongestionWindow)) } b.recoveryWindow = Min(b.maxCongestionWindow, Max(b.recoveryWindow, b.minCongestionWindow)) b.pacer.SetMaxDatagramSize(s) } // InSlowStart implements the SendAlgorithmWithDebugInfos interface. func (b *bbrSender) InSlowStart() bool { return b.mode == bbrModeStartup } // InRecovery implements the SendAlgorithmWithDebugInfos interface. func (b *bbrSender) InRecovery() bool { return b.recoveryState != bbrRecoveryStateNotInRecovery } // GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface. func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { if b.mode == bbrModeProbeRtt { return b.probeRttCongestionWindow() } if b.InRecovery() { return Min(b.congestionWindow, b.recoveryWindow) } return b.congestionWindow } func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { // Do nothing. } func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { totalBytesAckedBefore := b.sampler.TotalBytesAcked() totalBytesLostBefore := b.sampler.TotalBytesLost() var isRoundStart, minRttExpired bool var excessAcked, bytesLost congestion.ByteCount // The send state of the largest packet in acked_packets, unless it is // empty. If acked_packets is empty, it's the send state of the largest // packet in lost_packets. var lastPacketSendState sendTimeState b.maybeAppLimited(priorInFlight) // Update bytesInFlight b.bytesInFlight = priorInFlight for _, p := range ackedPackets { b.bytesInFlight -= p.BytesAcked } for _, p := range lostPackets { b.bytesInFlight -= p.BytesLost } if len(ackedPackets) != 0 { lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber isRoundStart = b.updateRoundTripCounter(lastAckedPacket) b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart) } sample := b.sampler.OnCongestionEvent(eventTime, ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount) if sample.lastPacketSendState.isValid { b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited } // Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all // packets in |acked_packets| did not generate valid samples. (e.g. ack of // ack-only packets). In both cases, sampler_.total_bytes_acked() will not // change. if totalBytesAckedBefore != b.sampler.TotalBytesAcked() { if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() { b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount) } } if sample.sampleRtt != infRTT { minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt) } bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore excessAcked = sample.extraAcked lastPacketSendState = sample.lastPacketSendState if len(lostPackets) != 0 { b.numLossEventsInRound++ b.bytesLostInRound += bytesLost } // Handle logic specific to PROBE_BW mode. if b.mode == bbrModeProbeBw { b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0) } // Handle logic specific to STARTUP and DRAIN modes. if isRoundStart && !b.isAtFullBandwidth { b.checkIfFullBandwidthReached(&lastPacketSendState) } b.maybeExitStartupOrDrain(eventTime) // Handle logic specific to PROBE_RTT. b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) // Calculate number of packets acked and lost. bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore // After the model is updated, recalculate the pacing rate and congestion // window. b.calculatePacingRate(bytesLost) b.calculateCongestionWindow(bytesAcked, excessAcked) b.calculateRecoveryWindow(bytesAcked, bytesLost) // Cleanup internal state. // This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler. // The "least unacked" should actually be FirstOutstanding, but since we are not passing // that through OnCongestionEventEx, we will only do an estimate using acked/lost packets // for now. Because of fast retransmission, they should differ by no more than 2 packets. // (this is controlled by packetThreshold in quic-go's sentPacketHandler) var leastUnacked congestion.PacketNumber if len(ackedPackets) != 0 { leastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2 } else { leastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1 } b.sampler.RemoveObsoletePackets(leastUnacked) if isRoundStart { b.numLossEventsInRound = 0 b.bytesLostInRound = 0 } } func (b *bbrSender) PacingRate() Bandwidth { if b.pacingRate == 0 { return Bandwidth(b.highGain * float64( BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()))) } return b.pacingRate } // Sets the CWND gain used in STARTUP. Must be greater than 1. func (b *bbrSender) setHighCwndGain(highCwndGain float64) { b.highCwndGain = highCwndGain if b.mode == bbrModeStartup { b.congestionWindowGain = highCwndGain } } // Get the current bandwidth estimate. Note that Bandwidth is in bits per second. func (b *bbrSender) bandwidthEstimate() Bandwidth { return b.maxBandwidth.GetBest() } func (b *bbrSender) bandwidthForPacer() congestion.ByteCount { bps := congestion.ByteCount(float64(b.PacingRate()) / float64(BytesPerSecond)) if bps < minBps { // We need to make sure that the bandwidth value for pacer is never zero, // otherwise it will go into an edge case where HasPacingBudget = false // but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck. return minBps } return bps } // Returns the current estimate of the RTT of the connection. Outside of the // edge cases, this is minimum RTT. func (b *bbrSender) getMinRtt() time.Duration { if b.minRtt != 0 { return b.minRtt } // min_rtt could be available if the handshake packet gets neutered then // gets acknowledged. This could only happen for QUIC crypto where we do not // drop keys. minRtt := b.rttStats.MinRTT() if minRtt == 0 { return 100 * time.Millisecond } else { return minRtt } } // Computes the target congestion window using the specified gain. func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount { bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate()) congestionWindow := congestion.ByteCount(gain * float64(bdp)) // BDP estimate will be zero if no bandwidth samples are available yet. if congestionWindow == 0 { congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) } return Max(congestionWindow, b.minCongestionWindow) } // The target congestion window during PROBE_RTT. func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount { return b.minCongestionWindow } func (b *bbrSender) maybeUpdateMinRtt(now monotime.Time, sampleMinRtt time.Duration) bool { // Do not expire min_rtt if none was ever available. minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry)) if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { b.minRtt = sampleMinRtt b.minRttTimestamp = now } return minRttExpired } // Enters the STARTUP mode. func (b *bbrSender) enterStartupMode() { b.mode = bbrModeStartup // b.maybeTraceStateChange(logging.CongestionStateStartup) b.pacingGain = b.highGain b.congestionWindowGain = b.highCwndGain } // Enters the PROBE_BW mode. func (b *bbrSender) enterProbeBandwidthMode(now monotime.Time) { b.mode = bbrModeProbeBw // b.maybeTraceStateChange(logging.CongestionStateProbeBw) b.congestionWindowGain = b.congestionWindowGainConstant // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is // excluded because in that case increased gain and decreased gain would not // follow each other. b.cycleCurrentOffset = int(randv2.Int32N(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) if b.cycleCurrentOffset >= 1 { b.cycleCurrentOffset += 1 } b.lastCycleStart = now b.pacingGain = pacingGain[b.cycleCurrentOffset] } // Updates the round-trip counter if a round-trip has passed. Returns true if // the counter has been advanced. func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd { b.roundTripCount++ b.currentRoundTripEnd = b.lastSentPacket return true } return false } // Updates the current gain used in PROBE_BW mode. func (b *bbrSender) updateGainCyclePhase(now monotime.Time, priorInFlight congestion.ByteCount, hasLosses bool) { // In most cases, the cycle is advanced after an RTT passes. shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt())) // If the pacing gain is above 1.0, the connection is trying to probe the // bandwidth by increasing the number of bytes in flight to at least // pacing_gain * BDP. Make sure that it actually reaches the target, as long // as there are no losses suggesting that the buffers are not able to hold // that much. if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) { shouldAdvanceGainCycling = false } // If pacing gain is below 1.0, the connection is trying to drain the extra // queue which could have been incurred by probing prior to it. If the number // of bytes in flight falls down to the estimated BDP value earlier, conclude // that the queue has been successfully drained and exit this cycle early. if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) { shouldAdvanceGainCycling = true } if shouldAdvanceGainCycling { b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength b.lastCycleStart = now // Stay in low gain mode until the target BDP is hit. // Low gain mode will be exited immediately when the target BDP is achieved. if b.drainToTarget && b.pacingGain < 1 && pacingGain[b.cycleCurrentOffset] == 1 && b.bytesInFlight > b.getTargetCongestionWindow(1) { return } b.pacingGain = pacingGain[b.cycleCurrentOffset] } } // Tracks for how many round-trips the bandwidth has not increased // significantly. func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) { if b.lastSampleIsAppLimited { return } target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget) if b.bandwidthEstimate() >= target { b.bandwidthAtLastRound = b.bandwidthEstimate() b.roundsWithoutBandwidthGain = 0 if b.expireAckAggregationInStartup { // Expire old excess delivery measurements now that bandwidth increased. b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount) } return } b.roundsWithoutBandwidthGain++ if b.roundsWithoutBandwidthGain >= b.numStartupRtts || b.shouldExitStartupDueToLoss(lastPacketSendState) { b.isAtFullBandwidth = true } } func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) { if bytesInFlight < b.getTargetCongestionWindow(1) { b.sampler.OnAppLimited() } } // Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if // appropriate. func (b *bbrSender) maybeExitStartupOrDrain(now monotime.Time) { if b.mode == bbrModeStartup && b.isAtFullBandwidth { b.mode = bbrModeDrain // b.maybeTraceStateChange(logging.CongestionStateDrain) b.pacingGain = b.drainGain b.congestionWindowGain = b.highCwndGain } if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) { b.enterProbeBandwidthMode(now) } } // Decides whether to enter or exit PROBE_RTT. func (b *bbrSender) maybeEnterOrExitProbeRtt(now monotime.Time, isRoundStart, minRttExpired bool) { if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt { b.mode = bbrModeProbeRtt // b.maybeTraceStateChange(logging.CongestionStateProbRtt) b.pacingGain = 1.0 // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| // is at the target small value. b.exitProbeRttAt = 0 } if b.mode == bbrModeProbeRtt { b.sampler.OnAppLimited() // b.maybeTraceStateChange(logging.CongestionStateApplicationLimited) if b.exitProbeRttAt.IsZero() { // If the window has reached the appropriate size, schedule exiting // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but // we allow an extra packet since QUIC checks CWND before sending a // packet. if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize { b.exitProbeRttAt = now.Add(probeRttTime) b.probeRttRoundPassed = false } } else { if isRoundStart { b.probeRttRoundPassed = true } if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed { b.minRttTimestamp = now if !b.isAtFullBandwidth { b.enterStartupMode() } else { b.enterProbeBandwidthMode(now) } } } } b.exitingQuiescence = false } // Determines whether BBR needs to enter, exit or advance state of the // recovery. func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) { // Disable recovery in startup, if loss-based exit is enabled. if !b.isAtFullBandwidth { return } // Exit recovery when there are no losses for a round. if hasLosses { b.endRecoveryAt = b.lastSentPacket } switch b.recoveryState { case bbrRecoveryStateNotInRecovery: if hasLosses { b.recoveryState = bbrRecoveryStateConservation // This will cause the |recovery_window_| to be set to the correct // value in CalculateRecoveryWindow(). b.recoveryWindow = 0 // Since the conservation phase is meant to be lasting for a whole // round, extend the current round as if it were started right now. b.currentRoundTripEnd = b.lastSentPacket } case bbrRecoveryStateConservation: if isRoundStart { b.recoveryState = bbrRecoveryStateGrowth } fallthrough case bbrRecoveryStateGrowth: // Exit recovery if appropriate. if !hasLosses && lastAckedPacket > b.endRecoveryAt { b.recoveryState = bbrRecoveryStateNotInRecovery } } } // Determines the appropriate pacing rate for the connection. func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) { if b.bandwidthEstimate() == 0 { return } targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate())) if b.isAtFullBandwidth { b.pacingRate = targetRate return } // Pace at the rate of initial_window / RTT as soon as RTT measurements are // available. if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 { b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) return } if b.detectOvershooting { b.bytesLostWhileDetectingOvershooting += bytesLost // Check for overshooting with network parameters adjusted when pacing rate // > target_rate and loss has been detected. if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 { if b.hasNoAppLimitedSample || b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow { // We are fairly sure overshoot happens if 1) there is at least one // non app-limited bw sample or 2) half of IW gets lost. Slow pacing // rate. b.pacingRate = Max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT())) b.bytesLostWhileDetectingOvershooting = 0 b.detectOvershooting = false } } } // Do not decrease the pacing rate during startup. b.pacingRate = Max(b.pacingRate, targetRate) } // Determines the appropriate congestion window for the connection. func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) { if b.mode == bbrModeProbeRtt { return } targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain) if b.isAtFullBandwidth { // Add the max recently measured ack aggregation to CWND. targetWindow += b.sampler.MaxAckHeight() } else if b.enableAckAggregationDuringStartup { // Add the most recent excess acked. Because CWND never decreases in // STARTUP, this will automatically create a very localized max filter. targetWindow += excessAcked } // Instead of immediately setting the target CWND as the new one, BBR grows // the CWND towards |target_window| by only increasing it |bytes_acked| at a // time. if b.isAtFullBandwidth { b.congestionWindow = Min(targetWindow, b.congestionWindow+bytesAcked) } else if b.congestionWindow < targetWindow || b.sampler.TotalBytesAcked() < b.initialCongestionWindow { // If the connection is not yet out of startup phase, do not decrease the // window. b.congestionWindow += bytesAcked } // Enforce the limits on the congestion window. b.congestionWindow = Max(b.congestionWindow, b.minCongestionWindow) b.congestionWindow = Min(b.congestionWindow, b.maxCongestionWindow) } // Determines the appropriate window that constrains the in-flight during recovery. func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) { if b.recoveryState == bbrRecoveryStateNotInRecovery { return } // Set up the initial recovery window. if b.recoveryWindow == 0 { b.recoveryWindow = b.bytesInFlight + bytesAcked b.recoveryWindow = Max(b.minCongestionWindow, b.recoveryWindow) return } // Remove losses from the recovery window, while accounting for a potential // integer underflow. if b.recoveryWindow >= bytesLost { b.recoveryWindow = b.recoveryWindow - bytesLost } else { b.recoveryWindow = b.maxDatagramSize } // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, // release additional |bytes_acked| to achieve a slow-start-like behavior. if b.recoveryState == bbrRecoveryStateGrowth { b.recoveryWindow += bytesAcked } // Always allow sending at least |bytes_acked| in response. b.recoveryWindow = Max(b.recoveryWindow, b.bytesInFlight+bytesAcked) b.recoveryWindow = Max(b.minCongestionWindow, b.recoveryWindow) } // Return whether we should exit STARTUP due to excessive loss. func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool { if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid { return false } inflightAtSend := lastPacketSendState.bytesInFlight if inflightAtSend > 0 && b.bytesLostInRound > 0 { if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) { return true } return false } return false } func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount { return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second) } func GetInitialPacketSize(quicConn *quic.Conn) congestion.ByteCount { return congestion.ByteCount(quicConn.Config().InitialPacketSize) } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/minmax_go120.go ================================================ //go:build !go1.21 package congestion import "golang.org/x/exp/constraints" func Max[T constraints.Ordered](a, b T) T { if a < b { return b } return a } func Min[T constraints.Ordered](a, b T) T { if a < b { return a } return b } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/minmax_go121.go ================================================ //go:build go1.21 package congestion import "cmp" func Max[T cmp.Ordered](a, b T) T { return max(a, b) } func Min[T cmp.Ordered](a, b T) T { return min(a, b) } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/pacer.go ================================================ package congestion import ( "time" "github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/monotime" ) const ( maxBurstPackets = 10 maxBurstPacingDelayMultiplier = 4 ) // Pacer implements a token bucket pacing algorithm. type Pacer struct { budgetAtLastSent congestion.ByteCount maxDatagramSize congestion.ByteCount lastSentTime monotime.Time getBandwidth func() congestion.ByteCount // in bytes/s } func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer { p := &Pacer{ budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSize, maxDatagramSize: congestion.InitialPacketSize, getBandwidth: getBandwidth, } return p } func (p *Pacer) SentPacket(sendTime monotime.Time, size congestion.ByteCount) { budget := p.Budget(sendTime) if size > budget { p.budgetAtLastSent = 0 } else { p.budgetAtLastSent = budget - size } p.lastSentTime = sendTime } func (p *Pacer) Budget(now monotime.Time) congestion.ByteCount { if p.lastSentTime.IsZero() { return p.maxBurstSize() } budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 if budget < 0 { // protect against overflows budget = congestion.ByteCount(1<<62 - 1) } return Min(p.maxBurstSize(), budget) } func (p *Pacer) maxBurstSize() congestion.ByteCount { return Max( congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9, maxBurstPackets*p.maxDatagramSize, ) } // TimeUntilSend returns when the next packet should be sent. // It returns the zero value if a packet can be sent immediately. func (p *Pacer) TimeUntilSend() monotime.Time { if p.budgetAtLastSent >= p.maxDatagramSize { return 0 } diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent) bw := uint64(p.getBandwidth()) // We might need to round up this value. // Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires. d := diff / bw // this is effectively a math.Ceil, but using only integer math if diff%bw > 0 { d++ } return p.lastSentTime.Add(Max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond)) } func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) { p.maxDatagramSize = s } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/packet_number_indexed_queue.go ================================================ package congestion import ( "github.com/metacubex/quic-go/congestion" ) // packetNumberIndexedQueue is a queue of mostly continuous numbered entries // which supports the following operations: // - adding elements to the end of the queue, or at some point past the end // - removing elements in any order // - retrieving elements // If all elements are inserted in order, all of the operations above are // amortized O(1) time. // // Internally, the data structure is a deque where each element is marked as // present or not. The deque starts at the lowest present index. Whenever an // element is removed, it's marked as not present, and the front of the deque is // cleared of elements that are not present. // // The tail of the queue is not cleared due to the assumption of entries being // inserted in order, though removing all elements of the queue will return it // to its initial state. // // Note that this data structure is inherently hazardous, since an addition of // just two entries will cause it to consume all of the memory available. // Because of that, it is not a general-purpose container and should not be used // as one. type entryWrapper[T any] struct { present bool entry T } type packetNumberIndexedQueue[T any] struct { entries RingBuffer[entryWrapper[T]] numberOfPresentEntries int firstPacket congestion.PacketNumber } func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] { q := &packetNumberIndexedQueue[T]{ firstPacket: invalidPacketNumber, } q.entries.Init(size) return q } // Emplace inserts data associated |packet_number| into (or past) the end of the // queue, filling up the missing intermediate entries as necessary. Returns // true if the element has been inserted successfully, false if it was already // in the queue or inserted out of order. func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool { if packetNumber == invalidPacketNumber || entry == nil { return false } if p.IsEmpty() { p.entries.PushBack(entryWrapper[T]{ present: true, entry: *entry, }) p.numberOfPresentEntries = 1 p.firstPacket = packetNumber return true } // Do not allow insertion out-of-order. if packetNumber <= p.LastPacket() { return false } // Handle potentially missing elements. offset := int(packetNumber - p.FirstPacket()) if gap := offset - p.entries.Len(); gap > 0 { for i := 0; i < gap; i++ { p.entries.PushBack(entryWrapper[T]{}) } } p.entries.PushBack(entryWrapper[T]{ present: true, entry: *entry, }) p.numberOfPresentEntries++ return true } // GetEntry Retrieve the entry associated with the packet number. Returns the pointer // to the entry in case of success, or nullptr if the entry does not exist. func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T { ew := p.getEntryWraper(packetNumber) if ew == nil { return nil } return &ew.entry } // Remove, Same as above, but if an entry is present in the queue, also call f(entry) // before removing it. func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool { ew := p.getEntryWraper(packetNumber) if ew == nil { return false } if f != nil { f(ew.entry) } ew.present = false p.numberOfPresentEntries-- if packetNumber == p.FirstPacket() { p.clearup() } return true } // RemoveUpTo, but not including |packet_number|. // Unused slots in the front are also removed, which means when the function // returns, |first_packet()| can be larger than |packet_number|. func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) { for !p.entries.Empty() && p.firstPacket != invalidPacketNumber && p.firstPacket < packetNumber { if p.entries.Front().present { p.numberOfPresentEntries-- } p.entries.PopFront() p.firstPacket++ } p.clearup() return } // IsEmpty return if queue is empty. func (p *packetNumberIndexedQueue[T]) IsEmpty() bool { return p.numberOfPresentEntries == 0 } // NumberOfPresentEntries returns the number of entries in the queue. func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int { return p.numberOfPresentEntries } // EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is // proportional to the memory usage of the queue. func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int { return p.entries.Len() } // FirstPacket returns packet number of the first entry in the queue. func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) { return p.firstPacket } // LastPacket returns packet number of the last entry ever inserted in the queue. Note that the // entry in question may have already been removed. Zero if the queue is // empty. func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) { if p.IsEmpty() { return invalidPacketNumber } return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1) } func (p *packetNumberIndexedQueue[T]) clearup() { for !p.entries.Empty() && !p.entries.Front().present { p.entries.PopFront() p.firstPacket++ } if p.entries.Empty() { p.firstPacket = invalidPacketNumber } } func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] { if packetNumber == invalidPacketNumber || p.IsEmpty() || packetNumber < p.firstPacket { return nil } offset := int(packetNumber - p.firstPacket) if offset >= p.entries.Len() { return nil } ew := p.entries.Offset(offset) if ew == nil || !ew.present { return nil } return ew } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/ringbuffer.go ================================================ package congestion // A RingBuffer is a ring buffer. // It acts as a heap that doesn't cause any allocations. type RingBuffer[T any] struct { ring []T headPos, tailPos int full bool } // Init preallocs a buffer with a certain size. func (r *RingBuffer[T]) Init(size int) { r.ring = make([]T, size) } // Len returns the number of elements in the ring buffer. func (r *RingBuffer[T]) Len() int { if r.full { return len(r.ring) } if r.tailPos >= r.headPos { return r.tailPos - r.headPos } return r.tailPos - r.headPos + len(r.ring) } // Empty says if the ring buffer is empty. func (r *RingBuffer[T]) Empty() bool { return !r.full && r.headPos == r.tailPos } // PushBack adds a new element. // If the ring buffer is full, its capacity is increased first. func (r *RingBuffer[T]) PushBack(t T) { if r.full || len(r.ring) == 0 { r.grow() } r.ring[r.tailPos] = t r.tailPos++ if r.tailPos == len(r.ring) { r.tailPos = 0 } if r.tailPos == r.headPos { r.full = true } } // PopFront returns the next element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first. func (r *RingBuffer[T]) PopFront() T { if r.Empty() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue") } r.full = false t := r.ring[r.headPos] r.ring[r.headPos] = *new(T) r.headPos++ if r.headPos == len(r.ring) { r.headPos = 0 } return t } // Offset returns the offset element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first // and check if the index larger than buffer length. func (r *RingBuffer[T]) Offset(index int) *T { if r.Empty() || index >= r.Len() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index") } offset := (r.headPos + index) % len(r.ring) return &r.ring[offset] } // Front returns the front element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first. func (r *RingBuffer[T]) Front() *T { if r.Empty() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue") } return &r.ring[r.headPos] } // Back returns the back element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first. func (r *RingBuffer[T]) Back() *T { if r.Empty() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue") } return r.Offset(r.Len() - 1) } // Grow the maximum size of the queue. // This method assume the queue is full. func (r *RingBuffer[T]) grow() { oldRing := r.ring newSize := len(oldRing) * 2 if newSize == 0 { newSize = 1 } r.ring = make([]T, newSize) headLen := copy(r.ring, oldRing[r.headPos:]) copy(r.ring[headLen:], oldRing[:r.headPos]) r.headPos, r.tailPos, r.full = 0, len(oldRing), false } // Clear removes all elements. func (r *RingBuffer[T]) Clear() { var zeroValue T for i := range r.ring { r.ring[i] = zeroValue } r.headPos, r.tailPos, r.full = 0, 0, false } ================================================ FILE: core/Clash.Meta/transport/tuic/congestion_v2/windowed_filter.go ================================================ package congestion import ( "golang.org/x/exp/constraints" ) // Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum) // estimate of a stream of samples over some fixed time interval. (E.g., // the minimum RTT over the past five minutes.) The algorithm keeps track of // the best, second best, and third best min (or max) estimates, maintaining an // invariant that the measurement time of the n'th best >= n-1'th best. // The algorithm works as follows. On a reset, all three estimates are set to // the same sample. The second best estimate is then recorded in the second // quarter of the window, and a third best estimate is recorded in the second // half of the window, bounding the worst case error when the true min is // monotonically increasing (or true max is monotonically decreasing) over the // window. // // A new best sample replaces all three estimates, since the new best is lower // (or higher) than everything else in the window and it is the most recent. // The window thus effectively gets reset on every new min. The same property // holds true for second best and third best estimates. Specifically, when a // sample arrives that is better than the second best but not better than the // best, it replaces the second and third best estimates but not the best // estimate. Similarly, a sample that is better than the third best estimate // but not the other estimates replaces only the third best estimate. // // Finally, when the best expires, it is replaced by the second best, which in // turn is replaced by the third best. The newest sample replaces the third // best. type WindowedFilterValue interface { any } type WindowedFilterTime interface { constraints.Integer | constraints.Float } type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct { // Time length of window. windowLength T estimates []entry[V, T] comparator func(V, V) int } type entry[V WindowedFilterValue, T WindowedFilterTime] struct { sample V time T } // Compares two values and returns true if the first is greater than or equal // to the second. func MaxFilter[O constraints.Ordered](a, b O) int { if a > b { return 1 } else if a < b { return -1 } return 0 } // Compares two values and returns true if the first is less than or equal // to the second. func MinFilter[O constraints.Ordered](a, b O) int { if a < b { return 1 } else if a > b { return -1 } return 0 } func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] { return &WindowedFilter[V, T]{ windowLength: windowLength, estimates: make([]entry[V, T], 3, 3), comparator: comparator, } } // Changes the window length. Does not update any current samples. func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) { f.windowLength = windowLength } func (f *WindowedFilter[V, T]) GetBest() V { return f.estimates[0].sample } func (f *WindowedFilter[V, T]) GetSecondBest() V { return f.estimates[1].sample } func (f *WindowedFilter[V, T]) GetThirdBest() V { return f.estimates[2].sample } // Updates best estimates with |sample|, and expires and updates best // estimates as necessary. func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) { // Reset all estimates if they have not yet been initialized, if new sample // is a new best, or if the newest recorded estimate is too old. if f.comparator(f.estimates[0].sample, *new(V)) == 0 || f.comparator(newSample, f.estimates[0].sample) >= 0 || newTime-f.estimates[2].time > f.windowLength { f.Reset(newSample, newTime) return } if f.comparator(newSample, f.estimates[1].sample) >= 0 { f.estimates[1] = entry[V, T]{newSample, newTime} f.estimates[2] = f.estimates[1] } else if f.comparator(newSample, f.estimates[2].sample) >= 0 { f.estimates[2] = entry[V, T]{newSample, newTime} } // Expire and update estimates as necessary. if newTime-f.estimates[0].time > f.windowLength { // The best estimate hasn't been updated for an entire window, so promote // second and third best estimates. f.estimates[0] = f.estimates[1] f.estimates[1] = f.estimates[2] f.estimates[2] = entry[V, T]{newSample, newTime} // Need to iterate one more time. Check if the new best estimate is // outside the window as well, since it may also have been recorded a // long time ago. Don't need to iterate once more since we cover that // case at the beginning of the method. if newTime-f.estimates[0].time > f.windowLength { f.estimates[0] = f.estimates[1] f.estimates[1] = f.estimates[2] } return } if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 && newTime-f.estimates[1].time > f.windowLength/4 { // A quarter of the window has passed without a better sample, so the // second-best estimate is taken from the second quarter of the window. f.estimates[1] = entry[V, T]{newSample, newTime} f.estimates[2] = f.estimates[1] return } if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 && newTime-f.estimates[2].time > f.windowLength/2 { // We've passed a half of the window without a better estimate, so take // a third-best estimate from the second half of the window. f.estimates[2] = entry[V, T]{newSample, newTime} } } // Resets all estimates to new sample. func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) { f.estimates[2] = entry[V, T]{newSample, newTime} f.estimates[1] = f.estimates[2] f.estimates[0] = f.estimates[1] } func (f *WindowedFilter[V, T]) Clear() { f.estimates = make([]entry[V, T], 3, 3) } ================================================ FILE: core/Clash.Meta/transport/tuic/pool_client.go ================================================ package tuic import ( "context" "errors" "net" "sync" "time" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" list "github.com/bahlo/generic-list-go" ) type PoolClient struct { newClientOptionV4 *ClientOptionV4 newClientOptionV5 *ClientOptionV5 dialFn DialFunc tcpClients list.List[Client] tcpClientsMutex sync.Mutex udpClients list.List[Client] udpClientsMutex sync.Mutex } func (t *PoolClient) DialContext(ctx context.Context, metadata *C.Metadata) (net.Conn, error) { conn, err := t.getClient(false).DialContext(ctx, metadata) if errors.Is(err, TooManyOpenStreams) { conn, err = t.newClient(false).DialContext(ctx, metadata) } if err != nil { return nil, err } return N.NewRefConn(conn, t), err } func (t *PoolClient) ListenPacket(ctx context.Context, metadata *C.Metadata) (net.PacketConn, error) { pc, err := t.getClient(true).ListenPacket(ctx, metadata) if errors.Is(err, TooManyOpenStreams) { pc, err = t.newClient(true).ListenPacket(ctx, metadata) } if err != nil { return nil, err } return N.NewRefPacketConn(pc, t), nil } func (t *PoolClient) newClient(udp bool) (client Client) { clients := &t.tcpClients clientsMutex := &t.tcpClientsMutex if udp { clients = &t.udpClients clientsMutex = &t.udpClientsMutex } clientsMutex.Lock() defer clientsMutex.Unlock() if t.newClientOptionV4 != nil { client = NewClientV4(t.newClientOptionV4, udp, t.dialFn) } else { client = NewClientV5(t.newClientOptionV5, udp, t.dialFn) } client.SetLastVisited(time.Now()) clients.PushFront(client) return client } func (t *PoolClient) getClient(udp bool) Client { clients := &t.tcpClients clientsMutex := &t.tcpClientsMutex if udp { clients = &t.udpClients clientsMutex = &t.udpClientsMutex } var bestClient Client func() { clientsMutex.Lock() defer clientsMutex.Unlock() for it := clients.Front(); it != nil; { client := it.Value if client == nil { next := it.Next() clients.Remove(it) it = next continue } if bestClient == nil { bestClient = client } else { if client.OpenStreams() < bestClient.OpenStreams() { bestClient = client } } it = it.Next() } for it := clients.Front(); it != nil; { client := it.Value if client != bestClient && client.OpenStreams() == 0 && time.Now().Sub(client.LastVisited()) > 30*time.Minute { client.Close() next := it.Next() clients.Remove(it) it = next continue } it = it.Next() } }() if bestClient == nil { return t.newClient(udp) } else { bestClient.SetLastVisited(time.Now()) return bestClient } } func NewPoolClientV4(clientOption *ClientOptionV4, dialFn DialFunc) *PoolClient { p := &PoolClient{ dialFn: dialFn, } newClientOption := *clientOption p.newClientOptionV4 = &newClientOption return p } func NewPoolClientV5(clientOption *ClientOptionV5, dialFn DialFunc) *PoolClient { p := &PoolClient{ dialFn: dialFn, } newClientOption := *clientOption p.newClientOptionV5 = &newClientOption return p } ================================================ FILE: core/Clash.Meta/transport/tuic/server.go ================================================ package tuic import ( "bufio" "context" "net" "time" "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/mihomo/transport/tuic/types" v4 "github.com/metacubex/mihomo/transport/tuic/v4" v5 "github.com/metacubex/mihomo/transport/tuic/v5" "github.com/gofrs/uuid/v5" "github.com/metacubex/quic-go" "github.com/metacubex/tls" ) type ServerOption struct { HandleTcpFn func(conn net.Conn, addr socks5.Addr, additions ...inbound.Addition) error HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket, additions ...inbound.Addition) error TlsConfig *tls.Config QuicConfig *quic.Config Tokens [][32]byte // V4 special Users map[[16]byte]string // V5 special CongestionController string AuthenticationTimeout time.Duration MaxUdpRelayPacketSize int CWND int BBRProfile string } type Server struct { *ServerOption optionV4 *v4.ServerOption optionV5 *v5.ServerOption listener *quic.EarlyListener } func (s *Server) Serve() error { for { conn, err := s.listener.Accept(context.Background()) if err != nil { return err } common.SetCongestionController(conn, s.CongestionController, s.CWND, s.BBRProfile) h := &serverHandler{ Server: s, quicConn: conn, uuid: utils.NewUUIDV4(), } if h.optionV4 != nil { h.v4Handler = v4.NewServerHandler(h.optionV4, conn, h.uuid) } if h.optionV5 != nil { h.v5Handler = v5.NewServerHandler(h.optionV5, conn, h.uuid) } go h.handle() } } func (s *Server) Close() error { return s.listener.Close() } type serverHandler struct { *Server quicConn *quic.Conn uuid uuid.UUID v4Handler types.ServerHandler v5Handler types.ServerHandler } func (s *serverHandler) handle() { go func() { _ = s.handleUniStream() }() go func() { _ = s.handleStream() }() go func() { _ = s.handleMessage() }() select { case <-s.quicConn.HandshakeComplete(): // this chan maybe not closed if handshake never complete case <-time.After(s.quicConn.Config().HandshakeIdleTimeout): // HandshakeIdleTimeout in real conn.Config() never be zero } time.AfterFunc(s.AuthenticationTimeout, func() { if s.v4Handler != nil { if s.v4Handler.AuthOk() { return } } if s.v5Handler != nil { if s.v5Handler.AuthOk() { return } } if s.v4Handler != nil { s.v4Handler.HandleTimeout() } if s.v5Handler != nil { s.v5Handler.HandleTimeout() } }) } func (s *serverHandler) handleMessage() (err error) { for { var message []byte message, err = s.quicConn.ReceiveDatagram(context.Background()) if err != nil { return err } go func() (err error) { if len(message) > 0 { switch message[0] { case v4.VER: if s.v4Handler != nil { return s.v4Handler.HandleMessage(message) } case v5.VER: if s.v5Handler != nil { return s.v5Handler.HandleMessage(message) } } } return }() } } func (s *serverHandler) handleStream() (err error) { for { var quicStream *quic.Stream quicStream, err = s.quicConn.AcceptStream(context.Background()) if err != nil { return err } go func() (err error) { stream := types.NewQuicStreamConn( quicStream, s.quicConn.LocalAddr(), s.quicConn.RemoteAddr(), nil, ) conn := N.NewBufferedConn(stream) verBytes, err := conn.Peek(1) if err != nil { _ = conn.Close() return err } switch verBytes[0] { case v4.VER: if s.v4Handler != nil { return s.v4Handler.HandleStream(conn) } case v5.VER: if s.v5Handler != nil { return s.v5Handler.HandleStream(conn) } } return }() } } func (s *serverHandler) handleUniStream() (err error) { for { var stream *quic.ReceiveStream stream, err = s.quicConn.AcceptUniStream(context.Background()) if err != nil { return err } go func() (err error) { defer func() { stream.CancelRead(0) }() reader := bufio.NewReader(stream) verBytes, err := reader.Peek(1) if err != nil { return err } switch verBytes[0] { case v4.VER: if s.v4Handler != nil { return s.v4Handler.HandleUniStream(reader) } case v5.VER: if s.v5Handler != nil { return s.v5Handler.HandleUniStream(reader) } } return }() } } func NewServer(option *ServerOption, pc net.PacketConn) (*Server, error) { listener, err := quic.ListenEarly(pc, option.TlsConfig, option.QuicConfig) if err != nil { return nil, err } server := &Server{ ServerOption: option, listener: listener, } if len(option.Tokens) > 0 { server.optionV4 = &v4.ServerOption{ HandleTcpFn: option.HandleTcpFn, HandleUdpFn: option.HandleUdpFn, Tokens: option.Tokens, MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, } } if len(option.Users) > 0 { maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize if maxUdpRelayPacketSize > MaxFragSizeV5 { maxUdpRelayPacketSize = MaxFragSizeV5 } server.optionV5 = &v5.ServerOption{ HandleTcpFn: option.HandleTcpFn, HandleUdpFn: option.HandleUdpFn, Users: option.Users, MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, } } return server, nil } ================================================ FILE: core/Clash.Meta/transport/tuic/tuic.go ================================================ package tuic import ( "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/mihomo/transport/tuic/types" v4 "github.com/metacubex/mihomo/transport/tuic/v4" v5 "github.com/metacubex/mihomo/transport/tuic/v5" ) type ClientOptionV4 = v4.ClientOption type ClientOptionV5 = v5.ClientOption type Client = types.Client func NewClientV4(clientOption *ClientOptionV4, udp bool, dialFn DialFunc) Client { return v4.NewClient(clientOption, udp, dialFn) } func NewClientV5(clientOption *ClientOptionV5, udp bool, dialFn DialFunc) Client { return v5.NewClient(clientOption, udp, dialFn) } type DialFunc = types.DialFunc var TooManyOpenStreams = types.TooManyOpenStreams const DefaultStreamReceiveWindow = common.DefaultStreamReceiveWindow const DefaultConnectionReceiveWindow = common.DefaultConnectionReceiveWindow var GenTKN = v4.GenTKN var PacketOverHeadV4 = v4.PacketOverHead var PacketOverHeadV5 = v5.PacketOverHead var MaxFragSizeV5 = v5.MaxFragSize type UdpRelayMode = types.UdpRelayMode const ( QUIC = types.QUIC NATIVE = types.NATIVE ) ================================================ FILE: core/Clash.Meta/transport/tuic/types/stream.go ================================================ package types import ( "net" "sync" "time" "github.com/metacubex/quic-go" ) type quicStreamConn struct { *quic.Stream lock sync.Mutex lAddr net.Addr rAddr net.Addr closeDeferFn func() closeOnce sync.Once closeErr error } func (q *quicStreamConn) Write(p []byte) (n int, err error) { q.lock.Lock() defer q.lock.Unlock() return q.Stream.Write(p) } func (q *quicStreamConn) Close() error { q.closeOnce.Do(func() { q.closeErr = q.close() }) return q.closeErr } func (q *quicStreamConn) close() error { if q.closeDeferFn != nil { defer q.closeDeferFn() } // https://github.com/cloudflare/cloudflared/commit/ed2bac026db46b239699ac5ce4fcf122d7cab2cd // Make sure a possible writer does not block the lock forever. We need it, so we can close the writer // side of the stream safely. _ = q.Stream.SetWriteDeadline(time.Now()) // This lock is eventually acquired despite Write also acquiring it, because we set a deadline to writes. q.lock.Lock() defer q.lock.Unlock() // We have to clean up the receiving stream ourselves since the Close in the bottom does not handle that. q.Stream.CancelRead(0) return q.Stream.Close() } func (q *quicStreamConn) LocalAddr() net.Addr { return q.lAddr } func (q *quicStreamConn) RemoteAddr() net.Addr { return q.rAddr } var _ net.Conn = (*quicStreamConn)(nil) func NewQuicStreamConn(stream *quic.Stream, lAddr, rAddr net.Addr, closeDeferFn func()) net.Conn { return &quicStreamConn{Stream: stream, lAddr: lAddr, rAddr: rAddr, closeDeferFn: closeDeferFn} } ================================================ FILE: core/Clash.Meta/transport/tuic/types/type.go ================================================ package types import ( "bufio" "context" "errors" "net" "time" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/quic-go" ) var ( ClientClosed = errors.New("tuic: client closed") TooManyOpenStreams = errors.New("tuic: too many open streams") ) type DialFunc func(ctx context.Context) (quicConn *quic.Conn, err error) type Client interface { DialContext(ctx context.Context, metadata *C.Metadata) (net.Conn, error) ListenPacket(ctx context.Context, metadata *C.Metadata) (net.PacketConn, error) OpenStreams() int64 LastVisited() time.Time SetLastVisited(last time.Time) Close() } type ServerHandler interface { AuthOk() bool HandleTimeout() HandleStream(conn *N.BufferedConn) (err error) HandleMessage(message []byte) (err error) HandleUniStream(reader *bufio.Reader) (err error) } type UdpRelayMode uint8 const ( QUIC UdpRelayMode = iota NATIVE ) ================================================ FILE: core/Clash.Meta/transport/tuic/v4/client.go ================================================ package v4 import ( "bufio" "bytes" "context" "errors" "net" "runtime" "sync" "sync/atomic" "time" atomic2 "github.com/metacubex/mihomo/common/atomic" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/xsync" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/tuic/types" "github.com/metacubex/quic-go" "github.com/metacubex/randv2" ) type ClientOption struct { Token [32]byte UdpRelayMode types.UdpRelayMode RequestTimeout time.Duration MaxUdpRelayPacketSize int FastOpen bool MaxOpenStreams int64 } type clientImpl struct { *ClientOption dialFn types.DialFunc udp bool quicConn *quic.Conn connMutex sync.Mutex openStreams atomic.Int64 closed atomic.Bool udpInputMap xsync.Map[uint32, net.Conn] // only ready for PoolClient lastVisited atomic2.TypedValue[time.Time] } func (t *clientImpl) OpenStreams() int64 { return t.openStreams.Load() } func (t *clientImpl) LastVisited() time.Time { return t.lastVisited.Load() } func (t *clientImpl) SetLastVisited(last time.Time) { t.lastVisited.Store(last) } func (t *clientImpl) getQuicConn(ctx context.Context) (*quic.Conn, error) { t.connMutex.Lock() defer t.connMutex.Unlock() if t.quicConn != nil { return t.quicConn, nil } quicConn, err := t.dialFn(ctx) if err != nil { return nil, err } go func() { _ = t.sendAuthentication(quicConn) }() if t.udp { go func() { switch t.UdpRelayMode { case types.QUIC: _ = t.handleUniStream(quicConn) default: // native _ = t.handleMessage(quicConn) } }() } t.quicConn = quicConn t.openStreams.Store(0) return quicConn, nil } func (t *clientImpl) sendAuthentication(quicConn *quic.Conn) (err error) { defer func() { t.deferQuicConn(quicConn, err) }() stream, err := quicConn.OpenUniStream() if err != nil { return err } buf := pool.GetBuffer() defer pool.PutBuffer(buf) err = NewAuthenticate(t.Token).WriteTo(buf) if err != nil { return err } _, err = buf.WriteTo(stream) if err != nil { return err } err = stream.Close() if err != nil { return } return nil } func (t *clientImpl) handleUniStream(quicConn *quic.Conn) (err error) { defer func() { t.deferQuicConn(quicConn, err) }() for { var stream *quic.ReceiveStream stream, err = quicConn.AcceptUniStream(context.Background()) if err != nil { return err } go func() (err error) { var assocId uint32 defer func() { t.deferQuicConn(quicConn, err) if err != nil && assocId != 0 { if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { if conn, ok := val.(net.Conn); ok { _ = conn.Close() } } } stream.CancelRead(0) }() reader := bufio.NewReader(stream) commandHead, err := ReadCommandHead(reader) if err != nil { return } switch commandHead.TYPE { case PacketType: var packet Packet packet, err = ReadPacketWithHead(commandHead, reader) if err != nil { return } if t.udp && t.UdpRelayMode == types.QUIC { assocId = packet.ASSOC_ID if val, ok := t.udpInputMap.Load(assocId); ok { if conn, ok := val.(net.Conn); ok { writer := bufio.NewWriterSize(conn, packet.BytesLen()) _ = packet.WriteTo(writer) _ = writer.Flush() } } } } return }() } } func (t *clientImpl) handleMessage(quicConn *quic.Conn) (err error) { defer func() { t.deferQuicConn(quicConn, err) }() for { var message []byte message, err = quicConn.ReceiveDatagram(context.Background()) if err != nil { return err } go func() (err error) { var assocId uint32 defer func() { t.deferQuicConn(quicConn, err) if err != nil && assocId != 0 { if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { if conn, ok := val.(net.Conn); ok { _ = conn.Close() } } } }() reader := bytes.NewBuffer(message) commandHead, err := ReadCommandHead(reader) if err != nil { return } switch commandHead.TYPE { case PacketType: var packet Packet packet, err = ReadPacketWithHead(commandHead, reader) if err != nil { return } if t.udp && t.UdpRelayMode == types.NATIVE { assocId = packet.ASSOC_ID if val, ok := t.udpInputMap.Load(assocId); ok { if conn, ok := val.(net.Conn); ok { _, _ = conn.Write(message) } } } } return }() } } func (t *clientImpl) deferQuicConn(quicConn *quic.Conn, err error) { var netError net.Error if err != nil && errors.As(err, &netError) { t.forceClose(quicConn, err) } } func (t *clientImpl) forceClose(quicConn *quic.Conn, err error) { t.connMutex.Lock() defer t.connMutex.Unlock() if quicConn == nil { quicConn = t.quicConn } if quicConn != nil { if quicConn == t.quicConn { t.quicConn = nil } } errStr := "" if err != nil { errStr = err.Error() } if quicConn != nil { _ = quicConn.CloseWithError(ProtocolError, errStr) } udpInputMap := &t.udpInputMap udpInputMap.Range(func(key uint32, value net.Conn) bool { conn := value _ = conn.Close() udpInputMap.Delete(key) return true }) } func (t *clientImpl) Close() { t.closed.Store(true) if t.openStreams.Load() == 0 { t.forceClose(nil, types.ClientClosed) } } func (t *clientImpl) DialContext(ctx context.Context, metadata *C.Metadata) (net.Conn, error) { quicConn, err := t.getQuicConn(ctx) if err != nil { return nil, err } openStreams := t.openStreams.Add(1) if openStreams >= t.MaxOpenStreams { t.openStreams.Add(-1) return nil, types.TooManyOpenStreams } stream, err := func() (stream net.Conn, err error) { defer func() { t.deferQuicConn(quicConn, err) }() buf := pool.GetBuffer() defer pool.PutBuffer(buf) err = NewConnect(NewAddress(metadata)).WriteTo(buf) if err != nil { return nil, err } quicStream, err := quicConn.OpenStream() if err != nil { return nil, err } stream = types.NewQuicStreamConn( quicStream, quicConn.LocalAddr(), quicConn.RemoteAddr(), func() { time.AfterFunc(C.DefaultTCPTimeout, func() { openStreams := t.openStreams.Add(-1) if openStreams == 0 && t.closed.Load() { t.forceClose(quicConn, types.ClientClosed) } }) }, ) _, err = buf.WriteTo(stream) if err != nil { _ = stream.Close() return nil, err } return stream, err }() if err != nil { return nil, err } bufConn := N.NewBufferedConn(stream) response := func() error { if t.RequestTimeout > 0 { _ = bufConn.SetReadDeadline(time.Now().Add(t.RequestTimeout)) } response, err := ReadResponse(bufConn) if err != nil { _ = bufConn.Close() return err } if response.IsFailed() { _ = bufConn.Close() return errors.New("connect failed") } _ = bufConn.SetReadDeadline(time.Time{}) return nil } if t.FastOpen { return N.NewEarlyConn(bufConn, response), nil } err = response() if err != nil { return nil, err } return bufConn, nil } func (t *clientImpl) ListenPacket(ctx context.Context, metadata *C.Metadata) (net.PacketConn, error) { quicConn, err := t.getQuicConn(ctx) if err != nil { return nil, err } openStreams := t.openStreams.Add(1) if openStreams >= t.MaxOpenStreams { t.openStreams.Add(-1) return nil, types.TooManyOpenStreams } pipe1, pipe2 := N.Pipe() var connId uint32 for { connId = randv2.Uint32() _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) if !loaded { break } } pc := &quicStreamPacketConn{ connId: connId, quicConn: quicConn, inputConn: N.NewBufferedConn(pipe2), udpRelayMode: t.UdpRelayMode, maxUdpRelayPacketSize: t.MaxUdpRelayPacketSize, deferQuicConnFn: t.deferQuicConn, closeDeferFn: func() { t.udpInputMap.Delete(connId) time.AfterFunc(C.DefaultUDPTimeout, func() { openStreams := t.openStreams.Add(-1) if openStreams == 0 && t.closed.Load() { t.forceClose(quicConn, types.ClientClosed) } }) }, } return pc, nil } type Client struct { *clientImpl // use an independent pointer to let Finalizer can work no matter somewhere handle an influence in clientImpl inner } func (t *Client) DialContext(ctx context.Context, metadata *C.Metadata) (net.Conn, error) { conn, err := t.clientImpl.DialContext(ctx, metadata) if err != nil { return nil, err } return N.NewRefConn(conn, t), err } func (t *Client) ListenPacket(ctx context.Context, metadata *C.Metadata) (net.PacketConn, error) { pc, err := t.clientImpl.ListenPacket(ctx, metadata) if err != nil { return nil, err } return N.NewRefPacketConn(pc, t), nil } func (t *Client) forceClose() { t.clientImpl.forceClose(nil, types.ClientClosed) } func NewClient(clientOption *ClientOption, udp bool, dialFn types.DialFunc) *Client { ci := &clientImpl{ ClientOption: clientOption, dialFn: dialFn, udp: udp, } c := &Client{ci} runtime.SetFinalizer(c, closeClient) log.Debugln("New TuicV4 Client at %p", c) return c } func closeClient(client *Client) { log.Debugln("Close TuicV4 Client at %p", client) client.forceClose() } ================================================ FILE: core/Clash.Meta/transport/tuic/v4/packet.go ================================================ package v4 import ( "net" "sync" "time" "github.com/metacubex/mihomo/common/atomic" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/transport/tuic/types" "github.com/metacubex/quic-go" ) type quicStreamPacketConn struct { connId uint32 quicConn *quic.Conn inputConn *N.BufferedConn udpRelayMode types.UdpRelayMode maxUdpRelayPacketSize int deferQuicConnFn func(quicConn *quic.Conn, err error) closeDeferFn func() writeClosed *atomic.Bool closeOnce sync.Once closeErr error closed bool } func (q *quicStreamPacketConn) Close() error { q.closeOnce.Do(func() { q.closed = true q.closeErr = q.close() }) return q.closeErr } func (q *quicStreamPacketConn) close() (err error) { if q.closeDeferFn != nil { defer q.closeDeferFn() } if q.deferQuicConnFn != nil { defer func() { q.deferQuicConnFn(q.quicConn, err) }() } if q.inputConn != nil { _ = q.inputConn.Close() q.inputConn = nil buf := pool.GetBuffer() defer pool.PutBuffer(buf) err = NewDissociate(q.connId).WriteTo(buf) if err != nil { return } var stream *quic.SendStream stream, err = q.quicConn.OpenUniStream() if err != nil { return } _, err = buf.WriteTo(stream) if err != nil { return } err = stream.Close() if err != nil { return } } return } func (q *quicStreamPacketConn) SetDeadline(t time.Time) error { //TODO implement me return nil } func (q *quicStreamPacketConn) SetReadDeadline(t time.Time) error { if q.inputConn != nil { return q.inputConn.SetReadDeadline(t) } return nil } func (q *quicStreamPacketConn) SetWriteDeadline(t time.Time) error { //TODO implement me return nil } func (q *quicStreamPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { if q.inputConn != nil { var packet Packet packet, err = ReadPacket(q.inputConn) if err != nil { return } n = copy(p, packet.DATA) addr = packet.ADDR.UDPAddr() } else { err = net.ErrClosed } return } func (q *quicStreamPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { if q.inputConn != nil { var packet Packet packet, err = ReadPacket(q.inputConn) if err != nil { return } data = packet.DATA addr = packet.ADDR.UDPAddr() } else { err = net.ErrClosed } return } func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { if q.udpRelayMode != types.QUIC && len(p) > q.maxUdpRelayPacketSize { return 0, &quic.DatagramTooLargeError{MaxDatagramPayloadSize: int64(q.maxUdpRelayPacketSize)} } if q.closed { return 0, net.ErrClosed } if q.writeClosed != nil && q.writeClosed.Load() { _ = q.Close() return 0, net.ErrClosed } if q.deferQuicConnFn != nil { defer func() { q.deferQuicConnFn(q.quicConn, err) }() } buf := pool.GetBuffer() defer pool.PutBuffer(buf) address, err := NewAddressNetAddr(addr) if err != nil { return } err = NewPacket(q.connId, uint16(len(p)), address, p).WriteTo(buf) if err != nil { return } switch q.udpRelayMode { case types.QUIC: var stream *quic.SendStream stream, err = q.quicConn.OpenUniStream() if err != nil { return } defer stream.Close() _, err = buf.WriteTo(stream) if err != nil { return } default: // native data := buf.Bytes() err = q.quicConn.SendDatagram(data) if err != nil { return } } n = len(p) return } func (q *quicStreamPacketConn) LocalAddr() net.Addr { return q.quicConn.LocalAddr() } var _ net.PacketConn = (*quicStreamPacketConn)(nil) ================================================ FILE: core/Clash.Meta/transport/tuic/v4/protocol.go ================================================ package v4 import ( "encoding/binary" "fmt" "io" "net" "net/netip" "strconv" "github.com/metacubex/blake3" "github.com/metacubex/quic-go" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) type BufferedReader interface { io.Reader io.ByteReader } type BufferedWriter interface { io.Writer io.ByteWriter } type CommandType byte const ( AuthenticateType = CommandType(0x00) ConnectType = CommandType(0x01) PacketType = CommandType(0x02) DissociateType = CommandType(0x03) HeartbeatType = CommandType(0x04) ResponseType = CommandType(0xff) ) const VER byte = 0x04 func (c CommandType) String() string { switch c { case AuthenticateType: return "Authenticate" case ConnectType: return "Connect" case PacketType: return "Packet" case DissociateType: return "Dissociate" case HeartbeatType: return "Heartbeat" case ResponseType: return "Response" default: return fmt.Sprintf("UnknowCommand: %#x", byte(c)) } } func (c CommandType) BytesLen() int { return 1 } type CommandHead struct { VER byte TYPE CommandType } func NewCommandHead(TYPE CommandType) CommandHead { return CommandHead{ VER: VER, TYPE: TYPE, } } func ReadCommandHead(reader BufferedReader) (c CommandHead, err error) { c.VER, err = reader.ReadByte() if err != nil { return } TYPE, err := reader.ReadByte() if err != nil { return } c.TYPE = CommandType(TYPE) return } func (c CommandHead) WriteTo(writer BufferedWriter) (err error) { err = writer.WriteByte(c.VER) if err != nil { return } err = writer.WriteByte(byte(c.TYPE)) if err != nil { return } return } func (c CommandHead) BytesLen() int { return 1 + c.TYPE.BytesLen() } type Authenticate struct { CommandHead TKN [32]byte } func NewAuthenticate(TKN [32]byte) Authenticate { return Authenticate{ CommandHead: NewCommandHead(AuthenticateType), TKN: TKN, } } func ReadAuthenticateWithHead(head CommandHead, reader BufferedReader) (c Authenticate, err error) { c.CommandHead = head if c.CommandHead.TYPE != AuthenticateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } _, err = io.ReadFull(reader, c.TKN[:]) if err != nil { return } return } func ReadAuthenticate(reader BufferedReader) (c Authenticate, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadAuthenticateWithHead(head, reader) } func GenTKN(token string) [32]byte { return blake3.Sum256([]byte(token)) } func (c Authenticate) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } _, err = writer.Write(c.TKN[:]) if err != nil { return } return } func (c Authenticate) BytesLen() int { return c.CommandHead.BytesLen() + 32 } type Connect struct { CommandHead ADDR Address } func NewConnect(ADDR Address) Connect { return Connect{ CommandHead: NewCommandHead(ConnectType), ADDR: ADDR, } } func ReadConnectWithHead(head CommandHead, reader BufferedReader) (c Connect, err error) { c.CommandHead = head if c.CommandHead.TYPE != ConnectType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } c.ADDR, err = ReadAddress(reader) if err != nil { return } return } func ReadConnect(reader BufferedReader) (c Connect, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadConnectWithHead(head, reader) } func (c Connect) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } err = c.ADDR.WriteTo(writer) if err != nil { return } return } func (c Connect) BytesLen() int { return c.CommandHead.BytesLen() + c.ADDR.BytesLen() } type Packet struct { CommandHead ASSOC_ID uint32 LEN uint16 ADDR Address DATA []byte } func NewPacket(ASSOC_ID uint32, LEN uint16, ADDR Address, DATA []byte) Packet { return Packet{ CommandHead: NewCommandHead(PacketType), ASSOC_ID: ASSOC_ID, LEN: LEN, ADDR: ADDR, DATA: DATA, } } func ReadPacketWithHead(head CommandHead, reader BufferedReader) (c Packet, err error) { c.CommandHead = head if c.CommandHead.TYPE != PacketType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) if err != nil { return } err = binary.Read(reader, binary.BigEndian, &c.LEN) if err != nil { return } c.ADDR, err = ReadAddress(reader) if err != nil { return } c.DATA = make([]byte, c.LEN) _, err = io.ReadFull(reader, c.DATA) if err != nil { return } return } func ReadPacket(reader BufferedReader) (c Packet, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadPacketWithHead(head, reader) } func (c Packet) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.LEN) if err != nil { return } err = c.ADDR.WriteTo(writer) if err != nil { return } _, err = writer.Write(c.DATA) if err != nil { return } return } func (c Packet) BytesLen() int { return c.CommandHead.BytesLen() + 4 + 2 + c.ADDR.BytesLen() + len(c.DATA) } var PacketOverHead = NewPacket(0, 0, NewAddressAddrPort(netip.AddrPortFrom(netip.IPv6Unspecified(), 0)), nil).BytesLen() type Dissociate struct { CommandHead ASSOC_ID uint32 } func NewDissociate(ASSOC_ID uint32) Dissociate { return Dissociate{ CommandHead: NewCommandHead(DissociateType), ASSOC_ID: ASSOC_ID, } } func ReadDissociateWithHead(head CommandHead, reader BufferedReader) (c Dissociate, err error) { c.CommandHead = head if c.CommandHead.TYPE != DissociateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) if err != nil { return } return } func ReadDissociate(reader BufferedReader) (c Dissociate, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadDissociateWithHead(head, reader) } func (c Dissociate) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) if err != nil { return } return } func (c Dissociate) BytesLen() int { return c.CommandHead.BytesLen() + 4 } type Heartbeat struct { CommandHead } func NewHeartbeat() Heartbeat { return Heartbeat{ CommandHead: NewCommandHead(HeartbeatType), } } func ReadHeartbeatWithHead(head CommandHead, reader BufferedReader) (c Heartbeat, err error) { c.CommandHead = head if c.CommandHead.TYPE != HeartbeatType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } return } func ReadHeartbeat(reader BufferedReader) (c Heartbeat, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadHeartbeatWithHead(head, reader) } type Response struct { CommandHead REP byte } func NewResponse(REP byte) Response { return Response{ CommandHead: NewCommandHead(ResponseType), REP: REP, } } func NewResponseSucceed() Response { return NewResponse(0x00) } func NewResponseFailed() Response { return NewResponse(0xff) } func ReadResponseWithHead(head CommandHead, reader BufferedReader) (c Response, err error) { c.CommandHead = head if c.CommandHead.TYPE != ResponseType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } c.REP, err = reader.ReadByte() if err != nil { return } return } func ReadResponse(reader BufferedReader) (c Response, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadResponseWithHead(head, reader) } func (c Response) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } err = writer.WriteByte(c.REP) if err != nil { return } return } func (c Response) IsSucceed() bool { return c.REP == 0x00 } func (c Response) IsFailed() bool { return c.REP == 0xff } func (c Response) BytesLen() int { return c.CommandHead.BytesLen() + 1 } // Addr types const ( AtypDomainName byte = 0 AtypIPv4 byte = 1 AtypIPv6 byte = 2 ) type Address struct { TYPE byte ADDR []byte PORT uint16 } func NewAddress(metadata *C.Metadata) Address { var addrType byte var addr []byte switch metadata.AddrType() { case C.AtypIPv4: addrType = AtypIPv4 addr = metadata.DstIP.AsSlice() case C.AtypIPv6: addrType = AtypIPv6 addr = metadata.DstIP.AsSlice() case C.AtypDomainName: addrType = AtypDomainName addr = make([]byte, len(metadata.Host)+1) addr[0] = byte(len(metadata.Host)) copy(addr[1:], metadata.Host) } return Address{ TYPE: addrType, ADDR: addr, PORT: metadata.DstPort, } } func NewAddressNetAddr(addr net.Addr) (Address, error) { if addr, ok := addr.(interface{ AddrPort() netip.AddrPort }); ok { if addrPort := addr.AddrPort(); addrPort.IsValid() { // sing's M.Socksaddr maybe return an invalid AddrPort if it's a DomainName return NewAddressAddrPort(addrPort), nil } } addrStr := addr.String() if addrPort, err := netip.ParseAddrPort(addrStr); err == nil { return NewAddressAddrPort(addrPort), nil } metadata := &C.Metadata{} if err := metadata.SetRemoteAddress(addrStr); err != nil { return Address{}, err } return NewAddress(metadata), nil } func NewAddressAddrPort(addrPort netip.AddrPort) Address { var addrType byte port := addrPort.Port() addr := addrPort.Addr().Unmap() if addr.Is4() { addrType = AtypIPv4 } else { addrType = AtypIPv6 } return Address{ TYPE: addrType, ADDR: addr.AsSlice(), PORT: port, } } func ReadAddress(reader BufferedReader) (c Address, err error) { c.TYPE, err = reader.ReadByte() if err != nil { return } switch c.TYPE { case AtypIPv4: c.ADDR = make([]byte, net.IPv4len) _, err = io.ReadFull(reader, c.ADDR) if err != nil { return } case AtypIPv6: c.ADDR = make([]byte, net.IPv6len) _, err = io.ReadFull(reader, c.ADDR) if err != nil { return } case AtypDomainName: var addrLen byte addrLen, err = reader.ReadByte() if err != nil { return } c.ADDR = make([]byte, addrLen+1) c.ADDR[0] = addrLen _, err = io.ReadFull(reader, c.ADDR[1:]) if err != nil { return } } err = binary.Read(reader, binary.BigEndian, &c.PORT) if err != nil { return } return } func (c Address) WriteTo(writer BufferedWriter) (err error) { err = writer.WriteByte(c.TYPE) if err != nil { return } _, err = writer.Write(c.ADDR[:]) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.PORT) if err != nil { return } return } func (c Address) String() string { switch c.TYPE { case AtypDomainName: return net.JoinHostPort(string(c.ADDR[1:]), strconv.Itoa(int(c.PORT))) default: addr, _ := netip.AddrFromSlice(c.ADDR) addrPort := netip.AddrPortFrom(addr, c.PORT) return addrPort.String() } } func (c Address) SocksAddr() socks5.Addr { addr := make([]byte, 1+len(c.ADDR)+2) switch c.TYPE { case AtypIPv4: addr[0] = socks5.AtypIPv4 case AtypIPv6: addr[0] = socks5.AtypIPv6 case AtypDomainName: addr[0] = socks5.AtypDomainName } copy(addr[1:], c.ADDR) binary.BigEndian.PutUint16(addr[len(addr)-2:], c.PORT) return addr } func (c Address) UDPAddr() *net.UDPAddr { return &net.UDPAddr{ IP: c.ADDR, Port: int(c.PORT), Zone: "", } } func (c Address) BytesLen() int { return 1 + len(c.ADDR) + 2 } const ( ProtocolError = quic.ApplicationErrorCode(0xfffffff0) AuthenticationFailed = quic.ApplicationErrorCode(0xfffffff1) AuthenticationTimeout = quic.ApplicationErrorCode(0xfffffff2) BadCommand = quic.ApplicationErrorCode(0xfffffff3) ) ================================================ FILE: core/Clash.Meta/transport/tuic/v4/server.go ================================================ package v4 import ( "bufio" "bytes" "fmt" "net" "sync" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/atomic" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/xsync" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/tuic/types" "github.com/gofrs/uuid/v5" "github.com/metacubex/quic-go" ) type ServerOption struct { HandleTcpFn func(conn net.Conn, addr socks5.Addr, additions ...inbound.Addition) error HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket, additions ...inbound.Addition) error Tokens [][32]byte MaxUdpRelayPacketSize int } func NewServerHandler(option *ServerOption, quicConn *quic.Conn, uuid uuid.UUID) types.ServerHandler { return &serverHandler{ ServerOption: option, quicConn: quicConn, uuid: uuid, authCh: make(chan struct{}), } } type serverHandler struct { *ServerOption quicConn *quic.Conn uuid uuid.UUID authCh chan struct{} authOk atomic.Bool authOnce sync.Once udpInputMap xsync.Map[uint32, *atomic.Bool] } func (s *serverHandler) AuthOk() bool { return s.authOk.Load() } func (s *serverHandler) HandleTimeout() { s.authOnce.Do(func() { _ = s.quicConn.CloseWithError(AuthenticationTimeout, "AuthenticationTimeout") s.authOk.Store(false) close(s.authCh) }) } func (s *serverHandler) HandleMessage(message []byte) (err error) { buffer := bytes.NewBuffer(message) packet, err := ReadPacket(buffer) if err != nil { return } return s.parsePacket(&packet, types.NATIVE) } func (s *serverHandler) parsePacket(packet *Packet, udpRelayMode types.UdpRelayMode) (err error) { <-s.authCh if !s.authOk.Load() { return } var assocId uint32 assocId = packet.ASSOC_ID writeClosed, _ := s.udpInputMap.LoadOrStoreFn(assocId, func() *atomic.Bool { return &atomic.Bool{} }) if writeClosed.Load() { return nil } pc := &quicStreamPacketConn{ connId: assocId, quicConn: s.quicConn, inputConn: nil, udpRelayMode: udpRelayMode, maxUdpRelayPacketSize: s.MaxUdpRelayPacketSize, deferQuicConnFn: nil, closeDeferFn: nil, writeClosed: writeClosed, } return s.HandleUdpFn(packet.ADDR.SocksAddr(), &serverUDPPacket{ pc: pc, packet: packet, rAddr: N.NewCustomAddr("tuic", fmt.Sprintf("tuic-%s-%d", s.uuid, assocId), s.quicConn.RemoteAddr()), // for tunnel's handleUDPConn }) } func (s *serverHandler) HandleStream(conn *N.BufferedConn) (err error) { connect, err := ReadConnect(conn) if err != nil { return err } <-s.authCh if !s.authOk.Load() { return conn.Close() } buf := pool.GetBuffer() defer pool.PutBuffer(buf) err = s.HandleTcpFn(conn, connect.ADDR.SocksAddr()) if err != nil { err = NewResponseFailed().WriteTo(buf) defer conn.Close() } else { err = NewResponseSucceed().WriteTo(buf) } if err != nil { _ = conn.Close() return err } _, err = buf.WriteTo(conn) if err != nil { _ = conn.Close() return err } return } func (s *serverHandler) HandleUniStream(reader *bufio.Reader) (err error) { commandHead, err := ReadCommandHead(reader) if err != nil { return } switch commandHead.TYPE { case AuthenticateType: var authenticate Authenticate authenticate, err = ReadAuthenticateWithHead(commandHead, reader) if err != nil { return } authOk := false for _, tkn := range s.Tokens { if authenticate.TKN == tkn { authOk = true break } } s.authOnce.Do(func() { if !authOk { _ = s.quicConn.CloseWithError(AuthenticationFailed, "AuthenticationFailed") } s.authOk.Store(authOk) close(s.authCh) }) case PacketType: var packet Packet packet, err = ReadPacketWithHead(commandHead, reader) if err != nil { return } return s.parsePacket(&packet, types.QUIC) case DissociateType: var disassociate Dissociate disassociate, err = ReadDissociateWithHead(commandHead, reader) if err != nil { return } if writeClosed, loaded := s.udpInputMap.LoadAndDelete(disassociate.ASSOC_ID); loaded { writeClosed.Store(true) } case HeartbeatType: var heartbeat Heartbeat heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) if err != nil { return } heartbeat.BytesLen() } return } type serverUDPPacket struct { pc *quicStreamPacketConn packet *Packet rAddr net.Addr } func (s *serverUDPPacket) InAddr() net.Addr { return s.pc.LocalAddr() } func (s *serverUDPPacket) LocalAddr() net.Addr { return s.rAddr } func (s *serverUDPPacket) Data() []byte { return s.packet.DATA } func (s *serverUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { return s.pc.WriteTo(b, addr) } func (s *serverUDPPacket) Drop() { s.packet.DATA = nil } var _ C.UDPPacket = (*serverUDPPacket)(nil) var _ C.UDPPacketInAddr = (*serverUDPPacket)(nil) ================================================ FILE: core/Clash.Meta/transport/tuic/v5/client.go ================================================ package v5 import ( "bufio" "bytes" "context" "errors" "net" "runtime" "sync" "sync/atomic" "time" atomic2 "github.com/metacubex/mihomo/common/atomic" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/xsync" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/tuic/types" "github.com/metacubex/quic-go" "github.com/metacubex/randv2" ) type ClientOption struct { Uuid [16]byte Password string UdpRelayMode types.UdpRelayMode MaxUdpRelayPacketSize int MaxOpenStreams int64 } type clientImpl struct { *ClientOption dialFn types.DialFunc udp bool quicConn *quic.Conn connMutex sync.Mutex openStreams atomic.Int64 closed atomic.Bool udpInputMap xsync.Map[uint16, net.Conn] // only ready for PoolClient lastVisited atomic2.TypedValue[time.Time] } func (t *clientImpl) OpenStreams() int64 { return t.openStreams.Load() } func (t *clientImpl) LastVisited() time.Time { return t.lastVisited.Load() } func (t *clientImpl) SetLastVisited(last time.Time) { t.lastVisited.Store(last) } func (t *clientImpl) getQuicConn(ctx context.Context) (*quic.Conn, error) { t.connMutex.Lock() defer t.connMutex.Unlock() if t.quicConn != nil { return t.quicConn, nil } quicConn, err := t.dialFn(ctx) if err != nil { return nil, err } go func() { _ = t.sendAuthentication(quicConn) }() if t.udp && t.UdpRelayMode == types.QUIC { go func() { _ = t.handleUniStream(quicConn) }() } go func() { _ = t.handleMessage(quicConn) // always handleMessage because tuicV5 using datagram to send the Heartbeat }() t.quicConn = quicConn t.openStreams.Store(0) return quicConn, nil } func (t *clientImpl) sendAuthentication(quicConn *quic.Conn) (err error) { defer func() { t.deferQuicConn(quicConn, err) }() stream, err := quicConn.OpenUniStream() if err != nil { return err } buf := pool.GetBuffer() defer pool.PutBuffer(buf) token, err := GenToken(quicConn.ConnectionState(), t.Uuid, t.Password) if err != nil { return err } err = NewAuthenticate(t.Uuid, token).WriteTo(buf) if err != nil { return err } _, err = buf.WriteTo(stream) if err != nil { return err } err = stream.Close() if err != nil { return } return nil } func (t *clientImpl) handleUniStream(quicConn *quic.Conn) (err error) { defer func() { t.deferQuicConn(quicConn, err) }() for { var stream *quic.ReceiveStream stream, err = quicConn.AcceptUniStream(context.Background()) if err != nil { return err } go func() (err error) { var assocId uint16 defer func() { t.deferQuicConn(quicConn, err) if err != nil && assocId != 0 { if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { if conn, ok := val.(net.Conn); ok { _ = conn.Close() } } } stream.CancelRead(0) }() reader := bufio.NewReader(stream) commandHead, err := ReadCommandHead(reader) if err != nil { return } switch commandHead.TYPE { case PacketType: var packet Packet packet, err = ReadPacketWithHead(commandHead, reader) if err != nil { return } if t.udp && t.UdpRelayMode == types.QUIC { assocId = packet.ASSOC_ID if val, ok := t.udpInputMap.Load(assocId); ok { if conn, ok := val.(net.Conn); ok { writer := bufio.NewWriterSize(conn, packet.BytesLen()) _ = packet.WriteTo(writer) _ = writer.Flush() } } } } return }() } } func (t *clientImpl) handleMessage(quicConn *quic.Conn) (err error) { defer func() { t.deferQuicConn(quicConn, err) }() for { var message []byte message, err = quicConn.ReceiveDatagram(context.Background()) if err != nil { return err } go func() (err error) { var assocId uint16 defer func() { t.deferQuicConn(quicConn, err) if err != nil && assocId != 0 { if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { if conn, ok := val.(net.Conn); ok { _ = conn.Close() } } } }() reader := bytes.NewBuffer(message) commandHead, err := ReadCommandHead(reader) if err != nil { return } switch commandHead.TYPE { case PacketType: var packet Packet packet, err = ReadPacketWithHead(commandHead, reader) if err != nil { return } if t.udp && t.UdpRelayMode == types.NATIVE { assocId = packet.ASSOC_ID if val, ok := t.udpInputMap.Load(assocId); ok { if conn, ok := val.(net.Conn); ok { _, _ = conn.Write(message) } } } case HeartbeatType: var heartbeat Heartbeat heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) if err != nil { return } heartbeat.BytesLen() } return }() } } func (t *clientImpl) deferQuicConn(quicConn *quic.Conn, err error) { var netError net.Error if err != nil && errors.As(err, &netError) { t.forceClose(quicConn, err) } } func (t *clientImpl) forceClose(quicConn *quic.Conn, err error) { t.connMutex.Lock() defer t.connMutex.Unlock() if quicConn == nil { quicConn = t.quicConn } if quicConn != nil { if quicConn == t.quicConn { t.quicConn = nil } } errStr := "" if err != nil { errStr = err.Error() } if quicConn != nil { _ = quicConn.CloseWithError(ProtocolError, errStr) } udpInputMap := &t.udpInputMap udpInputMap.Range(func(key uint16, value net.Conn) bool { conn := value _ = conn.Close() udpInputMap.Delete(key) return true }) } func (t *clientImpl) Close() { t.closed.Store(true) if t.openStreams.Load() == 0 { t.forceClose(nil, types.ClientClosed) } } func (t *clientImpl) DialContext(ctx context.Context, metadata *C.Metadata) (net.Conn, error) { quicConn, err := t.getQuicConn(ctx) if err != nil { return nil, err } openStreams := t.openStreams.Add(1) if openStreams >= t.MaxOpenStreams { t.openStreams.Add(-1) return nil, types.TooManyOpenStreams } stream, err := func() (stream net.Conn, err error) { defer func() { t.deferQuicConn(quicConn, err) }() buf := pool.GetBuffer() defer pool.PutBuffer(buf) err = NewConnect(NewAddress(metadata)).WriteTo(buf) if err != nil { return nil, err } quicStream, err := quicConn.OpenStream() if err != nil { return nil, err } stream = types.NewQuicStreamConn( quicStream, quicConn.LocalAddr(), quicConn.RemoteAddr(), func() { time.AfterFunc(C.DefaultTCPTimeout, func() { openStreams := t.openStreams.Add(-1) if openStreams == 0 && t.closed.Load() { t.forceClose(quicConn, types.ClientClosed) } }) }, ) _, err = buf.WriteTo(stream) if err != nil { _ = stream.Close() return nil, err } return stream, err }() if err != nil { return nil, err } return stream, nil } func (t *clientImpl) ListenPacket(ctx context.Context, metadata *C.Metadata) (net.PacketConn, error) { quicConn, err := t.getQuicConn(ctx) if err != nil { return nil, err } openStreams := t.openStreams.Add(1) if openStreams >= t.MaxOpenStreams { t.openStreams.Add(-1) return nil, types.TooManyOpenStreams } pipe1, pipe2 := N.Pipe() var connId uint16 for { connId = uint16(randv2.IntN(0xFFFF)) _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) if !loaded { break } } pc := &quicStreamPacketConn{ connId: connId, quicConn: quicConn, inputConn: N.NewBufferedConn(pipe2), udpRelayMode: t.UdpRelayMode, maxUdpRelayPacketSize: t.MaxUdpRelayPacketSize, deferQuicConnFn: t.deferQuicConn, closeDeferFn: func() { t.udpInputMap.Delete(connId) time.AfterFunc(C.DefaultUDPTimeout, func() { openStreams := t.openStreams.Add(-1) if openStreams == 0 && t.closed.Load() { t.forceClose(quicConn, types.ClientClosed) } }) }, } return pc, nil } type Client struct { *clientImpl // use an independent pointer to let Finalizer can work no matter somewhere handle an influence in clientImpl inner } func (t *Client) DialContext(ctx context.Context, metadata *C.Metadata) (net.Conn, error) { conn, err := t.clientImpl.DialContext(ctx, metadata) if err != nil { return nil, err } return N.NewRefConn(conn, t), err } func (t *Client) ListenPacket(ctx context.Context, metadata *C.Metadata) (net.PacketConn, error) { pc, err := t.clientImpl.ListenPacket(ctx, metadata) if err != nil { return nil, err } return N.NewRefPacketConn(pc, t), nil } func (t *Client) forceClose() { t.clientImpl.forceClose(nil, types.ClientClosed) } func NewClient(clientOption *ClientOption, udp bool, dialFn types.DialFunc) *Client { ci := &clientImpl{ ClientOption: clientOption, dialFn: dialFn, udp: udp, } c := &Client{ci} runtime.SetFinalizer(c, closeClient) log.Debugln("New TuicV5 Client at %p", c) return c } func closeClient(client *Client) { log.Debugln("Close TuicV5 Client at %p", client) client.forceClose() } ================================================ FILE: core/Clash.Meta/transport/tuic/v5/frag.go ================================================ package v5 import ( "bytes" "sync" "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/quic-go" ) // MaxFragSize is a safe udp relay packet size // because tuicv5 support udp fragment so we unneeded to do a magic modify for quic-go to increase MaxDatagramFrameSize // it may not work fine in some platform // "1200" from quic-go's MaxDatagramSize // "-3" from quic-go's DatagramFrame.MaxDataLen var MaxFragSize = 1200 - PacketOverHead - 3 func fragWriteNative(quicConn *quic.Conn, packet Packet, buf *bytes.Buffer, fragSize int) (err error) { fullPayload := packet.DATA off := 0 fragID := uint8(0) fragCount := uint8((len(fullPayload) + fragSize - 1) / fragSize) // round up packet.FRAG_TOTAL = fragCount for off < len(fullPayload) { payloadSize := len(fullPayload) - off if payloadSize > fragSize { payloadSize = fragSize } frag := packet frag.FRAG_ID = fragID frag.SIZE = uint16(payloadSize) frag.DATA = fullPayload[off : off+payloadSize] off += payloadSize fragID++ buf.Reset() err = frag.WriteTo(buf) if err != nil { return } data := buf.Bytes() err = quicConn.SendDatagram(data) if err != nil { return } packet.ADDR.TYPE = AtypNone // avoid "fragment 2/2: address in non-first fragment" } return } type deFragger struct { lru *lru.LruCache[uint16, *packetBag] once sync.Once } type packetBag struct { frags []*Packet count uint8 mutex sync.Mutex } func newPacketBag() *packetBag { return new(packetBag) } func (d *deFragger) init() { if d.lru == nil { d.lru = lru.New( lru.WithAge[uint16, *packetBag](10), lru.WithUpdateAgeOnGet[uint16, *packetBag](), ) } } func (d *deFragger) Feed(m *Packet) *Packet { if m.FRAG_TOTAL <= 1 { return m } if m.FRAG_ID >= m.FRAG_TOTAL { // wtf is this? return nil } d.once.Do(d.init) // lazy init bag, _ := d.lru.GetOrStore(m.PKT_ID, newPacketBag) bag.mutex.Lock() defer bag.mutex.Unlock() if int(m.FRAG_TOTAL) != len(bag.frags) { // new message, clear previous state bag.frags = make([]*Packet, m.FRAG_TOTAL) bag.count = 1 bag.frags[m.FRAG_ID] = m return nil } if bag.frags[m.FRAG_ID] != nil { return nil } bag.frags[m.FRAG_ID] = m bag.count++ if int(bag.count) != len(bag.frags) { return nil } // all fragments received, assemble var data []byte for _, frag := range bag.frags { data = append(data, frag.DATA...) } p := *bag.frags[0] // recover from first fragment p.SIZE = uint16(len(data)) p.DATA = data p.FRAG_ID = 0 p.FRAG_TOTAL = 1 bag.frags = nil d.lru.Delete(m.PKT_ID) return &p } ================================================ FILE: core/Clash.Meta/transport/tuic/v5/packet.go ================================================ package v5 import ( "errors" "net" "sync" "time" "github.com/metacubex/mihomo/common/atomic" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/transport/tuic/types" "github.com/metacubex/quic-go" "github.com/metacubex/randv2" ) type quicStreamPacketConn struct { connId uint16 quicConn *quic.Conn inputConn *N.BufferedConn udpRelayMode types.UdpRelayMode maxUdpRelayPacketSize int deferQuicConnFn func(quicConn *quic.Conn, err error) closeDeferFn func() writeClosed *atomic.Bool closeOnce sync.Once closeErr error closed bool deFragger } func (q *quicStreamPacketConn) Close() error { q.closeOnce.Do(func() { q.closed = true q.closeErr = q.close() }) return q.closeErr } func (q *quicStreamPacketConn) close() (err error) { if q.closeDeferFn != nil { defer q.closeDeferFn() } if q.deferQuicConnFn != nil { defer func() { q.deferQuicConnFn(q.quicConn, err) }() } if q.inputConn != nil { _ = q.inputConn.Close() q.inputConn = nil buf := pool.GetBuffer() defer pool.PutBuffer(buf) err = NewDissociate(q.connId).WriteTo(buf) if err != nil { return } var stream *quic.SendStream stream, err = q.quicConn.OpenUniStream() if err != nil { return } _, err = buf.WriteTo(stream) if err != nil { return } err = stream.Close() if err != nil { return } } return } func (q *quicStreamPacketConn) SetDeadline(t time.Time) error { //TODO implement me return nil } func (q *quicStreamPacketConn) SetReadDeadline(t time.Time) error { if q.inputConn != nil { return q.inputConn.SetReadDeadline(t) } return nil } func (q *quicStreamPacketConn) SetWriteDeadline(t time.Time) error { //TODO implement me return nil } func (q *quicStreamPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { if inputConn := q.inputConn; inputConn != nil { // copy inputConn avoid be nil in for loop for { var packet Packet packet, err = ReadPacket(inputConn) if err != nil { return } if packetPtr := q.deFragger.Feed(&packet); packetPtr != nil { n = copy(p, packet.DATA) addr = packetPtr.ADDR.UDPAddr() return } } } else { err = net.ErrClosed } return } func (q *quicStreamPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { if inputConn := q.inputConn; inputConn != nil { // copy inputConn avoid be nil in for loop for { var packet Packet packet, err = ReadPacket(inputConn) if err != nil { return } if packetPtr := q.deFragger.Feed(&packet); packetPtr != nil { data = packetPtr.DATA addr = packetPtr.ADDR.UDPAddr() return } } } else { err = net.ErrClosed } return } func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { if len(p) > 0xffff { // uint16 max return 0, &quic.DatagramTooLargeError{MaxDatagramPayloadSize: 0xffff} } if q.closed { return 0, net.ErrClosed } if q.writeClosed != nil && q.writeClosed.Load() { _ = q.Close() return 0, net.ErrClosed } if q.deferQuicConnFn != nil { defer func() { q.deferQuicConnFn(q.quicConn, err) }() } buf := pool.GetBuffer() defer pool.PutBuffer(buf) address, err := NewAddressNetAddr(addr) if err != nil { return } pktId := uint16(randv2.Uint32()) packet := NewPacket(q.connId, pktId, 1, 0, uint16(len(p)), address, p) switch q.udpRelayMode { case types.QUIC: err = packet.WriteTo(buf) if err != nil { return } var stream *quic.SendStream stream, err = q.quicConn.OpenUniStream() if err != nil { return } defer stream.Close() _, err = buf.WriteTo(stream) if err != nil { return } default: // native if len(p) > q.maxUdpRelayPacketSize { err = fragWriteNative(q.quicConn, packet, buf, q.maxUdpRelayPacketSize) } else { err = packet.WriteTo(buf) if err != nil { return } data := buf.Bytes() err = q.quicConn.SendDatagram(data) } var tooLarge *quic.DatagramTooLargeError if errors.As(err, &tooLarge) { err = fragWriteNative(q.quicConn, packet, buf, int(tooLarge.MaxDatagramPayloadSize)-PacketOverHead) } if err != nil { return } } n = len(p) return } func (q *quicStreamPacketConn) LocalAddr() net.Addr { return q.quicConn.LocalAddr() } var _ net.PacketConn = (*quicStreamPacketConn)(nil) ================================================ FILE: core/Clash.Meta/transport/tuic/v5/protocol.go ================================================ package v5 import ( "encoding/binary" "fmt" "io" "net" "net/netip" "strconv" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/quic-go" ) type BufferedReader interface { io.Reader io.ByteReader } type BufferedWriter interface { io.Writer io.ByteWriter } type CommandType byte const ( AuthenticateType = CommandType(0x00) ConnectType = CommandType(0x01) PacketType = CommandType(0x02) DissociateType = CommandType(0x03) HeartbeatType = CommandType(0x04) ) const VER byte = 0x05 func (c CommandType) String() string { switch c { case AuthenticateType: return "Authenticate" case ConnectType: return "Connect" case PacketType: return "Packet" case DissociateType: return "Dissociate" case HeartbeatType: return "Heartbeat" default: return fmt.Sprintf("UnknowCommand: %#x", byte(c)) } } func (c CommandType) BytesLen() int { return 1 } type CommandHead struct { VER byte TYPE CommandType } func NewCommandHead(TYPE CommandType) CommandHead { return CommandHead{ VER: VER, TYPE: TYPE, } } func ReadCommandHead(reader BufferedReader) (c CommandHead, err error) { c.VER, err = reader.ReadByte() if err != nil { return } TYPE, err := reader.ReadByte() if err != nil { return } c.TYPE = CommandType(TYPE) return } func (c CommandHead) WriteTo(writer BufferedWriter) (err error) { err = writer.WriteByte(c.VER) if err != nil { return } err = writer.WriteByte(byte(c.TYPE)) if err != nil { return } return } func (c CommandHead) BytesLen() int { return 1 + c.TYPE.BytesLen() } type Authenticate struct { CommandHead UUID [16]byte TOKEN [32]byte } func NewAuthenticate(UUID [16]byte, TOKEN [32]byte) Authenticate { return Authenticate{ CommandHead: NewCommandHead(AuthenticateType), UUID: UUID, TOKEN: TOKEN, } } func ReadAuthenticateWithHead(head CommandHead, reader BufferedReader) (c Authenticate, err error) { c.CommandHead = head if c.CommandHead.TYPE != AuthenticateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } _, err = io.ReadFull(reader, c.UUID[:]) if err != nil { return } _, err = io.ReadFull(reader, c.TOKEN[:]) if err != nil { return } return } func ReadAuthenticate(reader BufferedReader) (c Authenticate, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadAuthenticateWithHead(head, reader) } func GenToken(state quic.ConnectionState, uuid [16]byte, password string) (token [32]byte, err error) { var tokenBytes []byte tokenBytes, err = state.TLS.ExportKeyingMaterial(utils.StringFromImmutableBytes(uuid[:]), utils.ImmutableBytesFromString(password), 32) if err != nil { return } copy(token[:], tokenBytes) return } func (c Authenticate) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } _, err = writer.Write(c.UUID[:]) if err != nil { return } _, err = writer.Write(c.TOKEN[:]) if err != nil { return } return } func (c Authenticate) BytesLen() int { return c.CommandHead.BytesLen() + 16 + 32 } type Connect struct { CommandHead ADDR Address } func NewConnect(ADDR Address) Connect { return Connect{ CommandHead: NewCommandHead(ConnectType), ADDR: ADDR, } } func ReadConnectWithHead(head CommandHead, reader BufferedReader) (c Connect, err error) { c.CommandHead = head if c.CommandHead.TYPE != ConnectType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } c.ADDR, err = ReadAddress(reader) if err != nil { return } return } func ReadConnect(reader BufferedReader) (c Connect, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadConnectWithHead(head, reader) } func (c Connect) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } err = c.ADDR.WriteTo(writer) if err != nil { return } return } func (c Connect) BytesLen() int { return c.CommandHead.BytesLen() + c.ADDR.BytesLen() } type Packet struct { CommandHead ASSOC_ID uint16 PKT_ID uint16 FRAG_TOTAL uint8 FRAG_ID uint8 SIZE uint16 ADDR Address DATA []byte } func NewPacket(ASSOC_ID uint16, PKT_ID uint16, FRGA_TOTAL uint8, FRAG_ID uint8, SIZE uint16, ADDR Address, DATA []byte) Packet { return Packet{ CommandHead: NewCommandHead(PacketType), ASSOC_ID: ASSOC_ID, PKT_ID: PKT_ID, FRAG_ID: FRAG_ID, FRAG_TOTAL: FRGA_TOTAL, SIZE: SIZE, ADDR: ADDR, DATA: DATA, } } func ReadPacketWithHead(head CommandHead, reader BufferedReader) (c Packet, err error) { c.CommandHead = head if c.CommandHead.TYPE != PacketType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) if err != nil { return } err = binary.Read(reader, binary.BigEndian, &c.PKT_ID) if err != nil { return } err = binary.Read(reader, binary.BigEndian, &c.FRAG_TOTAL) if err != nil { return } err = binary.Read(reader, binary.BigEndian, &c.FRAG_ID) if err != nil { return } err = binary.Read(reader, binary.BigEndian, &c.SIZE) if err != nil { return } c.ADDR, err = ReadAddress(reader) if err != nil { return } c.DATA = make([]byte, c.SIZE) _, err = io.ReadFull(reader, c.DATA) if err != nil { return } return } func ReadPacket(reader BufferedReader) (c Packet, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadPacketWithHead(head, reader) } func (c Packet) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.PKT_ID) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.FRAG_TOTAL) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.FRAG_ID) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.SIZE) if err != nil { return } err = c.ADDR.WriteTo(writer) if err != nil { return } _, err = writer.Write(c.DATA) if err != nil { return } return } func (c Packet) BytesLen() int { return c.CommandHead.BytesLen() + 4 + 2 + c.ADDR.BytesLen() + len(c.DATA) } var PacketOverHead = NewPacket(0, 0, 0, 0, 0, NewAddressAddrPort(netip.AddrPortFrom(netip.IPv6Unspecified(), 0)), nil).BytesLen() type Dissociate struct { CommandHead ASSOC_ID uint16 } func NewDissociate(ASSOC_ID uint16) Dissociate { return Dissociate{ CommandHead: NewCommandHead(DissociateType), ASSOC_ID: ASSOC_ID, } } func ReadDissociateWithHead(head CommandHead, reader BufferedReader) (c Dissociate, err error) { c.CommandHead = head if c.CommandHead.TYPE != DissociateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) if err != nil { return } return } func ReadDissociate(reader BufferedReader) (c Dissociate, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadDissociateWithHead(head, reader) } func (c Dissociate) WriteTo(writer BufferedWriter) (err error) { err = c.CommandHead.WriteTo(writer) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) if err != nil { return } return } func (c Dissociate) BytesLen() int { return c.CommandHead.BytesLen() + 4 } type Heartbeat struct { CommandHead } func NewHeartbeat() Heartbeat { return Heartbeat{ CommandHead: NewCommandHead(HeartbeatType), } } func ReadHeartbeatWithHead(head CommandHead, reader BufferedReader) (c Heartbeat, err error) { c.CommandHead = head if c.CommandHead.TYPE != HeartbeatType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return } return } func ReadHeartbeat(reader BufferedReader) (c Heartbeat, err error) { head, err := ReadCommandHead(reader) if err != nil { return } return ReadHeartbeatWithHead(head, reader) } // Addr types const ( AtypDomainName byte = 0 AtypIPv4 byte = 1 AtypIPv6 byte = 2 AtypNone byte = 255 // Address type None is used in Packet commands that is not the first fragment of a UDP packet. ) type Address struct { TYPE byte ADDR []byte PORT uint16 } func NewAddress(metadata *C.Metadata) Address { var addrType byte var addr []byte switch metadata.AddrType() { case C.AtypIPv4: addrType = AtypIPv4 addr = metadata.DstIP.AsSlice() case C.AtypIPv6: addrType = AtypIPv6 addr = metadata.DstIP.AsSlice() case C.AtypDomainName: addrType = AtypDomainName addr = make([]byte, len(metadata.Host)+1) addr[0] = byte(len(metadata.Host)) copy(addr[1:], metadata.Host) } return Address{ TYPE: addrType, ADDR: addr, PORT: metadata.DstPort, } } func NewAddressNetAddr(addr net.Addr) (Address, error) { if addr, ok := addr.(interface{ AddrPort() netip.AddrPort }); ok { if addrPort := addr.AddrPort(); addrPort.IsValid() { // sing's M.Socksaddr maybe return an invalid AddrPort if it's a DomainName return NewAddressAddrPort(addrPort), nil } } addrStr := addr.String() if addrPort, err := netip.ParseAddrPort(addrStr); err == nil { return NewAddressAddrPort(addrPort), nil } metadata := &C.Metadata{} if err := metadata.SetRemoteAddress(addrStr); err != nil { return Address{}, err } return NewAddress(metadata), nil } func NewAddressAddrPort(addrPort netip.AddrPort) Address { var addrType byte port := addrPort.Port() addr := addrPort.Addr().Unmap() if addr.Is4() { addrType = AtypIPv4 } else { addrType = AtypIPv6 } return Address{ TYPE: addrType, ADDR: addr.AsSlice(), PORT: port, } } func ReadAddress(reader BufferedReader) (c Address, err error) { c.TYPE, err = reader.ReadByte() if err != nil { return } switch c.TYPE { case AtypIPv4: c.ADDR = make([]byte, net.IPv4len) _, err = io.ReadFull(reader, c.ADDR) if err != nil { return } case AtypIPv6: c.ADDR = make([]byte, net.IPv6len) _, err = io.ReadFull(reader, c.ADDR) if err != nil { return } case AtypDomainName: var addrLen byte addrLen, err = reader.ReadByte() if err != nil { return } c.ADDR = make([]byte, addrLen+1) c.ADDR[0] = addrLen _, err = io.ReadFull(reader, c.ADDR[1:]) if err != nil { return } } if c.TYPE == AtypNone { return } err = binary.Read(reader, binary.BigEndian, &c.PORT) if err != nil { return } return } func (c Address) WriteTo(writer BufferedWriter) (err error) { err = writer.WriteByte(c.TYPE) if err != nil { return } if c.TYPE == AtypNone { return } _, err = writer.Write(c.ADDR[:]) if err != nil { return } err = binary.Write(writer, binary.BigEndian, c.PORT) if err != nil { return } return } func (c Address) String() string { switch c.TYPE { case AtypDomainName: return net.JoinHostPort(string(c.ADDR[1:]), strconv.Itoa(int(c.PORT))) default: addr, _ := netip.AddrFromSlice(c.ADDR) addrPort := netip.AddrPortFrom(addr, c.PORT) return addrPort.String() } } func (c Address) SocksAddr() socks5.Addr { addr := make([]byte, 1+len(c.ADDR)+2) switch c.TYPE { case AtypIPv4: addr[0] = socks5.AtypIPv4 case AtypIPv6: addr[0] = socks5.AtypIPv6 case AtypDomainName: addr[0] = socks5.AtypDomainName } copy(addr[1:], c.ADDR) binary.BigEndian.PutUint16(addr[len(addr)-2:], c.PORT) return addr } func (c Address) UDPAddr() *net.UDPAddr { return &net.UDPAddr{ IP: c.ADDR, Port: int(c.PORT), Zone: "", } } func (c Address) BytesLen() int { return 1 + len(c.ADDR) + 2 } const ( ProtocolError = quic.ApplicationErrorCode(0xfffffff0) AuthenticationFailed = quic.ApplicationErrorCode(0xfffffff1) AuthenticationTimeout = quic.ApplicationErrorCode(0xfffffff2) BadCommand = quic.ApplicationErrorCode(0xfffffff3) ) ================================================ FILE: core/Clash.Meta/transport/tuic/v5/server.go ================================================ package v5 import ( "bufio" "bytes" "fmt" "net" "sync" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/atomic" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/xsync" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/tuic/types" "github.com/gofrs/uuid/v5" "github.com/metacubex/quic-go" ) type ServerOption struct { HandleTcpFn func(conn net.Conn, addr socks5.Addr, additions ...inbound.Addition) error HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket, additions ...inbound.Addition) error Users map[[16]byte]string MaxUdpRelayPacketSize int } func NewServerHandler(option *ServerOption, quicConn *quic.Conn, uuid uuid.UUID) types.ServerHandler { return &serverHandler{ ServerOption: option, quicConn: quicConn, uuid: uuid, authCh: make(chan struct{}), } } type serverHandler struct { *ServerOption quicConn *quic.Conn uuid uuid.UUID authCh chan struct{} authOk atomic.Bool authUUID atomic.TypedValue[string] authOnce sync.Once udpInputMap xsync.Map[uint16, *serverUDPInput] } func (s *serverHandler) AuthOk() bool { return s.authOk.Load() } func (s *serverHandler) HandleTimeout() { s.authOnce.Do(func() { _ = s.quicConn.CloseWithError(AuthenticationTimeout, "AuthenticationTimeout") s.authOk.Store(false) close(s.authCh) }) } func (s *serverHandler) HandleMessage(message []byte) (err error) { reader := bytes.NewBuffer(message) commandHead, err := ReadCommandHead(reader) if err != nil { return } switch commandHead.TYPE { case PacketType: var packet Packet packet, err = ReadPacketWithHead(commandHead, reader) if err != nil { return } return s.parsePacket(&packet, types.NATIVE) case HeartbeatType: var heartbeat Heartbeat heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) if err != nil { return } heartbeat.BytesLen() } return } func (s *serverHandler) parsePacket(packet *Packet, udpRelayMode types.UdpRelayMode) (err error) { <-s.authCh if !s.authOk.Load() { return } var assocId uint16 assocId = packet.ASSOC_ID input, _ := s.udpInputMap.LoadOrStoreFn(assocId, func() *serverUDPInput { return &serverUDPInput{} }) if input.writeClosed.Load() { return nil } packetPtr := input.Feed(packet) if packetPtr == nil { return } pc := &quicStreamPacketConn{ connId: assocId, quicConn: s.quicConn, inputConn: nil, udpRelayMode: udpRelayMode, maxUdpRelayPacketSize: s.MaxUdpRelayPacketSize, deferQuicConnFn: nil, closeDeferFn: nil, writeClosed: &input.writeClosed, } return s.HandleUdpFn(packetPtr.ADDR.SocksAddr(), &serverUDPPacket{ pc: pc, packet: packetPtr, rAddr: N.NewCustomAddr("tuic", fmt.Sprintf("tuic-%s-%d", s.uuid, assocId), s.quicConn.RemoteAddr()), // for tunnel's handleUDPConn }, inbound.WithInUser(s.authUUID.Load())) } func (s *serverHandler) HandleStream(conn *N.BufferedConn) (err error) { connect, err := ReadConnect(conn) if err != nil { return err } <-s.authCh if !s.authOk.Load() { return conn.Close() } err = s.HandleTcpFn(conn, connect.ADDR.SocksAddr(), inbound.WithInUser(s.authUUID.Load())) if err != nil { _ = conn.Close() return err } return } func (s *serverHandler) HandleUniStream(reader *bufio.Reader) (err error) { commandHead, err := ReadCommandHead(reader) if err != nil { return } switch commandHead.TYPE { case AuthenticateType: var authenticate Authenticate authenticate, err = ReadAuthenticateWithHead(commandHead, reader) if err != nil { return } authOk := false var authUUID uuid.UUID var token [32]byte if password, ok := s.Users[authenticate.UUID]; ok { token, err = GenToken(s.quicConn.ConnectionState(), authenticate.UUID, password) if err != nil { return } if token == authenticate.TOKEN { authOk = true authUUID = authenticate.UUID } } s.authOnce.Do(func() { if !authOk { _ = s.quicConn.CloseWithError(AuthenticationFailed, "AuthenticationFailed") } s.authOk.Store(authOk) s.authUUID.Store(authUUID.String()) close(s.authCh) }) case PacketType: var packet Packet packet, err = ReadPacketWithHead(commandHead, reader) if err != nil { return } return s.parsePacket(&packet, types.QUIC) case DissociateType: var disassociate Dissociate disassociate, err = ReadDissociateWithHead(commandHead, reader) if err != nil { return } if input, loaded := s.udpInputMap.LoadAndDelete(disassociate.ASSOC_ID); loaded { input.writeClosed.Store(true) } } return } type serverUDPInput struct { writeClosed atomic.Bool deFragger } type serverUDPPacket struct { pc *quicStreamPacketConn packet *Packet rAddr net.Addr } func (s *serverUDPPacket) InAddr() net.Addr { return s.pc.LocalAddr() } func (s *serverUDPPacket) LocalAddr() net.Addr { return s.rAddr } func (s *serverUDPPacket) Data() []byte { return s.packet.DATA } func (s *serverUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { return s.pc.WriteTo(b, addr) } func (s *serverUDPPacket) Drop() { s.packet.DATA = nil } var _ C.UDPPacket = (*serverUDPPacket)(nil) var _ C.UDPPacketInAddr = (*serverUDPPacket)(nil) ================================================ FILE: core/Clash.Meta/transport/v2ray-plugin/mux.go ================================================ package obfs import ( "bytes" "encoding/binary" "errors" "io" "net" "time" ) type SessionStatus = byte const ( SessionStatusNew SessionStatus = 0x01 SessionStatusKeep SessionStatus = 0x02 SessionStatusEnd SessionStatus = 0x03 SessionStatusKeepAlive SessionStatus = 0x04 ) const ( OptionNone = byte(0x00) OptionData = byte(0x01) OptionError = byte(0x02) ) type MuxOption struct { ID [2]byte Port uint16 Host string Type string } // Mux is an mux-compatible client for v2ray-plugin, not a complete implementation type Mux struct { net.Conn buf bytes.Buffer id [2]byte length [2]byte status [2]byte remain int } func (m *Mux) Read(b []byte) (int, error) { if m.remain != 0 { length := m.remain if len(b) < m.remain { length = len(b) } n, err := m.Conn.Read(b[:length]) if err != nil { return 0, err } m.remain -= n return n, nil } for { _, err := io.ReadFull(m.Conn, m.length[:]) if err != nil { return 0, err } length := binary.BigEndian.Uint16(m.length[:]) if length > 512 { return 0, errors.New("invalid metalen") } _, err = io.ReadFull(m.Conn, m.id[:]) if err != nil { return 0, err } _, err = m.Conn.Read(m.status[:]) if err != nil { return 0, err } opcode := m.status[0] if opcode == SessionStatusKeepAlive { continue } opts := m.status[1] if opts != OptionData { continue } _, err = io.ReadFull(m.Conn, m.length[:]) if err != nil { return 0, err } dataLen := int(binary.BigEndian.Uint16(m.length[:])) m.remain = dataLen if dataLen > len(b) { dataLen = len(b) } n, err := m.Conn.Read(b[:dataLen]) m.remain -= n return n, err } } func (m *Mux) Write(b []byte) (int, error) { defer m.buf.Reset() // reset must after write (keep the data fill in NewMux can be sent) binary.Write(&m.buf, binary.BigEndian, uint16(4)) m.buf.Write(m.id[:]) m.buf.WriteByte(SessionStatusKeep) m.buf.WriteByte(OptionData) binary.Write(&m.buf, binary.BigEndian, uint16(len(b))) m.buf.Write(b) return m.Conn.Write(m.buf.Bytes()) } func (m *Mux) Close() error { errChan := make(chan error, 1) t := time.AfterFunc(time.Second, func() { // maybe conn write too slowly, force close underlay conn after one second errChan <- m.Conn.Close() }) _, _ = m.Conn.Write([]byte{0x0, 0x4, m.id[0], m.id[1], SessionStatusEnd, OptionNone}) // ignore session end frame write error if !t.Stop() { // Stop does not wait for f to complete before returning, so we used a chan to know whether f is completed return <-errChan } return m.Conn.Close() } func NewMux(conn net.Conn, option MuxOption) *Mux { mux := &Mux{ Conn: conn, id: option.ID, } // create a sub connection (in buf) buf := &mux.buf // fill empty length buf.Write([]byte{0x0, 0x0}) buf.Write(option.ID[:]) buf.WriteByte(SessionStatusNew) buf.WriteByte(OptionNone) // tcp netType := byte(0x1) if option.Type == "udp" { netType = byte(0x2) } buf.WriteByte(netType) // port binary.Write(buf, binary.BigEndian, option.Port) // address ip := net.ParseIP(option.Host) if ip == nil { buf.WriteByte(0x2) buf.WriteString(option.Host) } else if ipv4 := ip.To4(); ipv4 != nil { buf.WriteByte(0x1) buf.Write(ipv4) } else { buf.WriteByte(0x3) buf.Write(ip.To16()) } metadata := buf.Bytes() binary.BigEndian.PutUint16(metadata[:2], uint16(len(metadata)-2)) return mux } ================================================ FILE: core/Clash.Meta/transport/v2ray-plugin/websocket.go ================================================ package obfs import ( "context" "net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/http" "github.com/metacubex/tls" ) // Option is options of websocket obfs type Option struct { Host string Port string Path string Headers map[string]string TLS bool ECHConfig *ech.Config SkipCertVerify bool Fingerprint string Certificate string PrivateKey string Mux bool V2rayHttpUpgrade bool V2rayHttpUpgradeFastOpen bool } // NewV2rayObfs return a HTTPObfs func NewV2rayObfs(ctx context.Context, conn net.Conn, option *Option) (net.Conn, error) { header := http.Header{} for k, v := range option.Headers { header.Add(k, v) } config := &vmess.WebsocketConfig{ Host: option.Host, Port: option.Port, Path: option.Path, V2rayHttpUpgrade: option.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: option.V2rayHttpUpgradeFastOpen, ECHConfig: option.ECHConfig, Headers: header, } var err error if option.TLS { config.TLS = true config.TLSConfig, err = ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: option.Host, InsecureSkipVerify: option.SkipCertVerify, NextProtos: []string{"http/1.1"}, }, Fingerprint: option.Fingerprint, Certificate: option.Certificate, PrivateKey: option.PrivateKey, }) if err != nil { return nil, err } if host := config.Headers.Get("Host"); host != "" { config.TLSConfig.ServerName = host } } conn, err = vmess.StreamWebsocketConn(ctx, conn, config) if err != nil { return nil, err } if option.Mux { conn = NewMux(conn, MuxOption{ ID: [2]byte{0, 0}, Host: "127.0.0.1", Port: 0, }) } return conn, nil } ================================================ FILE: core/Clash.Meta/transport/vless/addons.go ================================================ package vless import ( "bytes" "encoding/binary" "fmt" "io" ) func ReadAddons(data []byte) (*Addons, error) { reader := bytes.NewReader(data) var addons Addons for reader.Len() > 0 { tag, err := binary.ReadUvarint(reader) if err != nil { return nil, err } number, typ := int32(tag>>3), int8(tag&7) switch typ { case 0: // VARINT _, err = binary.ReadUvarint(reader) if err != nil { return nil, err } case 5: // I32 var i32 [4]byte _, err = io.ReadFull(reader, i32[:]) if err != nil { return nil, err } case 1: // I64 var i64 [8]byte _, err = io.ReadFull(reader, i64[:]) if err != nil { return nil, err } case 2: // LEN var bytesLen uint64 bytesLen, err = binary.ReadUvarint(reader) if err != nil { return nil, err } bytesData := make([]byte, bytesLen) _, err = io.ReadFull(reader, bytesData) if err != nil { return nil, err } switch number { case 1: addons.Flow = string(bytesData) case 2: addons.Seed = bytesData } default: // group (3,4) has been deprecated we unneeded support return nil, fmt.Errorf("unknown protobuf message tag: %v", tag) } } return &addons, nil } func WriteAddons(addons *Addons) []byte { var writer bytes.Buffer if len(addons.Flow) > 0 { WriteUvarint(&writer, (1<<3)|2) // (field << 3) bit-or wire_type encoded as uint32 varint WriteUvarint(&writer, uint64(len(addons.Flow))) writer.WriteString(addons.Flow) } if len(addons.Seed) > 0 { WriteUvarint(&writer, (2<<3)|2) // (field << 3) bit-or wire_type encoded as uint32 varint WriteUvarint(&writer, uint64(len(addons.Seed))) writer.Write(addons.Seed) } return writer.Bytes() } func WriteUvarint(writer *bytes.Buffer, x uint64) { for x >= 0x80 { writer.WriteByte(byte(x) | 0x80) x >>= 7 } writer.WriteByte(byte(x)) } ================================================ FILE: core/Clash.Meta/transport/vless/addons_test.go ================================================ package vless import ( "bytes" "strconv" "testing" "google.golang.org/protobuf/proto" ) func TestAddons(t *testing.T) { var tests = []struct { flow string seed []byte }{ {XRV, nil}, {XRS, []byte{1, 2, 3}}, {"", []byte{1, 2, 3}}, {"", nil}, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { t.Run("proto->handwritten", func(t *testing.T) { addons := new(Addons) addons.Flow = test.flow addons.Seed = test.seed addonsBytes, err := proto.Marshal(addons) if err != nil { t.Errorf("error marshalling addons: %v", err) return } addons, err = ReadAddons(addonsBytes) if err != nil { t.Errorf("error reading addons: %v", err) return } if addons.Flow != test.flow { t.Errorf("got %v; want %v", addons.Flow, test.flow) return } if !bytes.Equal(addons.Seed, test.seed) { t.Errorf("got %v; want %v", addons.Seed, test.seed) return } }) t.Run("handwritten->proto", func(t *testing.T) { addons := new(Addons) addons.Flow = test.flow addons.Seed = test.seed addonsBytes := WriteAddons(addons) err := proto.Unmarshal(addonsBytes, addons) if err != nil { t.Errorf("error reading addons: %v", err) return } if addons.Flow != test.flow { t.Errorf("got %v; want %v", addons.Flow, test.flow) return } if !bytes.Equal(addons.Seed, test.seed) { t.Errorf("got %v; want %v", addons.Seed, test.seed) return } }) t.Run("handwritten->handwritten", func(t *testing.T) { addons := new(Addons) addons.Flow = test.flow addons.Seed = test.seed addonsBytes := WriteAddons(addons) addons, err := ReadAddons(addonsBytes) if err != nil { t.Errorf("error reading addons: %v", err) return } if addons.Flow != test.flow { t.Errorf("got %v; want %v", addons.Flow, test.flow) return } if !bytes.Equal(addons.Seed, test.seed) { t.Errorf("got %v; want %v", addons.Seed, test.seed) return } }) }) } } ================================================ FILE: core/Clash.Meta/transport/vless/config.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.0 // protoc v3.19.1 // source: transport/vless/config.proto package vless import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) 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 Addons struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Flow string `protobuf:"bytes,1,opt,name=Flow,proto3" json:"Flow,omitempty"` Seed []byte `protobuf:"bytes,2,opt,name=Seed,proto3" json:"Seed,omitempty"` } func (x *Addons) Reset() { *x = Addons{} if protoimpl.UnsafeEnabled { mi := &file_transport_vless_config_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Addons) String() string { return protoimpl.X.MessageStringOf(x) } func (*Addons) ProtoMessage() {} func (x *Addons) ProtoReflect() protoreflect.Message { mi := &file_transport_vless_config_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Addons.ProtoReflect.Descriptor instead. func (*Addons) Descriptor() ([]byte, []int) { return file_transport_vless_config_proto_rawDescGZIP(), []int{0} } func (x *Addons) GetFlow() string { if x != nil { return x.Flow } return "" } func (x *Addons) GetSeed() []byte { if x != nil { return x.Seed } return nil } var File_transport_vless_config_proto protoreflect.FileDescriptor var file_transport_vless_config_proto_rawDesc = []byte{ 0x0a, 0x1c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x30, 0x0a, 0x06, 0x41, 0x64, 0x64, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x46, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x53, 0x65, 0x65, 0x64, 0x42, 0x61, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x61, 0x63, 0x72, 0x6f, 0x2f, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, 0x15, 0x43, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_transport_vless_config_proto_rawDescOnce sync.Once file_transport_vless_config_proto_rawDescData = file_transport_vless_config_proto_rawDesc ) func file_transport_vless_config_proto_rawDescGZIP() []byte { file_transport_vless_config_proto_rawDescOnce.Do(func() { file_transport_vless_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_vless_config_proto_rawDescData) }) return file_transport_vless_config_proto_rawDescData } var file_transport_vless_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_transport_vless_config_proto_goTypes = []interface{}{ (*Addons)(nil), // 0: mihomo.transport.vless.Addons } var file_transport_vless_config_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] 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_vless_config_proto_init() } func file_transport_vless_config_proto_init() { if File_transport_vless_config_proto != nil { return } if !protoimpl.UnsafeEnabled { file_transport_vless_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Addons); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_transport_vless_config_proto_rawDesc, NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_transport_vless_config_proto_goTypes, DependencyIndexes: file_transport_vless_config_proto_depIdxs, MessageInfos: file_transport_vless_config_proto_msgTypes, }.Build() File_transport_vless_config_proto = out.File file_transport_vless_config_proto_rawDesc = nil file_transport_vless_config_proto_goTypes = nil file_transport_vless_config_proto_depIdxs = nil } ================================================ FILE: core/Clash.Meta/transport/vless/config.proto ================================================ syntax = "proto3"; package mihomo.transport.vless; option csharp_namespace = "Mihomo.Transport.Vless"; option go_package = "github.com/metacubex/mihomo/transport/vless"; option java_package = "com.mihomo.transport.vless"; option java_multiple_files = true; message Addons { string Flow = 1; bytes Seed = 2; } ================================================ FILE: core/Clash.Meta/transport/vless/conn.go ================================================ package vless import ( "encoding/binary" "errors" "io" "net" "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/transport/vless/vision" "github.com/gofrs/uuid/v5" "google.golang.org/protobuf/proto" ) type Conn struct { N.ExtendedConn dst *DstAddr id uuid.UUID addons *Addons received bool sent bool } func (vc *Conn) Read(b []byte) (int, error) { if !vc.received { if err := vc.recvResponse(); err != nil { return 0, err } vc.received = true } return vc.ExtendedConn.Read(b) } func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { if !vc.received { if err := vc.recvResponse(); err != nil { return err } vc.received = true } return vc.ExtendedConn.ReadBuffer(buffer) } func (vc *Conn) Write(p []byte) (int, error) { if !vc.sent { if err := vc.sendRequest(p); err != nil { return 0, err } vc.sent = true return len(p), nil } return vc.ExtendedConn.Write(p) } func (vc *Conn) WriteBuffer(buffer *buf.Buffer) error { if !vc.sent { if err := vc.sendRequest(buffer.Bytes()); err != nil { return err } vc.sent = true return nil } return vc.ExtendedConn.WriteBuffer(buffer) } func (vc *Conn) sendRequest(p []byte) (err error) { var addonsBytes []byte if vc.addons != nil { addonsBytes, err = proto.Marshal(vc.addons) if err != nil { return } } requestLen := 1 // protocol version requestLen += 16 // UUID requestLen += 1 // addons length requestLen += len(addonsBytes) requestLen += 1 // command if !vc.dst.Mux { requestLen += 2 // port requestLen += 1 // addr type requestLen += len(vc.dst.Addr) } requestLen += len(p) buffer := buf.NewSize(requestLen) defer buffer.Release() buf.Must( buffer.WriteByte(Version), // protocol version buf.Error(buffer.Write(vc.id.Bytes())), // 16 bytes of uuid buffer.WriteByte(byte(len(addonsBytes))), buf.Error(buffer.Write(addonsBytes)), ) if vc.dst.Mux { buf.Must(buffer.WriteByte(CommandMux)) } else { if vc.dst.UDP { buf.Must(buffer.WriteByte(CommandUDP)) } else { buf.Must(buffer.WriteByte(CommandTCP)) } binary.BigEndian.PutUint16(buffer.Extend(2), vc.dst.Port) buf.Must( buffer.WriteByte(vc.dst.AddrType), buf.Error(buffer.Write(vc.dst.Addr)), ) } buf.Must(buf.Error(buffer.Write(p))) _, err = vc.ExtendedConn.Write(buffer.Bytes()) return } func (vc *Conn) recvResponse() (err error) { var buffer [2]byte _, err = io.ReadFull(vc.ExtendedConn, buffer[:]) if err != nil { return err } if buffer[0] != Version { return errors.New("unexpected response version") } length := int64(buffer[1]) if length != 0 { // addon data length > 0 io.CopyN(io.Discard, vc.ExtendedConn, length) // just discard } return } func (vc *Conn) Upstream() any { return vc.ExtendedConn } func (vc *Conn) ReaderReplaceable() bool { return vc.received } func (vc *Conn) WriterReplaceable() bool { return vc.sent } func (vc *Conn) NeedHandshake() bool { return !vc.sent } // newConn return a Conn instance func newConn(conn net.Conn, client *Client, dst *DstAddr) (net.Conn, error) { c := &Conn{ ExtendedConn: N.NewExtendedConn(conn), id: client.uuid, addons: client.Addons, dst: dst, } if client.Addons != nil { switch client.Addons.Flow { case XRV: visionConn, err := vision.NewConn(c, conn, c.id) if err != nil { return nil, err } return visionConn, nil } } return c, nil } ================================================ FILE: core/Clash.Meta/transport/vless/encryption/client.go ================================================ package encryption import ( "crypto/cipher" "crypto/ecdh" "crypto/rand" "errors" "io" "net" "runtime" "sync" "time" "github.com/metacubex/blake3" "github.com/metacubex/cpu" "github.com/metacubex/mlkem" ) var ( // Keep in sync with crypto/internal/fips140/aes/gcm.supportsAESGCM. hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3 hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCTR && cpu.S390X.HasGHASH hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" HasAESGCMHardwareSupport = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64 ) type ClientInstance struct { NfsPKeys []any NfsPKeysBytes [][]byte Hash32s [][32]byte RelaysLength int XorMode uint32 Seconds uint32 PaddingLens [][3]int PaddingGaps [][3]int RWLock sync.RWMutex Expire time.Time PfsKey []byte Ticket []byte } func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) { if i.NfsPKeys != nil { return errors.New("already initialized") } l := len(nfsPKeysBytes) if l == 0 { return errors.New("empty nfsPKeysBytes") } i.NfsPKeys = make([]any, l) i.NfsPKeysBytes = nfsPKeysBytes i.Hash32s = make([][32]byte, l) for j, k := range nfsPKeysBytes { if len(k) == 32 { if i.NfsPKeys[j], err = ecdh.X25519().NewPublicKey(k); err != nil { return } i.RelaysLength += 32 + 32 } else { if i.NfsPKeys[j], err = mlkem.NewEncapsulationKey768(k); err != nil { return } i.RelaysLength += 1088 + 32 } i.Hash32s[j] = blake3.Sum256(k) } i.RelaysLength -= 32 i.XorMode = xorMode i.Seconds = seconds return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) } func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { if i.NfsPKeys == nil { return nil, errors.New("uninitialized") } c := NewCommonConn(conn, HasAESGCMHardwareSupport) ivAndRealysLength := 16 + i.RelaysLength pfsKeyExchangeLength := 18 + 1184 + 32 + 16 paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps) clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength) iv := clientHello[:16] rand.Read(iv) relays := clientHello[16:ivAndRealysLength] var nfsKey []byte var lastCTR cipher.Stream for j, k := range i.NfsPKeys { var index = 32 if k, ok := k.(*ecdh.PublicKey); ok { privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader) copy(relays, privateKey.PublicKey().Bytes()) var err error nfsKey, err = privateKey.ECDH(k) if err != nil { return nil, err } } if k, ok := k.(*mlkem.EncapsulationKey768); ok { var ciphertext []byte nfsKey, ciphertext = k.Encapsulate() copy(relays, ciphertext) index = 1088 } if i.XorMode > 0 { // this xor can (others can't) be recovered by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, that's why "native" values NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // make X25519 public key / ML-KEM-768 ciphertext distinguishable from random bytes } if lastCTR != nil { lastCTR.XORKeyStream(relays, relays[:32]) // make this relay irreplaceable } if j == len(i.NfsPKeys)-1 { break } lastCTR = NewCTR(nfsKey, iv) lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:]) relays = relays[index+32:] } nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES) if i.Seconds > 0 { i.RWLock.RLock() if time.Now().Before(i.Expire) { c.Client = i c.UnitedKey = append(i.PfsKey, nfsKey...) // different unitedKey for each connection nfsAEAD.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil) nfsAEAD.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil) i.RWLock.RUnlock() c.PreWrite = clientHello[:ivAndRealysLength+18+32] c.AEAD = NewAEAD(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey, c.UseAES) if i.XorMode == 2 { c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16) } return c, nil } i.RWLock.RUnlock() } pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength] nfsAEAD.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil) mlkem768DKey, _ := mlkem.GenerateKey768() x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...) nfsAEAD.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil) padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:] nfsAEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) nfsAEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) paddingLens[0] = ivAndRealysLength + pfsKeyExchangeLength + paddingLens[0] for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control if l > 0 { if _, err := conn.Write(clientHello[:l]); err != nil { return nil, err } clientHello = clientHello[l:] } if len(paddingGaps) > i { time.Sleep(paddingGaps[i]) } } encryptedPfsPublicKey := make([]byte, 1088+32+16) if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { return nil, err } nfsAEAD.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil) mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088]) if err != nil { return nil, err } peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32]) if err != nil { return nil, err } x25519Key, err := x25519SKey.ECDH(peerX25519PKey) if err != nil { return nil, err } pfsKey := make([]byte, 32+32) // no more capacity copy(pfsKey, mlkem768Key) copy(pfsKey[32:], x25519Key) c.UnitedKey = append(pfsKey, nfsKey...) c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES) c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1088+32], c.UnitedKey, c.UseAES) encryptedTicket := make([]byte, 32) if _, err := io.ReadFull(conn, encryptedTicket); err != nil { return nil, err } if _, err := c.PeerAEAD.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil { return nil, err } seconds := DecodeLength(encryptedTicket) if i.Seconds > 0 && seconds > 0 { i.RWLock.Lock() i.Expire = time.Now().Add(time.Duration(seconds) * time.Second) i.PfsKey = pfsKey i.Ticket = encryptedTicket[:16] i.RWLock.Unlock() } encryptedLength := make([]byte, 18) if _, err := io.ReadFull(conn, encryptedLength); err != nil { return nil, err } if _, err := c.PeerAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { return nil, err } length := DecodeLength(encryptedLength[:2]) c.PeerPadding = make([]byte, length) // important: allows server sends padding slowly, eliminating 1-RTT's traffic pattern if i.XorMode == 2 { c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, length) } return c, nil } ================================================ FILE: core/Clash.Meta/transport/vless/encryption/client_test.go ================================================ package encryption import ( "fmt" "runtime" "testing" ) func TestHasAESGCMHardwareSupport(t *testing.T) { fmt.Println("HasAESGCMHardwareSupport:", HasAESGCMHardwareSupport) if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" { // It should be supported starting from Apple Silicon M1 // https://github.com/golang/go/blob/go1.25.0/src/internal/cpu/cpu_arm64_darwin.go#L26-L30 if !HasAESGCMHardwareSupport { t.Errorf("For ARM64 Darwin platforms (excluding iOS), AES GCM hardware acceleration should always be available.") } } } ================================================ FILE: core/Clash.Meta/transport/vless/encryption/common.go ================================================ package encryption import ( "bytes" "crypto/aes" "crypto/cipher" "errors" "fmt" "io" "net" "strconv" "strings" "time" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/blake3" "github.com/metacubex/randv2" "golang.org/x/crypto/chacha20poly1305" ) type CommonConn struct { net.Conn UseAES bool Client *ClientInstance UnitedKey []byte PreWrite []byte AEAD *AEAD PeerAEAD *AEAD PeerPadding []byte rawInput bytes.Buffer // PeerInBytes input bytes.Reader // PeerCache } func NewCommonConn(conn net.Conn, useAES bool) *CommonConn { return &CommonConn{ Conn: conn, UseAES: useAES, } } func (c *CommonConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } outBytes := pool.Get(5 + 8192 + 16) defer pool.Put(outBytes) for n := 0; n < len(b); { b := b[n:] if len(b) > 8192 { b = b[:8192] // for avoiding another copy() in peer's Read() } n += len(b) headerAndData := outBytes[:5+len(b)+16] EncodeHeader(headerAndData, len(b)+16) max := false if bytes.Equal(c.AEAD.Nonce[:], MaxNonce) { max = true } c.AEAD.Seal(headerAndData[:5], nil, b, headerAndData[:5]) if max { c.AEAD = NewAEAD(headerAndData, c.UnitedKey, c.UseAES) } if c.PreWrite != nil { headerAndData = append(c.PreWrite, headerAndData...) c.PreWrite = nil } if _, err := c.Conn.Write(headerAndData); err != nil { return 0, err } } return len(b), nil } func (c *CommonConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } if c.PeerAEAD == nil { // client's 0-RTT serverRandom := make([]byte, 16) if _, err := io.ReadFull(c.Conn, serverRandom); err != nil { return 0, err } c.PeerAEAD = NewAEAD(serverRandom, c.UnitedKey, c.UseAES) if xorConn, ok := c.Conn.(*XorConn); ok { xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom) } } if c.PeerPadding != nil { // client's 1-RTT if _, err := io.ReadFull(c.Conn, c.PeerPadding); err != nil { return 0, err } if _, err := c.PeerAEAD.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil { return 0, err } c.PeerPadding = nil } if c.input.Len() > 0 { return c.input.Read(b) } peerHeader := [5]byte{} if _, err := io.ReadFull(c.Conn, peerHeader[:]); err != nil { return 0, err } l, err := DecodeHeader(peerHeader[:]) // l: 17~17000 if err != nil { if c.Client != nil && errors.Is(err, ErrInvalidHeader) { // client's 0-RTT c.Client.RWLock.Lock() if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) { c.Client.Expire = time.Now() // expired } c.Client.RWLock.Unlock() return 0, errors.New("new handshake needed") } return 0, err } c.Client = nil if c.rawInput.Cap() < l { c.rawInput.Grow(l) // no need to use sync.Pool, because we are always reading } peerData := c.rawInput.Bytes()[:l] if _, err := io.ReadFull(c.Conn, peerData); err != nil { return 0, err } dst := peerData[:l-16] if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } var newAEAD *AEAD if bytes.Equal(c.PeerAEAD.Nonce[:], MaxNonce) { newAEAD = NewAEAD(append(peerHeader[:], peerData...), c.UnitedKey, c.UseAES) } _, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader[:]) if newAEAD != nil { c.PeerAEAD = newAEAD } if err != nil { return 0, err } if len(dst) > len(b) { c.input.Reset(dst[copy(b, dst):]) dst = b // for len(dst) } return len(dst), nil } type AEAD struct { cipher.AEAD Nonce [12]byte } func NewAEAD(ctx, key []byte, useAES bool) *AEAD { k := make([]byte, 32) blake3.DeriveKey(k, string(ctx), key) var aead cipher.AEAD if useAES { block, _ := aes.NewCipher(k) aead, _ = cipher.NewGCM(block) } else { aead, _ = chacha20poly1305.New(k) } return &AEAD{AEAD: aead} } func (a *AEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte { if nonce == nil { nonce = IncreaseNonce(a.Nonce[:]) } return a.AEAD.Seal(dst, nonce, plaintext, additionalData) } func (a *AEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { if nonce == nil { nonce = IncreaseNonce(a.Nonce[:]) } return a.AEAD.Open(dst, nonce, ciphertext, additionalData) } func IncreaseNonce(nonce []byte) []byte { for i := 0; i < 12; i++ { nonce[11-i]++ if nonce[11-i] != 0 { break } } return nonce } var MaxNonce = bytes.Repeat([]byte{255}, 12) func EncodeLength(l int) []byte { return []byte{byte(l >> 8), byte(l)} } func DecodeLength(b []byte) int { return int(b[0])<<8 | int(b[1]) } func EncodeHeader(h []byte, l int) { h[0] = 23 h[1] = 3 h[2] = 3 h[3] = byte(l >> 8) h[4] = byte(l) } var ErrInvalidHeader = errors.New("invalid header") func DecodeHeader(h []byte) (l int, err error) { l = int(h[3])<<8 | int(h[4]) if h[0] != 23 || h[1] != 3 || h[2] != 3 { l = 0 } if l < 17 || l > 17000 { // TODO: TLSv1.3 max length err = fmt.Errorf("%w: %v", ErrInvalidHeader, h[:5]) // DO NOT CHANGE: relied by client's Read() } return } func ParsePadding(padding string, paddingLens, paddingGaps *[][3]int) (err error) { if padding == "" { return } maxLen := 0 for i, s := range strings.Split(padding, ".") { x := strings.Split(s, "-") if len(x) < 3 || x[0] == "" || x[1] == "" || x[2] == "" { return errors.New("invalid padding lenth/gap parameter: " + s) } y := [3]int{} if y[0], err = strconv.Atoi(x[0]); err != nil { return } if y[1], err = strconv.Atoi(x[1]); err != nil { return } if y[2], err = strconv.Atoi(x[2]); err != nil { return } if i == 0 && (y[0] < 100 || y[1] < 18+17 || y[2] < 18+17) { return errors.New("first padding length must not be smaller than 35") } if i%2 == 0 { *paddingLens = append(*paddingLens, y) maxLen += max(y[1], y[2]) } else { *paddingGaps = append(*paddingGaps, y) } } if maxLen > 18+65535 { return errors.New("total padding length must not be larger than 65553") } return } func CreatPadding(paddingLens, paddingGaps [][3]int) (length int, lens []int, gaps []time.Duration) { if len(paddingLens) == 0 { paddingLens = [][3]int{{100, 111, 1111}, {50, 0, 3333}} paddingGaps = [][3]int{{75, 0, 111}} } for _, y := range paddingLens { l := 0 if y[0] >= int(randBetween(0, 100)) { l = int(randBetween(int64(y[1]), int64(y[2]))) } lens = append(lens, l) length += l } for _, y := range paddingGaps { g := 0 if y[0] >= int(randBetween(0, 100)) { g = int(randBetween(int64(y[1]), int64(y[2]))) } gaps = append(gaps, time.Duration(g)*time.Millisecond) } return } func max[T ~int | ~uint | ~int64 | ~uint64 | ~int32 | ~uint32 | ~int16 | ~uint16 | ~int8 | ~uint8](a, b T) T { if a > b { return a } return b } func randBetween(from int64, to int64) int64 { if from == to { return from } if to < from { from, to = to, from } return from + randv2.Int64N(to-from) } ================================================ FILE: core/Clash.Meta/transport/vless/encryption/doc.go ================================================ // Package encryption copy and modify from xray-core // https://github.com/XTLS/Xray-core/commit/f61c14e9c63dc41a8a09135db3aea337974f3f37 // https://github.com/XTLS/Xray-core/commit/3e19bf9233bdd9bafc073a71c65b737cc1ffba5e // https://github.com/XTLS/Xray-core/commit/7ffb555fc8ec51bd1e3e60f26f1d6957984dba80 // https://github.com/XTLS/Xray-core/commit/ec1cc35188c1a5f38a2ff75e88b5d043ffdc59da // https://github.com/XTLS/Xray-core/commit/5c611420487a92f931faefc01d4bf03869f477f6 // https://github.com/XTLS/Xray-core/commit/23d7aad461d232bc5bed52dd6aaa731ecd88ad35 // https://github.com/XTLS/Xray-core/commit/3c20bddfcfd8999be5f9a2ac180dc959950e4c61 // https://github.com/XTLS/Xray-core/commit/1720be168fa069332c418503d30341fc6e01df7f // https://github.com/XTLS/Xray-core/commit/0fd7691d6b28e05922d7a5a9313d97745a51ea63 // https://github.com/XTLS/Xray-core/commit/09cc92c61d9067e0d65c1cae9124664ecfc78f43 // https://github.com/XTLS/Xray-core/commit/2807ee432a1fbeb301815647189eacd650b12a8b // https://github.com/XTLS/Xray-core/commit/bfe4820f2f086daf639b1957eb23dc13c843cad1 // https://github.com/XTLS/Xray-core/commit/d1fb48521271251a8c74bd64fcc2fc8700717a3b // https://github.com/XTLS/Xray-core/commit/49580705f6029648399304b816a2737f991582a8 // https://github.com/XTLS/Xray-core/commit/84835bec7d0d8555d0dd30953ed26a272de814c4 // https://github.com/XTLS/Xray-core/commit/373558ed7abdbac3de41745cf30ec04c9adde604 // https://github.com/XTLS/Xray-core/commit/38cc306c955c362f044e074049a5e67b6b9fb389 // https://github.com/XTLS/Xray-core/commit/b33555cc0a52d0af3c23d2af8fca42f8a685d9af // https://github.com/XTLS/Xray-core/commit/ad7140641c44239c9dcdc3d7215ea639b1f0841c // https://github.com/XTLS/Xray-core/commit/0199dea39988a1a1b846d0bf8598631bade40902 // https://github.com/XTLS/Xray-core/commit/fce1195b60f48ca18a953dbd5c7d991869de9a5e // https://github.com/XTLS/Xray-core/commit/b0b220985c9c1bc832665458d5fd6e0c287b67ae // https://github.com/XTLS/Xray-core/commit/82ea7a3cc5ff23280b87e3052f0f83b04f0267fa // https://github.com/XTLS/Xray-core/commit/e8b02cd6649f14889841e8ab8ee6b2acca71dbe6 // https://github.com/XTLS/Xray-core/commit/6768a22f676c9121cfc9dc4f51181a8a07837c8d // https://github.com/XTLS/Xray-core/commit/4c6fd94d97159f5a3e740ba6dd2d9b65e3ed320c // https://github.com/XTLS/Xray-core/commit/19f890729656bc923ae3dee8426168c93b8ee9c2 // https://github.com/XTLS/Xray-core/commit/cbade89ab11af26ba1e480a3688a6c205fa3c3f8 package encryption ================================================ FILE: core/Clash.Meta/transport/vless/encryption/factory.go ================================================ package encryption import ( "encoding/base64" "fmt" "strconv" "strings" ) // NewClient new client from encryption string // maybe return a nil *ClientInstance without any error, that means don't need to encrypt func NewClient(encryption string) (*ClientInstance, error) { switch encryption { case "", "none": // We will not reject empty string like xray-core does, because we need to ensure compatibility return nil, nil } if s := strings.Split(encryption, "."); len(s) >= 4 && s[0] == "mlkem768x25519plus" { var xorMode uint32 switch s[1] { case "native": case "xorpub": xorMode = 1 case "random": xorMode = 2 default: return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) } var seconds uint32 switch s[2] { case "1rtt": case "0rtt": seconds = 1 default: return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) } var nfsPKeysBytes [][]byte var paddings []string for _, r := range s[3:] { if len(r) < 20 { paddings = append(paddings, r) continue } b, err := base64.RawURLEncoding.DecodeString(r) if err != nil { return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) } if len(b) != X25519PasswordSize && len(b) != MLKEM768ClientLength { return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) } nfsPKeysBytes = append(nfsPKeysBytes, b) } padding := strings.Join(paddings, ".") client := &ClientInstance{} if err := client.Init(nfsPKeysBytes, xorMode, seconds, padding); err != nil { return nil, fmt.Errorf("failed to use encryption: %w", err) } return client, nil } return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) } // NewServer new server from decryption string // maybe return a nil *ServerInstance without any error, that means don't need to decrypt func NewServer(decryption string) (*ServerInstance, error) { switch decryption { case "", "none": // We will not reject empty string like xray-core does, because we need to ensure compatibility return nil, nil } if s := strings.Split(decryption, "."); len(s) >= 4 && s[0] == "mlkem768x25519plus" { var xorMode uint32 switch s[1] { case "native": case "xorpub": xorMode = 1 case "random": xorMode = 2 default: return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } t := strings.SplitN(strings.TrimSuffix(s[2], "s"), "-", 2) i, err := strconv.Atoi(t[0]) if err != nil { return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } secondsFrom := int64(i) secondsTo := int64(0) if len(t) == 2 { i, err = strconv.Atoi(t[1]) if err != nil { return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } secondsTo = int64(i) } var nfsSKeysBytes [][]byte var paddings []string for _, r := range s[3:] { if len(r) < 20 { paddings = append(paddings, r) continue } b, err := base64.RawURLEncoding.DecodeString(r) if err != nil { return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } if len(b) != X25519PrivateKeySize && len(b) != MLKEM768SeedLength { return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } nfsSKeysBytes = append(nfsSKeysBytes, b) } padding := strings.Join(paddings, ".") server := &ServerInstance{} if err := server.Init(nfsSKeysBytes, xorMode, secondsFrom, secondsTo, padding); err != nil { return nil, fmt.Errorf("failed to use decryption: %w", err) } return server, nil } return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } ================================================ FILE: core/Clash.Meta/transport/vless/encryption/key.go ================================================ package encryption import ( "crypto/ecdh" "crypto/rand" "encoding/base64" "fmt" "github.com/metacubex/blake3" "github.com/metacubex/mlkem" ) const MLKEM768SeedLength = mlkem.SeedSize const MLKEM768ClientLength = mlkem.EncapsulationKeySize768 const X25519PasswordSize = 32 const X25519PrivateKeySize = 32 func GenMLKEM768(seedStr string) (seedBase64, clientBase64, hash32Base64 string, err error) { var seed [MLKEM768SeedLength]byte if len(seedStr) > 0 { s, _ := base64.RawURLEncoding.DecodeString(seedStr) if len(s) != MLKEM768SeedLength { err = fmt.Errorf("invalid length of ML-KEM-768 seed: %s", seedStr) return } seed = [MLKEM768SeedLength]byte(s) } else { _, err = rand.Read(seed[:]) if err != nil { return } } key, _ := mlkem.NewDecapsulationKey768(seed[:]) client := key.EncapsulationKey().Bytes() hash32 := blake3.Sum256(client) seedBase64 = base64.RawURLEncoding.EncodeToString(seed[:]) clientBase64 = base64.RawURLEncoding.EncodeToString(client) hash32Base64 = base64.RawURLEncoding.EncodeToString(hash32[:]) return } func GenX25519(privateKeyStr string) (privateKeyBase64, passwordBase64, hash32Base64 string, err error) { var privateKey [X25519PrivateKeySize]byte if len(privateKeyStr) > 0 { s, _ := base64.RawURLEncoding.DecodeString(privateKeyStr) if len(s) != X25519PrivateKeySize { err = fmt.Errorf("invalid length of X25519 private key: %s", privateKeyStr) return } privateKey = [X25519PrivateKeySize]byte(s) } else { _, err = rand.Read(privateKey[:]) if err != nil { return } } // Avoid generating equivalent X25519 private keys // https://github.com/XTLS/Xray-core/pull/1747 // // Modify random bytes using algorithm described at: // https://cr.yp.to/ecdh.html. privateKey[0] &= 248 privateKey[31] &= 127 privateKey[31] |= 64 key, err := ecdh.X25519().NewPrivateKey(privateKey[:]) if err != nil { fmt.Println(err.Error()) return } password := key.PublicKey().Bytes() hash32 := blake3.Sum256(password) privateKeyBase64 = base64.RawURLEncoding.EncodeToString(privateKey[:]) passwordBase64 = base64.RawURLEncoding.EncodeToString(password) hash32Base64 = base64.RawURLEncoding.EncodeToString(hash32[:]) return } ================================================ FILE: core/Clash.Meta/transport/vless/encryption/server.go ================================================ package encryption import ( "bytes" "crypto/cipher" "crypto/ecdh" "crypto/rand" "errors" "fmt" "io" "net" "sync" "time" "github.com/metacubex/blake3" "github.com/metacubex/mlkem" ) type ServerSession struct { PfsKey []byte NfsKeys sync.Map } type ServerInstance struct { NfsSKeys []any NfsPKeysBytes [][]byte Hash32s [][32]byte RelaysLength int XorMode uint32 SecondsFrom int64 SecondsTo int64 PaddingLens [][3]int PaddingGaps [][3]int RWLock sync.RWMutex Closed bool Lasts map[int64][16]byte Tickets [][16]byte Sessions map[[16]byte]*ServerSession } func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode uint32, secondsFrom, secondsTo int64, padding string) (err error) { if i.NfsSKeys != nil { return errors.New("already initialized") } l := len(nfsSKeysBytes) if l == 0 { return errors.New("empty nfsSKeysBytes") } i.NfsSKeys = make([]any, l) i.NfsPKeysBytes = make([][]byte, l) i.Hash32s = make([][32]byte, l) for j, k := range nfsSKeysBytes { if len(k) == 32 { if i.NfsSKeys[j], err = ecdh.X25519().NewPrivateKey(k); err != nil { return } i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*ecdh.PrivateKey).PublicKey().Bytes() i.RelaysLength += 32 + 32 } else { if i.NfsSKeys[j], err = mlkem.NewDecapsulationKey768(k); err != nil { return } i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*mlkem.DecapsulationKey768).EncapsulationKey().Bytes() i.RelaysLength += 1088 + 32 } i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j]) } i.RelaysLength -= 32 i.XorMode = xorMode i.SecondsFrom = secondsFrom i.SecondsTo = secondsTo err = ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) if err != nil { return } if i.SecondsFrom > 0 || i.SecondsTo > 0 { i.Lasts = make(map[int64][16]byte) i.Tickets = make([][16]byte, 0, 1024) i.Sessions = make(map[[16]byte]*ServerSession) go func() { for { time.Sleep(time.Minute) i.RWLock.Lock() if i.Closed { i.RWLock.Unlock() return } minute := time.Now().Unix() / 60 last := i.Lasts[minute] delete(i.Lasts, minute) delete(i.Lasts, minute-1) // for insurance if last != [16]byte{} { for j, ticket := range i.Tickets { delete(i.Sessions, ticket) if ticket == last { i.Tickets = i.Tickets[j+1:] break } } } i.RWLock.Unlock() } }() } return } func (i *ServerInstance) Close() (err error) { i.RWLock.Lock() i.Closed = true i.RWLock.Unlock() return } func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) { if i.NfsSKeys == nil { return nil, errors.New("uninitialized") } c := NewCommonConn(conn, true) ivAndRelays := make([]byte, 16+i.RelaysLength) if _, err := io.ReadFull(conn, ivAndRelays); err != nil { return nil, err } if fallback != nil { *fallback = append(*fallback, ivAndRelays...) } iv := ivAndRelays[:16] relays := ivAndRelays[16:] var nfsKey []byte var lastCTR cipher.Stream for j, k := range i.NfsSKeys { if lastCTR != nil { lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay } var index = 32 if _, ok := k.(*mlkem.DecapsulationKey768); ok { index = 1088 } if i.XorMode > 0 { NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator2, because we have PSK :) } if k, ok := k.(*ecdh.PrivateKey); ok { publicKey, err := ecdh.X25519().NewPublicKey(relays[:index]) if err != nil { return nil, err } if publicKey.Bytes()[31] > 127 { // we just don't want the observer can change even one bit without breaking the connection, though it has nothing to do with security return nil, errors.New("the highest bit of the last byte of the peer-sent X25519 public key is not 0") } nfsKey, err = k.ECDH(publicKey) if err != nil { return nil, err } } if k, ok := k.(*mlkem.DecapsulationKey768); ok { var err error nfsKey, err = k.Decapsulate(relays[:index]) if err != nil { return nil, err } } if j == len(i.NfsSKeys)-1 { break } relays = relays[index:] lastCTR = NewCTR(nfsKey, iv) lastCTR.XORKeyStream(relays, relays[:32]) if !bytes.Equal(relays[:32], i.Hash32s[j+1][:]) { return nil, fmt.Errorf("unexpected hash32: %v", relays[:32]) } relays = relays[32:] } nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES) encryptedLength := make([]byte, 18) if _, err := io.ReadFull(conn, encryptedLength); err != nil { return nil, err } if fallback != nil { *fallback = append(*fallback, encryptedLength...) } decryptedLength := make([]byte, 2) if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil { c.UseAES = !c.UseAES nfsAEAD = NewAEAD(iv, nfsKey, c.UseAES) if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil { return nil, err } } if fallback != nil { *fallback = nil } length := DecodeLength(decryptedLength) if length == 32 { if i.SecondsFrom == 0 && i.SecondsTo == 0 { return nil, errors.New("0-RTT is not allowed") } encryptedTicket := make([]byte, 32) if _, err := io.ReadFull(conn, encryptedTicket); err != nil { return nil, err } ticket, err := nfsAEAD.Open(nil, nil, encryptedTicket, nil) if err != nil { return nil, err } i.RWLock.RLock() s := i.Sessions[[16]byte(ticket)] i.RWLock.RUnlock() if s == nil { noises := make([]byte, randBetween(1279, 2279)) // matches 1-RTT's server hello length for "random", though it is not important, just for example var err error for err == nil { rand.Read(noises) _, err = DecodeHeader(noises) } conn.Write(noises) // make client do new handshake return nil, errors.New("expired ticket") } if _, loaded := s.NfsKeys.LoadOrStore([32]byte(nfsKey), true); loaded { // prevents bad client also return nil, errors.New("replay detected") } c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download (prevents server -> client's another request) c.PreWrite = make([]byte, 16) rand.Read(c.PreWrite) // always trust yourself, not the client (also prevents being parsed as TLS thus causing false interruption for "native" and "xorpub") c.AEAD = NewAEAD(c.PreWrite, c.UnitedKey, c.UseAES) c.PeerAEAD = NewAEAD(encryptedTicket, c.UnitedKey, c.UseAES) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client) if i.XorMode == 2 { c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite), NewCTR(c.UnitedKey, iv), 16, 0) // it doesn't matter if the attacker sends client's iv back to the client } return c, nil } if length < 1184+32+16 { // client may send more public keys in the future's version return nil, errors.New("too short length") } encryptedPfsPublicKey := make([]byte, length) if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { return nil, err } if _, err := nfsAEAD.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil { return nil, err } mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184]) if err != nil { return nil, err } mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate() peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32]) if err != nil { return nil, err } x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) x25519Key, err := x25519SKey.ECDH(peerX25519PKey) if err != nil { return nil, err } pfsKey := make([]byte, 32+32) // no more capacity copy(pfsKey, mlkem768Key) copy(pfsKey[32:], x25519Key) pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...) c.UnitedKey = append(pfsKey, nfsKey...) c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES) c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1184+32], c.UnitedKey, c.UseAES) ticket := [16]byte{} rand.Read(ticket[:]) var seconds int64 if i.SecondsTo == 0 { seconds = i.SecondsFrom * randBetween(50, 100) / 100 } else { seconds = randBetween(i.SecondsFrom, i.SecondsTo) } copy(ticket[:], EncodeLength(int(seconds))) if seconds > 0 { i.RWLock.Lock() i.Lasts[(time.Now().Unix()+max(i.SecondsFrom, i.SecondsTo))/60+2] = ticket i.Tickets = append(i.Tickets, ticket) i.Sessions[ticket] = &ServerSession{PfsKey: pfsKey} i.RWLock.Unlock() } pfsKeyExchangeLength := 1088 + 32 + 16 encryptedTicketLength := 32 paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps) serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength) nfsAEAD.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil) c.AEAD.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket[:], nil) padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:] c.AEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) c.AEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) paddingLens[0] = pfsKeyExchangeLength + encryptedTicketLength + paddingLens[0] for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control if l > 0 { if _, err := conn.Write(serverHello[:l]); err != nil { return nil, err } serverHello = serverHello[l:] } if len(paddingGaps) > i { time.Sleep(paddingGaps[i]) } } // important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern if _, err := io.ReadFull(conn, encryptedLength); err != nil { return nil, err } if _, err := nfsAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { return nil, err } encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) if _, err := io.ReadFull(conn, encryptedPadding); err != nil { return nil, err } if _, err := nfsAEAD.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil { return nil, err } if i.XorMode == 2 { c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket[:]), NewCTR(c.UnitedKey, iv), 0, 0) } return c, nil } ================================================ FILE: core/Clash.Meta/transport/vless/encryption/xor.go ================================================ package encryption import ( "crypto/aes" "crypto/cipher" "net" "github.com/metacubex/blake3" ) func NewCTR(key, iv []byte) cipher.Stream { k := make([]byte, 32) blake3.DeriveKey(k, "VLESS", key) // avoids using key directly block, _ := aes.NewCipher(k) return cipher.NewCTR(block, iv) //chacha20.NewUnauthenticatedCipher() } type XorConn struct { net.Conn CTR cipher.Stream PeerCTR cipher.Stream OutSkip int OutHeader []byte InSkip int InHeader []byte } func NewXorConn(conn net.Conn, ctr, peerCTR cipher.Stream, outSkip, inSkip int) *XorConn { return &XorConn{ Conn: conn, CTR: ctr, PeerCTR: peerCTR, OutSkip: outSkip, OutHeader: make([]byte, 0, 5), // important InSkip: inSkip, InHeader: make([]byte, 0, 5), // important } } func (c *XorConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } for p := b; ; { if len(p) <= c.OutSkip { c.OutSkip -= len(p) break } p = p[c.OutSkip:] c.OutSkip = 0 need := 5 - len(c.OutHeader) if len(p) < need { c.OutHeader = append(c.OutHeader, p...) c.CTR.XORKeyStream(p, p) break } c.OutSkip, _ = DecodeHeader(append(c.OutHeader, p[:need]...)) c.OutHeader = c.OutHeader[:0] c.CTR.XORKeyStream(p[:need], p[:need]) p = p[need:] } if _, err := c.Conn.Write(b); err != nil { return 0, err } return len(b), nil } func (c *XorConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } n, err := c.Conn.Read(b) for p := b[:n]; ; { if len(p) <= c.InSkip { c.InSkip -= len(p) break } p = p[c.InSkip:] c.InSkip = 0 need := 5 - len(c.InHeader) if len(p) < need { c.PeerCTR.XORKeyStream(p, p) c.InHeader = append(c.InHeader, p...) break } c.PeerCTR.XORKeyStream(p[:need], p[:need]) c.InSkip, _ = DecodeHeader(append(c.InHeader, p[:need]...)) c.InHeader = c.InHeader[:0] p = p[need:] } return n, err } ================================================ FILE: core/Clash.Meta/transport/vless/packet.go ================================================ package vless import ( "encoding/binary" "io" "net" "github.com/metacubex/mihomo/common/pool" ) type PacketConn struct { net.Conn rAddr net.Addr } func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { err := binary.Write(c.Conn, binary.BigEndian, uint16(len(b))) if err != nil { return 0, err } return c.Conn.Write(b) } func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { var length uint16 err := binary.Read(c.Conn, binary.BigEndian, &length) if err != nil { return 0, nil, err } if len(b) < int(length) { return 0, nil, io.ErrShortBuffer } n, err := io.ReadFull(c.Conn, b[:length]) return n, c.rAddr, err } func (c *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { var length uint16 err = binary.Read(c.Conn, binary.BigEndian, &length) if err != nil { return } readBuf := pool.Get(int(length)) put = func() { _ = pool.Put(readBuf) } n, err := io.ReadFull(c.Conn, readBuf) if err != nil { put() put = nil return } data = readBuf[:n] addr = c.rAddr return } ================================================ FILE: core/Clash.Meta/transport/vless/vision/conn.go ================================================ package vision import ( "bytes" "encoding/binary" "errors" "fmt" "io" "net" "unsafe" "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/log" "github.com/gofrs/uuid/v5" ) var ( _ N.ExtendedConn = (*Conn)(nil) ) type Conn struct { net.Conn // should be *vless.Conn N.ExtendedReader N.ExtendedWriter userUUID uuid.UUID // [*tls.Conn] or other tls-like [net.Conn]'s internal variables netConn net.Conn // tlsConn.NetConn() input *bytes.Reader // &tlsConn.input or nil rawInput *bytes.Buffer // &tlsConn.rawInput or nil packetsToFilter int isTLS bool isTLS12orAbove bool enableXTLS bool cipher uint16 remainingServerHello uint16 readRemainingBuffer *buf.Buffer readRemainingContent int readRemainingPadding int readProcess bool readFilterUUID bool readLastCommand byte writeFilterApplicationData bool writeDirect bool writeOnceUserUUID []byte } func (vc *Conn) Read(b []byte) (int, error) { if vc.readProcess { buffer := buf.With(b) err := vc.ReadBuffer(buffer) if unsafe.SliceData(buffer.Bytes()) != unsafe.SliceData(b) { // buffer.Bytes() not at the beginning of b copy(b, buffer.Bytes()) } return buffer.Len(), err } return vc.ExtendedReader.Read(b) } func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { if vc.readRemainingBuffer != nil { _, err := buffer.ReadOnceFrom(vc.readRemainingBuffer) if vc.readRemainingBuffer.IsEmpty() { vc.readRemainingBuffer.Release() vc.readRemainingBuffer = nil } return err } if vc.readRemainingContent > 0 { readSize := xrayBufSize // at least read xrayBufSize if buffer.FreeLen() > readSize { // input buffer larger than xrayBufSize, read as much as possible readSize = buffer.FreeLen() } if readSize > vc.readRemainingContent { // don't read out of bounds readSize = vc.readRemainingContent } readBuffer := buffer if buffer.FreeLen() < readSize { readBuffer = buf.NewSize(readSize) vc.readRemainingBuffer = readBuffer } n, err := vc.ExtendedReader.Read(readBuffer.FreeBytes()[:readSize]) readBuffer.Truncate(n) vc.readRemainingContent -= n vc.FilterTLS(readBuffer.Bytes()) if vc.readRemainingBuffer != nil { innerErr := vc.ReadBuffer(buffer) // back to top but not losing err if err != nil { err = innerErr } } return err } if vc.readRemainingPadding > 0 { n, err := io.CopyN(io.Discard, vc.ExtendedReader, int64(vc.readRemainingPadding)) if err != nil { return err } vc.readRemainingPadding -= int(n) } if vc.readProcess { switch vc.readLastCommand { case commandPaddingContinue: //if vc.isTLS || vc.packetsToFilter > 0 { need := PaddingHeaderLen if !vc.readFilterUUID { need = PaddingHeaderLen - uuid.Size } var header []byte if buffer.FreeLen() < need { header = make([]byte, need) } else { header = buffer.FreeBytes()[:need] } _, err := io.ReadFull(vc.ExtendedReader, header) if err != nil { return err } if vc.readFilterUUID { vc.readFilterUUID = false if !bytes.Equal(vc.userUUID.Bytes(), header[:uuid.Size]) { err = fmt.Errorf("XTLS Vision server responded unknown UUID: %s", uuid.FromBytesOrNil(header[:uuid.Size])) log.Errorln(err.Error()) return err } header = header[uuid.Size:] } vc.readRemainingPadding = int(binary.BigEndian.Uint16(header[3:])) vc.readRemainingContent = int(binary.BigEndian.Uint16(header[1:])) vc.readLastCommand = header[0] log.Debugln("XTLS Vision read padding: command=%d, payloadLen=%d, paddingLen=%d", vc.readLastCommand, vc.readRemainingContent, vc.readRemainingPadding) return vc.ReadBuffer(buffer) //} case commandPaddingEnd: vc.readProcess = false return vc.ReadBuffer(buffer) case commandPaddingDirect: needReturn := false if vc.input != nil { _, err := buffer.ReadOnceFrom(vc.input) if err != nil { if !errors.Is(err, io.EOF) { return err } } if vc.input.Len() == 0 { needReturn = true *vc.input = bytes.Reader{} // full reset vc.input = nil } else { // buffer is full return nil } } if vc.rawInput != nil { _, err := buffer.ReadOnceFrom(vc.rawInput) if err != nil { if !errors.Is(err, io.EOF) { return err } } needReturn = true if vc.rawInput.Len() == 0 { *vc.rawInput = bytes.Buffer{} // full reset vc.rawInput = nil } } if vc.input == nil && vc.rawInput == nil { vc.readProcess = false vc.ExtendedReader = N.NewExtendedReader(vc.netConn) log.Debugln("XTLS Vision direct read start") } if needReturn { return nil } default: err := fmt.Errorf("XTLS Vision read unknown command: %d", vc.readLastCommand) log.Debugln(err.Error()) return err } } return vc.ExtendedReader.ReadBuffer(buffer) } func (vc *Conn) Write(p []byte) (int, error) { if vc.writeFilterApplicationData { return N.WriteBuffer(vc, buf.As(p)) } return vc.ExtendedWriter.Write(p) } func (vc *Conn) WriteBuffer(buffer *buf.Buffer) (err error) { if vc.writeFilterApplicationData { if buffer.IsEmpty() { ApplyPadding(buffer, commandPaddingContinue, &vc.writeOnceUserUUID, true) // we do a long padding to hide vless header return vc.ExtendedWriter.WriteBuffer(buffer) } vc.FilterTLS(buffer.Bytes()) buffers := vc.ReshapeBuffer(buffer) applyPadding := true for i, buffer := range buffers { command := commandPaddingContinue if applyPadding { if vc.isTLS && buffer.Len() > 6 && bytes.Equal(tlsApplicationDataStart, buffer.To(3)) { command = commandPaddingEnd if vc.enableXTLS { command = commandPaddingDirect vc.writeDirect = true } vc.writeFilterApplicationData = false applyPadding = false } else if !vc.isTLS12orAbove && vc.packetsToFilter <= 1 { command = commandPaddingEnd vc.writeFilterApplicationData = false applyPadding = false } ApplyPadding(buffer, command, &vc.writeOnceUserUUID, vc.isTLS) } err = vc.ExtendedWriter.WriteBuffer(buffer) if err != nil { buf.ReleaseMulti(buffers[i:]) // release unwritten buffers return } if command == commandPaddingDirect { vc.ExtendedWriter = N.NewExtendedWriter(vc.netConn) log.Debugln("XTLS Vision direct write start") //time.Sleep(5 * time.Millisecond) } } return err } /*if vc.writeDirect { log.Debugln("XTLS Vision Direct write, payloadLen=%d", buffer.Len()) }*/ return vc.ExtendedWriter.WriteBuffer(buffer) } func (vc *Conn) FrontHeadroom() int { fontHeadroom := PaddingHeaderLen - uuid.Size if vc.readFilterUUID || vc.writeOnceUserUUID != nil { fontHeadroom = PaddingHeaderLen } if vc.writeFilterApplicationData { // The writer may be replaced, add the required value for vc.netConn if abs := N.CalculateFrontHeadroom(vc.netConn) - N.CalculateFrontHeadroom(vc.Conn); abs > 0 { fontHeadroom += abs } } return fontHeadroom } func (vc *Conn) RearHeadroom() int { rearHeadroom := 500 + 900 if vc.writeFilterApplicationData { // The writer may be replaced, add the required value for vc.netConn if abs := N.CalculateRearHeadroom(vc.netConn) - N.CalculateRearHeadroom(vc.Conn); abs > 0 { rearHeadroom += abs } } return rearHeadroom } func (vc *Conn) NeedHandshake() bool { return vc.writeOnceUserUUID != nil } func (vc *Conn) NeedAdditionalReadDeadline() bool { return true } func (vc *Conn) Upstream() any { if vc.writeDirect || vc.readLastCommand == commandPaddingDirect { return vc.netConn } return vc.Conn } func (vc *Conn) ReaderPossiblyReplaceable() bool { return vc.readProcess } func (vc *Conn) ReaderReplaceable() bool { if !vc.readProcess && vc.readLastCommand == commandPaddingDirect { return true } return false } func (vc *Conn) WriterPossiblyReplaceable() bool { return vc.writeFilterApplicationData } func (vc *Conn) WriterReplaceable() bool { if vc.writeDirect { return true } return false } func (vc *Conn) Close() error { if vc.ReaderReplaceable() || vc.WriterReplaceable() { // ignore send closeNotify alert in tls.Conn return vc.netConn.Close() } return vc.Conn.Close() } ================================================ FILE: core/Clash.Meta/transport/vless/vision/filter.go ================================================ package vision import ( "bytes" "encoding/binary" "github.com/metacubex/mihomo/log" ) var ( tls13SupportedVersions = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04} tlsClientHandshakeStart = []byte{0x16, 0x03} tlsServerHandshakeStart = []byte{0x16, 0x03, 0x03} tlsApplicationDataStart = []byte{0x17, 0x03, 0x03} tls13CipherSuiteMap = map[uint16]string{ 0x1301: "TLS_AES_128_GCM_SHA256", 0x1302: "TLS_AES_256_GCM_SHA384", 0x1303: "TLS_CHACHA20_POLY1305_SHA256", 0x1304: "TLS_AES_128_CCM_SHA256", 0x1305: "TLS_AES_128_CCM_8_SHA256", } ) const ( tlsHandshakeTypeClientHello byte = 0x01 tlsHandshakeTypeServerHello byte = 0x02 ) func (vc *Conn) FilterTLS(buffer []byte) (index int) { if vc.packetsToFilter <= 0 { return 0 } lenP := len(buffer) vc.packetsToFilter-- if index = bytes.Index(buffer, tlsServerHandshakeStart); index != -1 { if lenP > index+5 { if buffer[0] == 22 && buffer[1] == 3 && buffer[2] == 3 { vc.isTLS = true if buffer[5] == tlsHandshakeTypeServerHello { //log.Debugln("isTLS12orAbove") vc.remainingServerHello = binary.BigEndian.Uint16(buffer[index+3:]) + 5 vc.isTLS12orAbove = true if lenP-index >= 79 && vc.remainingServerHello >= 79 { sessionIDLen := int(buffer[index+43]) vc.cipher = binary.BigEndian.Uint16(buffer[index+43+sessionIDLen+1:]) } } } } } else if index = bytes.Index(buffer, tlsClientHandshakeStart); index != -1 { if lenP > index+5 && buffer[index+5] == tlsHandshakeTypeClientHello { vc.isTLS = true } } if vc.remainingServerHello > 0 { end := int(vc.remainingServerHello) i := index if i < 0 { i = 0 } if i+end > lenP { end = lenP vc.remainingServerHello -= uint16(end - i) } else { vc.remainingServerHello -= uint16(end) end += i } if bytes.Contains(buffer[i:end], tls13SupportedVersions) { // TLS 1.3 Client Hello cs, ok := tls13CipherSuiteMap[vc.cipher] if ok && cs != "TLS_AES_128_CCM_8_SHA256" { vc.enableXTLS = true } log.Debugln("XTLS Vision found TLS 1.3, packetLength=%d, CipherSuite=%s", lenP, cs) vc.packetsToFilter = 0 return } else if vc.remainingServerHello <= 0 { log.Debugln("XTLS Vision found TLS 1.2, packetLength=%d", lenP) vc.packetsToFilter = 0 return } log.Debugln("XTLS Vision found inconclusive server hello, packetLength=%d, remainingServerHelloBytes=%d", lenP, vc.remainingServerHello) } if vc.packetsToFilter <= 0 { log.Debugln("XTLS Vision stop filtering") } return } ================================================ FILE: core/Clash.Meta/transport/vless/vision/padding.go ================================================ package vision import ( "bytes" "encoding/binary" "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/log" "github.com/gofrs/uuid/v5" "github.com/metacubex/randv2" ) const ( PaddingHeaderLen = uuid.Size + 1 + 2 + 2 // =21 commandPaddingContinue byte = 0x00 commandPaddingEnd byte = 0x01 commandPaddingDirect byte = 0x02 ) func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *[]byte, paddingTLS bool) { contentLen := int32(buffer.Len()) var paddingLen int32 if contentLen < 900 { if paddingTLS { //log.Debugln("long padding") paddingLen = randv2.Int32N(500) + 900 - contentLen } else { paddingLen = randv2.Int32N(256) } } binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(paddingLen)) binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(contentLen)) buffer.ExtendHeader(1)[0] = command if userUUID != nil && *userUUID != nil { copy(buffer.ExtendHeader(uuid.Size), *userUUID) *userUUID = nil } buffer.Extend(int(paddingLen)) log.Debugln("XTLS Vision write padding: command=%d, payloadLen=%d, paddingLen=%d", command, contentLen, paddingLen) } const xrayBufSize = 8192 func (vc *Conn) ReshapeBuffer(buffer *buf.Buffer) []*buf.Buffer { const bufferLimit = xrayBufSize - PaddingHeaderLen if buffer.Len() < bufferLimit { return []*buf.Buffer{buffer} } options := N.NewReadWaitOptions(nil, vc) var buffers []*buf.Buffer for buffer.Len() >= bufferLimit { cutAt := bytes.LastIndex(buffer.Bytes(), tlsApplicationDataStart) if cutAt < 21 || cutAt > bufferLimit { cutAt = xrayBufSize / 2 } buffer2 := options.NewBuffer() // ensure the new buffer can send used in vc.WriteBuffer buf.Must(buf.Error(buffer2.ReadFullFrom(buffer, cutAt))) buffers = append(buffers, buffer2) } buffers = append(buffers, buffer) return buffers } ================================================ FILE: core/Clash.Meta/transport/vless/vision/vision.go ================================================ // Package vision implements VLESS flow `xtls-rprx-vision` introduced by Xray-core. // // same logic as https://github.com/XTLS/Xray-core/blob/v25.9.11/proxy/proxy.go package vision import ( "bytes" gotls "crypto/tls" "errors" "fmt" "net" "reflect" "unsafe" N "github.com/metacubex/mihomo/common/net" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/vless/encryption" "github.com/gofrs/uuid/v5" "github.com/metacubex/tls" ) var ErrNotHandshakeComplete = errors.New("tls connection not handshake complete") var ErrNotTLS13 = errors.New("XTLS Vision based on TLS 1.3 outer connection") func NewConn(conn net.Conn, tlsConn net.Conn, userUUID uuid.UUID) (*Conn, error) { c := &Conn{ ExtendedReader: N.NewExtendedReader(conn), ExtendedWriter: N.NewExtendedWriter(conn), Conn: conn, userUUID: userUUID, packetsToFilter: 8, readProcess: true, readFilterUUID: true, writeFilterApplicationData: true, writeOnceUserUUID: userUUID.Bytes(), } var t reflect.Type var p unsafe.Pointer var upstream any = tlsConn for { switch underlying := upstream.(type) { case *gotls.Conn: //log.Debugln("type tls") tlsConn = underlying c.netConn = underlying.NetConn() t = reflect.TypeOf(underlying).Elem() p = unsafe.Pointer(underlying) break case *tls.Conn: //log.Debugln("type tls") tlsConn = underlying c.netConn = underlying.NetConn() t = reflect.TypeOf(underlying).Elem() p = unsafe.Pointer(underlying) break case *tlsC.Conn: //log.Debugln("type *tlsC.Conn") tlsConn = underlying c.netConn = underlying.NetConn() t = reflect.TypeOf(underlying).Elem() p = unsafe.Pointer(underlying) break case *tlsC.UConn: //log.Debugln("type *tlsC.UConn") tlsConn = underlying c.netConn = underlying.NetConn() t = reflect.TypeOf(underlying.Conn).Elem() //log.Debugln("t:%v", t) p = unsafe.Pointer(underlying.Conn) break case *encryption.CommonConn: //log.Debugln("type *encryption.CommonConn") tlsConn = underlying c.netConn = underlying.Conn t = reflect.TypeOf(underlying).Elem() p = unsafe.Pointer(underlying) break } if u, ok := upstream.(N.ReaderWithUpstream); !ok || !u.ReaderReplaceable() { // must replaceable break } if u, ok := upstream.(N.WithUpstreamReader); ok { upstream = u.UpstreamReader() continue } if u, ok := upstream.(N.WithUpstream); ok { upstream = u.Upstream() continue } } if t == nil || p == nil { log.Warnln("vision: not a valid supported TLS connection: %s", reflect.TypeOf(tlsConn)) return nil, fmt.Errorf(`failed to use vision, maybe "tls" is not enable and "encryption" is empty`) } if err := checkTLSVersion(tlsConn); err != nil { if errors.Is(err, ErrNotHandshakeComplete) { log.Warnln("vision: TLS connection not handshake complete: %s", reflect.TypeOf(tlsConn)) } else { return nil, err } } if i, ok := t.FieldByName("input"); ok { c.input = (*bytes.Reader)(unsafe.Add(p, i.Offset)) } if r, ok := t.FieldByName("rawInput"); ok { c.rawInput = (*bytes.Buffer)(unsafe.Add(p, r.Offset)) } return c, nil } func checkTLSVersion(tlsConn net.Conn) error { switch underlying := tlsConn.(type) { case *gotls.Conn: state := underlying.ConnectionState() if !state.HandshakeComplete { return ErrNotHandshakeComplete } if state.Version != gotls.VersionTLS13 { return ErrNotTLS13 } case *tls.Conn: state := underlying.ConnectionState() if !state.HandshakeComplete { return ErrNotHandshakeComplete } if state.Version != tls.VersionTLS13 { return ErrNotTLS13 } case *tlsC.Conn: state := underlying.ConnectionState() if !state.HandshakeComplete { return ErrNotHandshakeComplete } if state.Version != tlsC.VersionTLS13 { return ErrNotTLS13 } case *tlsC.UConn: state := underlying.ConnectionState() if !state.HandshakeComplete { return ErrNotHandshakeComplete } if state.Version != tlsC.VersionTLS13 { return ErrNotTLS13 } } return nil } ================================================ FILE: core/Clash.Meta/transport/vless/vless.go ================================================ package vless import ( "net" "github.com/metacubex/mihomo/common/utils" "github.com/gofrs/uuid/v5" ) const ( XRO = "xtls-rprx-origin" XRD = "xtls-rprx-direct" XRS = "xtls-rprx-splice" XRV = "xtls-rprx-vision" Version byte = 0 // protocol version. preview version is 0 ) // Command types const ( CommandTCP byte = 1 CommandUDP byte = 2 CommandMux byte = 3 ) // Addr types const ( AtypIPv4 byte = 1 AtypDomainName byte = 2 AtypIPv6 byte = 3 ) // DstAddr store destination address type DstAddr struct { UDP bool AddrType byte Addr []byte Port uint16 Mux bool // currently used for XUDP only } // Client is vless connection generator type Client struct { uuid uuid.UUID Addons *Addons } // StreamConn return a Conn with net.Conn and DstAddr func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { return newConn(conn, c, dst) } func (c *Client) PacketConn(conn net.Conn, rAddr net.Addr) net.PacketConn { return &PacketConn{conn, rAddr} } // NewClient return Client instance func NewClient(uuidStr string, addons *Addons) (*Client, error) { uid := utils.UUIDMap(uuidStr) return &Client{ uuid: uid, Addons: addons, }, nil } ================================================ FILE: core/Clash.Meta/transport/vmess/aead.go ================================================ package vmess import ( "crypto/cipher" "encoding/binary" "errors" "io" "sync" "github.com/metacubex/mihomo/common/pool" ) type aeadWriter struct { io.Writer cipher.AEAD nonce [32]byte count uint16 iv []byte writeLock sync.Mutex } func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter { return &aeadWriter{Writer: w, AEAD: aead, iv: iv} } func (w *aeadWriter) Write(b []byte) (n int, err error) { w.writeLock.Lock() buf := pool.Get(pool.RelayBufferSize) defer func() { w.writeLock.Unlock() pool.Put(buf) }() length := len(b) for { if length == 0 { break } readLen := chunkSize - w.Overhead() if length < readLen { readLen = length } payloadBuf := buf[lenSize : lenSize+chunkSize-w.Overhead()] copy(payloadBuf, b[n:n+readLen]) binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen+w.Overhead())) binary.BigEndian.PutUint16(w.nonce[:2], w.count) copy(w.nonce[2:], w.iv[2:12]) w.Seal(payloadBuf[:0], w.nonce[:w.NonceSize()], payloadBuf[:readLen], nil) w.count++ _, err = w.Writer.Write(buf[:lenSize+readLen+w.Overhead()]) if err != nil { break } n += readLen length -= readLen } return } type aeadReader struct { io.Reader cipher.AEAD nonce [32]byte buf []byte offset int iv []byte sizeBuf []byte count uint16 } func newAEADReader(r io.Reader, aead cipher.AEAD, iv []byte) *aeadReader { return &aeadReader{Reader: r, AEAD: aead, iv: iv, sizeBuf: make([]byte, lenSize)} } func (r *aeadReader) Read(b []byte) (int, error) { if r.buf != nil { n := copy(b, r.buf[r.offset:]) r.offset += n if r.offset == len(r.buf) { pool.Put(r.buf) r.buf = nil } return n, nil } _, err := io.ReadFull(r.Reader, r.sizeBuf) if err != nil { return 0, err } size := int(binary.BigEndian.Uint16(r.sizeBuf)) if size > maxSize { return 0, errors.New("buffer is larger than standard") } buf := pool.Get(size) _, err = io.ReadFull(r.Reader, buf[:size]) if err != nil { pool.Put(buf) return 0, err } binary.BigEndian.PutUint16(r.nonce[:2], r.count) copy(r.nonce[2:], r.iv[2:12]) _, err = r.Open(buf[:0], r.nonce[:r.NonceSize()], buf[:size], nil) r.count++ if err != nil { return 0, err } realLen := size - r.Overhead() n := copy(b, buf[:realLen]) if len(b) >= realLen { pool.Put(buf) return n, nil } r.offset = n r.buf = buf[:realLen] return n, nil } ================================================ FILE: core/Clash.Meta/transport/vmess/chunk.go ================================================ package vmess import ( "encoding/binary" "errors" "io" "github.com/metacubex/mihomo/common/pool" ) const ( lenSize = 2 chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 maxSize = 17 * 1024 // 2 + chunkSize + aead.Overhead() ) type chunkReader struct { io.Reader buf []byte sizeBuf []byte offset int } func newChunkReader(reader io.Reader) *chunkReader { return &chunkReader{Reader: reader, sizeBuf: make([]byte, lenSize)} } func newChunkWriter(writer io.WriteCloser) *chunkWriter { return &chunkWriter{Writer: writer} } func (cr *chunkReader) Read(b []byte) (int, error) { if cr.buf != nil { n := copy(b, cr.buf[cr.offset:]) cr.offset += n if cr.offset == len(cr.buf) { pool.Put(cr.buf) cr.buf = nil } return n, nil } _, err := io.ReadFull(cr.Reader, cr.sizeBuf) if err != nil { return 0, err } size := int(binary.BigEndian.Uint16(cr.sizeBuf)) if size > maxSize { return 0, errors.New("buffer is larger than standard") } if len(b) >= size { _, err := io.ReadFull(cr.Reader, b[:size]) if err != nil { return 0, err } return size, nil } buf := pool.Get(size) _, err = io.ReadFull(cr.Reader, buf) if err != nil { pool.Put(buf) return 0, err } n := copy(b, buf) cr.offset = n cr.buf = buf return n, nil } type chunkWriter struct { io.Writer } func (cw *chunkWriter) Write(b []byte) (n int, err error) { buf := pool.Get(pool.RelayBufferSize) defer pool.Put(buf) length := len(b) for { if length == 0 { break } readLen := chunkSize if length < chunkSize { readLen = length } payloadBuf := buf[lenSize : lenSize+chunkSize] copy(payloadBuf, b[n:n+readLen]) binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen)) _, err = cw.Writer.Write(buf[:lenSize+readLen]) if err != nil { break } n += readLen length -= readLen } return } ================================================ FILE: core/Clash.Meta/transport/vmess/conn.go ================================================ package vmess import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/md5" "crypto/rand" "crypto/sha256" "encoding/binary" "errors" "hash/fnv" "io" "net" "time" "github.com/metacubex/randv2" "golang.org/x/crypto/chacha20poly1305" ) // Conn wrapper a net.Conn with vmess protocol type Conn struct { net.Conn reader io.Reader writer io.Writer dst *DstAddr id *ID reqBodyIV []byte reqBodyKey []byte respBodyIV []byte respBodyKey []byte respV byte security byte isAead bool received bool } func (vc *Conn) Write(b []byte) (int, error) { return vc.writer.Write(b) } func (vc *Conn) Read(b []byte) (int, error) { if vc.received { return vc.reader.Read(b) } if err := vc.recvResponse(); err != nil { return 0, err } vc.received = true return vc.reader.Read(b) } func (vc *Conn) sendRequest() error { timestamp := time.Now() mbuf := &bytes.Buffer{} if !vc.isAead { h := hmac.New(md5.New, vc.id.UUID.Bytes()) binary.Write(h, binary.BigEndian, uint64(timestamp.Unix())) mbuf.Write(h.Sum(nil)) } buf := &bytes.Buffer{} // Ver IV Key V Opt buf.WriteByte(Version) buf.Write(vc.reqBodyIV[:]) buf.Write(vc.reqBodyKey[:]) buf.WriteByte(vc.respV) buf.WriteByte(OptionChunkStream) p := randv2.IntN(16) // P Sec Reserve Cmd buf.WriteByte(byte(p<<4) | byte(vc.security)) buf.WriteByte(0) if vc.dst.UDP { buf.WriteByte(CommandUDP) } else { buf.WriteByte(CommandTCP) } // Port AddrType Addr binary.Write(buf, binary.BigEndian, uint16(vc.dst.Port)) buf.WriteByte(vc.dst.AddrType) buf.Write(vc.dst.Addr) // padding if p > 0 { padding := make([]byte, p) rand.Read(padding) buf.Write(padding) } fnv1a := fnv.New32a() fnv1a.Write(buf.Bytes()) buf.Write(fnv1a.Sum(nil)) if !vc.isAead { block, err := aes.NewCipher(vc.id.CmdKey) if err != nil { return err } stream := cipher.NewCFBEncrypter(block, hashTimestamp(timestamp)) stream.XORKeyStream(buf.Bytes(), buf.Bytes()) mbuf.Write(buf.Bytes()) _, err = vc.Conn.Write(mbuf.Bytes()) return err } var fixedLengthCmdKey [16]byte copy(fixedLengthCmdKey[:], vc.id.CmdKey) vmessout := sealVMessAEADHeader(fixedLengthCmdKey, buf.Bytes(), timestamp) _, err := vc.Conn.Write(vmessout) return err } func (vc *Conn) recvResponse() error { var buf []byte if !vc.isAead { block, err := aes.NewCipher(vc.respBodyKey[:]) if err != nil { return err } stream := cipher.NewCFBDecrypter(block, vc.respBodyIV[:]) buf = make([]byte, 4) _, err = io.ReadFull(vc.Conn, buf) if err != nil { return err } stream.XORKeyStream(buf, buf) } else { aeadResponseHeaderLengthEncryptionKey := kdf(vc.respBodyKey[:], kdfSaltConstAEADRespHeaderLenKey)[:16] aeadResponseHeaderLengthEncryptionIV := kdf(vc.respBodyIV[:], kdfSaltConstAEADRespHeaderLenIV)[:12] aeadResponseHeaderLengthEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderLengthEncryptionKey) aeadResponseHeaderLengthEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock) aeadEncryptedResponseHeaderLength := make([]byte, 18) if _, err := io.ReadFull(vc.Conn, aeadEncryptedResponseHeaderLength); err != nil { return err } decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil) if err != nil { return err } decryptedResponseHeaderLength := binary.BigEndian.Uint16(decryptedResponseHeaderLengthBinaryBuffer) aeadResponseHeaderPayloadEncryptionKey := kdf(vc.respBodyKey[:], kdfSaltConstAEADRespHeaderPayloadKey)[:16] aeadResponseHeaderPayloadEncryptionIV := kdf(vc.respBodyIV[:], kdfSaltConstAEADRespHeaderPayloadIV)[:12] aeadResponseHeaderPayloadEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey) aeadResponseHeaderPayloadEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock) encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16) if _, err := io.ReadFull(vc.Conn, encryptedResponseHeaderBuffer); err != nil { return err } buf, err = aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil) if err != nil { return err } if len(buf) < 4 { return errors.New("unexpected buffer length") } } if buf[0] != vc.respV { return errors.New("unexpected response header") } if buf[2] != 0 { return errors.New("dynamic port is not supported now") } return nil } func hashTimestamp(t time.Time) []byte { md5hash := md5.New() ts := make([]byte, 8) binary.BigEndian.PutUint64(ts, uint64(t.Unix())) md5hash.Write(ts) md5hash.Write(ts) md5hash.Write(ts) md5hash.Write(ts) return md5hash.Sum(nil) } // newConn return a Conn instance func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security, isAead bool) (*Conn, error) { randBytes := make([]byte, 33) rand.Read(randBytes) reqBodyIV := make([]byte, 16) reqBodyKey := make([]byte, 16) copy(reqBodyIV[:], randBytes[:16]) copy(reqBodyKey[:], randBytes[16:32]) respV := randBytes[32] var ( respBodyKey []byte respBodyIV []byte ) if isAead { bodyKey := sha256.Sum256(reqBodyKey) bodyIV := sha256.Sum256(reqBodyIV) respBodyKey = bodyKey[:16] respBodyIV = bodyIV[:16] } else { bodyKey := md5.Sum(reqBodyKey) bodyIV := md5.Sum(reqBodyIV) respBodyKey = bodyKey[:] respBodyIV = bodyIV[:] } var writer io.Writer var reader io.Reader switch security { case SecurityNone: reader = newChunkReader(conn) writer = newChunkWriter(conn) case SecurityAES128GCM: block, _ := aes.NewCipher(reqBodyKey[:]) aead, _ := cipher.NewGCM(block) writer = newAEADWriter(conn, aead, reqBodyIV[:]) block, _ = aes.NewCipher(respBodyKey[:]) aead, _ = cipher.NewGCM(block) reader = newAEADReader(conn, aead, respBodyIV[:]) case SecurityCHACHA20POLY1305: key := make([]byte, 32) t := md5.Sum(reqBodyKey[:]) copy(key, t[:]) t = md5.Sum(key[:16]) copy(key[16:], t[:]) aead, _ := chacha20poly1305.New(key) writer = newAEADWriter(conn, aead, reqBodyIV[:]) t = md5.Sum(respBodyKey[:]) copy(key, t[:]) t = md5.Sum(key[:16]) copy(key[16:], t[:]) aead, _ = chacha20poly1305.New(key) reader = newAEADReader(conn, aead, respBodyIV[:]) } c := &Conn{ Conn: conn, id: id, dst: dst, reqBodyIV: reqBodyIV, reqBodyKey: reqBodyKey, respV: respV, respBodyIV: respBodyIV[:], respBodyKey: respBodyKey[:], reader: reader, writer: writer, security: security, isAead: isAead, } if err := c.sendRequest(); err != nil { return nil, err } return c, nil } ================================================ FILE: core/Clash.Meta/transport/vmess/h2.go ================================================ package vmess import ( "context" "io" "net" "net/url" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/http" "github.com/metacubex/randv2" ) type h2Conn struct { net.Conn *http.ClientConn pwriter *io.PipeWriter res *http.Response cfg *H2Config } type H2Config struct { Hosts []string Path string } func (hc *h2Conn) establishConn() error { preader, pwriter := io.Pipe() host := hc.cfg.Hosts[randv2.IntN(len(hc.cfg.Hosts))] path := hc.cfg.Path // TODO: connect use VMess Host instead of H2 Host req := http.Request{ Method: "PUT", Host: host, URL: &url.URL{ Scheme: "https", Host: host, Path: path, }, Proto: "HTTP/2", ProtoMajor: 2, ProtoMinor: 0, Body: preader, Header: map[string][]string{ "Accept-Encoding": {"identity"}, }, } // it will be close at : `func (hc *h2Conn) Close() error` res, err := hc.ClientConn.RoundTrip(&req) if err != nil { return err } hc.pwriter = pwriter hc.res = res return nil } // Read implements net.Conn.Read() func (hc *h2Conn) Read(b []byte) (int, error) { if hc.res != nil && !hc.res.Close { n, err := hc.res.Body.Read(b) return n, err } if err := hc.establishConn(); err != nil { return 0, err } return hc.res.Body.Read(b) } // Write implements io.Writer. func (hc *h2Conn) Write(b []byte) (int, error) { if hc.pwriter != nil { return hc.pwriter.Write(b) } if err := hc.establishConn(); err != nil { return 0, err } return hc.pwriter.Write(b) } func (hc *h2Conn) Close() error { if hc.pwriter != nil { if err := hc.pwriter.Close(); err != nil { return err } } return hc.Conn.Close() } func StreamH2Conn(ctx context.Context, conn net.Conn, cfg *H2Config) (_ net.Conn, err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, conn) defer done(&err) } // use h2c mode to disallow the net/http fallback to http1.1 protocols := new(http.Protocols) protocols.SetUnencryptedHTTP2(true) transport := &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil }, Protocols: protocols, } clientConn, err := transport.NewClientConn(ctx, "https", ":0") if err != nil { return nil, err } return &h2Conn{ Conn: conn, ClientConn: clientConn, cfg: cfg, }, nil } ================================================ FILE: core/Clash.Meta/transport/vmess/header.go ================================================ package vmess import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/binary" "hash" "hash/crc32" "time" ) const ( kdfSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption" kdfSaltConstAEADRespHeaderLenKey = "AEAD Resp Header Len Key" kdfSaltConstAEADRespHeaderLenIV = "AEAD Resp Header Len IV" kdfSaltConstAEADRespHeaderPayloadKey = "AEAD Resp Header Key" kdfSaltConstAEADRespHeaderPayloadIV = "AEAD Resp Header IV" kdfSaltConstVMessAEADKDF = "VMess AEAD KDF" kdfSaltConstVMessHeaderPayloadAEADKey = "VMess Header AEAD Key" kdfSaltConstVMessHeaderPayloadAEADIV = "VMess Header AEAD Nonce" kdfSaltConstVMessHeaderPayloadLengthAEADKey = "VMess Header AEAD Key_Length" kdfSaltConstVMessHeaderPayloadLengthAEADIV = "VMess Header AEAD Nonce_Length" ) func kdf(key []byte, path ...string) []byte { hmacCreator := &hMacCreator{value: []byte(kdfSaltConstVMessAEADKDF)} for _, v := range path { hmacCreator = &hMacCreator{value: []byte(v), parent: hmacCreator} } hmacf := hmacCreator.Create() hmacf.Write(key) return hmacf.Sum(nil) } type hMacCreator struct { parent *hMacCreator value []byte } func (h *hMacCreator) Create() hash.Hash { if h.parent == nil { return hmac.New(sha256.New, h.value) } return hmac.New(h.parent.Create, h.value) } func createAuthID(cmdKey []byte, time int64) [16]byte { buf := &bytes.Buffer{} binary.Write(buf, binary.BigEndian, time) random := make([]byte, 4) rand.Read(random) buf.Write(random) zero := crc32.ChecksumIEEE(buf.Bytes()) binary.Write(buf, binary.BigEndian, zero) aesBlock, _ := aes.NewCipher(kdf(cmdKey[:], kdfSaltConstAuthIDEncryptionKey)[:16]) var result [16]byte aesBlock.Encrypt(result[:], buf.Bytes()) return result } func sealVMessAEADHeader(key [16]byte, data []byte, t time.Time) []byte { generatedAuthID := createAuthID(key[:], t.Unix()) connectionNonce := make([]byte, 8) rand.Read(connectionNonce) aeadPayloadLengthSerializedByte := make([]byte, 2) binary.BigEndian.PutUint16(aeadPayloadLengthSerializedByte, uint16(len(data))) var payloadHeaderLengthAEADEncrypted []byte { payloadHeaderLengthAEADKey := kdf(key[:], kdfSaltConstVMessHeaderPayloadLengthAEADKey, string(generatedAuthID[:]), string(connectionNonce))[:16] payloadHeaderLengthAEADNonce := kdf(key[:], kdfSaltConstVMessHeaderPayloadLengthAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] payloadHeaderLengthAEADAESBlock, _ := aes.NewCipher(payloadHeaderLengthAEADKey) payloadHeaderAEAD, _ := cipher.NewGCM(payloadHeaderLengthAEADAESBlock) payloadHeaderLengthAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderLengthAEADNonce, aeadPayloadLengthSerializedByte, generatedAuthID[:]) } var payloadHeaderAEADEncrypted []byte { payloadHeaderAEADKey := kdf(key[:], kdfSaltConstVMessHeaderPayloadAEADKey, string(generatedAuthID[:]), string(connectionNonce))[:16] payloadHeaderAEADNonce := kdf(key[:], kdfSaltConstVMessHeaderPayloadAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] payloadHeaderAEADAESBlock, _ := aes.NewCipher(payloadHeaderAEADKey) payloadHeaderAEAD, _ := cipher.NewGCM(payloadHeaderAEADAESBlock) payloadHeaderAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderAEADNonce, data, generatedAuthID[:]) } outputBuffer := &bytes.Buffer{} outputBuffer.Write(generatedAuthID[:]) outputBuffer.Write(payloadHeaderLengthAEADEncrypted) outputBuffer.Write(connectionNonce) outputBuffer.Write(payloadHeaderAEADEncrypted) return outputBuffer.Bytes() } ================================================ FILE: core/Clash.Meta/transport/vmess/http.go ================================================ package vmess import ( "bufio" "bytes" "io" "net" "net/textproto" "net/url" "github.com/metacubex/http" "github.com/metacubex/randv2" ) type httpConn struct { net.Conn cfg *HTTPConfig reader *bufio.Reader whandshake bool } type HTTPConfig struct { Method string Host string Path []string Headers map[string][]string } // Read implements net.Conn.Read() func (hc *httpConn) Read(b []byte) (int, error) { if hc.reader != nil { n, err := hc.reader.Read(b) return n, err } reader := textproto.NewConn(hc.Conn) // First line: GET /index.html HTTP/1.0 if _, err := reader.ReadLine(); err != nil { return 0, err } if _, err := reader.ReadMIMEHeader(); err != nil { return 0, err } hc.reader = reader.R return reader.R.Read(b) } // Write implements io.Writer. func (hc *httpConn) Write(b []byte) (int, error) { if hc.whandshake { return hc.Conn.Write(b) } path := "/" if len(hc.cfg.Path) > 0 { path = hc.cfg.Path[randv2.IntN(len(hc.cfg.Path))] } host := hc.cfg.Host if header := hc.cfg.Headers["Host"]; len(header) != 0 { host = header[randv2.IntN(len(header))] } req := http.Request{ Method: hc.cfg.Method, // default is GET Host: host, URL: &url.URL{Scheme: "http", Host: host, Path: path}, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(b)), } for key, list := range hc.cfg.Headers { req.Header.Set(key, list[randv2.IntN(len(list))]) } req.ContentLength = int64(len(b)) if err := req.Write(hc.Conn); err != nil { return 0, err } hc.whandshake = true return len(b), nil } func (hc *httpConn) Close() error { return hc.Conn.Close() } func StreamHTTPConn(conn net.Conn, cfg *HTTPConfig) net.Conn { return &httpConn{ Conn: conn, cfg: cfg, } } ================================================ FILE: core/Clash.Meta/transport/vmess/tls.go ================================================ package vmess import ( "context" "errors" "net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/tls" ) type TLSConfig struct { Host string SkipCertVerify bool FingerPrint string Certificate string PrivateKey string ClientFingerprint string NextProtos []string ECH *ech.Config Reality *tlsC.RealityConfig } func (cfg *TLSConfig) ToStdConfig() (*tls.Config, error) { return ca.GetTLSConfig(ca.Option{ TLSConfig: &tls.Config{ ServerName: cfg.Host, InsecureSkipVerify: cfg.SkipCertVerify, NextProtos: cfg.NextProtos, }, Fingerprint: cfg.FingerPrint, Certificate: cfg.Certificate, PrivateKey: cfg.PrivateKey, }) } func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) { tlsConfig, err := cfg.ToStdConfig() if err != nil { return nil, err } if clientFingerprint, ok := tlsC.GetFingerprint(cfg.ClientFingerprint); ok { if cfg.Reality != nil { return tlsC.GetRealityConn(ctx, conn, clientFingerprint, tlsConfig.ServerName, cfg.Reality) } tlsConfig := tlsC.UConfig(tlsConfig) err = cfg.ECH.ClientHandleUTLS(ctx, tlsConfig) if err != nil { return nil, err } tlsConn := tlsC.UClient(conn, tlsConfig, clientFingerprint) err = tlsConn.HandshakeContext(ctx) if err != nil { return nil, err } return tlsConn, nil } if cfg.Reality != nil { return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") } err = cfg.ECH.ClientHandle(ctx, tlsConfig) if err != nil { return nil, err } tlsConn := tls.Client(conn, tlsConfig) err = tlsConn.HandshakeContext(ctx) return tlsConn, err } ================================================ FILE: core/Clash.Meta/transport/vmess/user.go ================================================ package vmess import ( "bytes" "crypto/md5" "github.com/gofrs/uuid/v5" ) // ID cmdKey length const ( IDBytesLen = 16 ) // The ID of en entity, in the form of a UUID. type ID struct { UUID *uuid.UUID CmdKey []byte } // newID returns an ID with given UUID. func newID(uuid *uuid.UUID) *ID { id := &ID{UUID: uuid, CmdKey: make([]byte, IDBytesLen)} md5hash := md5.New() md5hash.Write(uuid.Bytes()) md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21")) md5hash.Sum(id.CmdKey[:0]) return id } func nextID(u *uuid.UUID) *uuid.UUID { md5hash := md5.New() md5hash.Write(u.Bytes()) md5hash.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81")) var newid uuid.UUID for { md5hash.Sum(newid[:0]) if !bytes.Equal(newid.Bytes(), u.Bytes()) { return &newid } md5hash.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2")) } } func newAlterIDs(primary *ID, alterIDCount uint16) []*ID { alterIDs := make([]*ID, alterIDCount) prevID := primary.UUID for idx := range alterIDs { newid := nextID(prevID) alterIDs[idx] = &ID{UUID: newid, CmdKey: primary.CmdKey[:]} prevID = newid } alterIDs = append(alterIDs, primary) return alterIDs } ================================================ FILE: core/Clash.Meta/transport/vmess/vmess.go ================================================ package vmess import ( "fmt" "net" "runtime" "github.com/metacubex/mihomo/common/utils" "github.com/gofrs/uuid/v5" "github.com/metacubex/randv2" ) // Version of vmess const Version byte = 1 // Request Options const ( OptionChunkStream byte = 1 OptionChunkMasking byte = 4 ) // Security type vmess type Security = byte // Cipher types const ( SecurityAES128GCM Security = 3 SecurityCHACHA20POLY1305 Security = 4 SecurityNone Security = 5 ) // CipherMapping return var CipherMapping = map[string]byte{ "none": SecurityNone, "aes-128-gcm": SecurityAES128GCM, "chacha20-poly1305": SecurityCHACHA20POLY1305, } // Command types const ( CommandTCP byte = 1 CommandUDP byte = 2 ) // Addr types const ( AtypIPv4 byte = 1 AtypDomainName byte = 2 AtypIPv6 byte = 3 ) // DstAddr store destination address type DstAddr struct { UDP bool AddrType byte Addr []byte Port uint } // Client is vmess connection generator type Client struct { user []*ID uuid *uuid.UUID security Security isAead bool } // Config of vmess type Config struct { UUID string AlterID uint16 Security string Port string HostName string IsAead bool } // StreamConn return a Conn with net.Conn and DstAddr func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { r := randv2.IntN(len(c.user)) return newConn(conn, c.user[r], dst, c.security, c.isAead) } // NewClient return Client instance func NewClient(config Config) (*Client, error) { uid := utils.UUIDMap(config.UUID) var security Security switch config.Security { case "aes-128-gcm": security = SecurityAES128GCM case "chacha20-poly1305": security = SecurityCHACHA20POLY1305 case "none": security = SecurityNone case "auto": security = SecurityCHACHA20POLY1305 if runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x" || runtime.GOARCH == "arm64" { security = SecurityAES128GCM } default: return nil, fmt.Errorf("unknown security type: %s", config.Security) } return &Client{ user: newAlterIDs(newID(&uid), config.AlterID), uuid: &uid, security: security, isAead: config.IsAead, }, nil } ================================================ FILE: core/Clash.Meta/transport/vmess/websocket.go ================================================ package vmess import ( "bufio" "bytes" "context" "crypto/rand" "encoding/base64" "encoding/binary" "errors" "fmt" "io" "net" "net/url" "strconv" "strings" "time" "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/log" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" "github.com/metacubex/http" "github.com/metacubex/randv2" "github.com/metacubex/tls" ) type websocketConn struct { net.Conn state ws.State reader *wsutil.Reader controlHandler wsutil.FrameHandlerFunc rawWriter N.ExtendedWriter } type websocketWithEarlyDataConn struct { conn N.ExtendedConn underlay net.Conn dialed chan struct{} cancel context.CancelFunc ctx context.Context config *WebsocketConfig } type WebsocketConfig struct { Host string Port string Path string Headers http.Header TLS bool TLSConfig *tls.Config ECHConfig *ech.Config MaxEarlyData int EarlyDataHeaderName string ClientFingerprint string V2rayHttpUpgrade bool V2rayHttpUpgradeFastOpen bool } // Read implements net.Conn.Read() // modify from gobwas/ws/wsutil.readData func (wsc *websocketConn) Read(b []byte) (n int, err error) { defer func() { // avoid gobwas/ws pbytes.GetLen panic if value := recover(); value != nil { err = fmt.Errorf("websocket error: %s", value) } }() var header ws.Header for { n, err = wsc.reader.Read(b) // in gobwas/ws: "The error is io.EOF only if all of message bytes were read." // but maybe next frame still have data, so drop it if errors.Is(err, io.EOF) { err = nil } if !errors.Is(err, wsutil.ErrNoFrameAdvance) { return } header, err = wsc.reader.NextFrame() if err != nil { return } if header.OpCode.IsControl() { err = wsc.controlHandler(header, wsc.reader) if err != nil { return } continue } if header.OpCode&(ws.OpBinary|ws.OpText) == 0 { err = wsc.reader.Discard() if err != nil { return } continue } } } // Write implements io.Writer. func (wsc *websocketConn) Write(b []byte) (n int, err error) { err = wsutil.WriteMessage(wsc.Conn, wsc.state, ws.OpBinary, b) if err != nil { return } n = len(b) return } func (wsc *websocketConn) 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 wsc.state.ClientSide() { headerLen += 4 // MASK KEY } header := buffer.ExtendHeader(headerLen) header[0] = byte(ws.OpBinary) | 0x80 if wsc.state.ClientSide() { header[1] = 1 << 7 } else { header[1] = 0 } 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 wsc.state.ClientSide() { maskKey := randv2.Uint32() binary.LittleEndian.PutUint32(header[1+payloadBitLength:], maskKey) N.MaskWebSocket(maskKey, data) } return wsc.rawWriter.WriteBuffer(buffer) } func (wsc *websocketConn) FrontHeadroom() int { return 14 } func (wsc *websocketConn) Upstream() any { return wsc.Conn } func (wsc *websocketConn) Close() error { _ = wsc.Conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) _ = wsutil.WriteMessage(wsc.Conn, wsc.state, ws.OpClose, ws.NewCloseFrameBody(ws.StatusNormalClosure, "")) _ = wsc.Conn.Close() return nil } func (wsedc *websocketWithEarlyDataConn) dial(earlyData []byte) error { base64DataBuf := &bytes.Buffer{} base64EarlyDataEncoder := base64.NewEncoder(base64.RawURLEncoding, base64DataBuf) earlyDataBuf := bytes.NewBuffer(earlyData) if _, err := base64EarlyDataEncoder.Write(earlyDataBuf.Next(wsedc.config.MaxEarlyData)); err != nil { return fmt.Errorf("failed to encode early data: %w", err) } if err := base64EarlyDataEncoder.Close(); err != nil { return fmt.Errorf("failed to encode early data tail: %w", err) } conn, err := streamWebsocketConn(wsedc.ctx, wsedc.underlay, wsedc.config, base64DataBuf) if err != nil { _ = wsedc.Close() return fmt.Errorf("failed to dial WebSocket: %w", err) } wsedc.conn = N.NewExtendedConn(conn) close(wsedc.dialed) if earlyDataBuf.Len() != 0 { _, err = wsedc.conn.Write(earlyDataBuf.Bytes()) } return err } func (wsedc *websocketWithEarlyDataConn) Write(b []byte) (int, error) { select { case <-wsedc.ctx.Done(): return 0, io.ErrClosedPipe case <-wsedc.dialed: return wsedc.conn.Write(b) default: if err := wsedc.dial(b); err != nil { return 0, err } return len(b), nil } } func (wsedc *websocketWithEarlyDataConn) WriteBuffer(buffer *buf.Buffer) error { select { case <-wsedc.ctx.Done(): return io.ErrClosedPipe case <-wsedc.dialed: return wsedc.conn.WriteBuffer(buffer) default: if err := wsedc.dial(buffer.Bytes()); err != nil { return err } return nil } } func (wsedc *websocketWithEarlyDataConn) Read(b []byte) (int, error) { select { case <-wsedc.ctx.Done(): return 0, io.ErrClosedPipe case <-wsedc.dialed: return wsedc.conn.Read(b) } } func (wsedc *websocketWithEarlyDataConn) Close() error { wsedc.cancel() select { case <-wsedc.dialed: return wsedc.conn.Close() default: return wsedc.underlay.Close() } } func (wsedc *websocketWithEarlyDataConn) LocalAddr() net.Addr { select { case <-wsedc.dialed: return wsedc.conn.LocalAddr() default: return wsedc.underlay.LocalAddr() } } func (wsedc *websocketWithEarlyDataConn) RemoteAddr() net.Addr { select { case <-wsedc.dialed: return wsedc.conn.RemoteAddr() default: return wsedc.underlay.RemoteAddr() } } func (wsedc *websocketWithEarlyDataConn) SetDeadline(t time.Time) error { if err := wsedc.SetReadDeadline(t); err != nil { return err } return wsedc.SetWriteDeadline(t) } func (wsedc *websocketWithEarlyDataConn) SetReadDeadline(t time.Time) error { select { case <-wsedc.dialed: return wsedc.conn.SetReadDeadline(t) default: return nil } } func (wsedc *websocketWithEarlyDataConn) SetWriteDeadline(t time.Time) error { select { case <-wsedc.dialed: return wsedc.conn.SetReadDeadline(t) default: return nil } } func (wsedc *websocketWithEarlyDataConn) FrontHeadroom() int { return 14 } func (wsedc *websocketWithEarlyDataConn) Upstream() any { return wsedc.underlay } //func (wsedc *websocketWithEarlyDataConn) LazyHeadroom() bool { // return wsedc.Conn == nil //} // //func (wsedc *websocketWithEarlyDataConn) Upstream() any { // if wsedc.Conn == nil { // ensure return a nil interface not an interface with nil value // return nil // } // return wsedc.Conn //} func (wsedc *websocketWithEarlyDataConn) NeedHandshake() bool { select { case <-wsedc.dialed: return false default: return true } } func streamWebsocketWithEarlyDataConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) { ctx, cancel := context.WithCancel(context.Background()) conn = &websocketWithEarlyDataConn{ dialed: make(chan struct{}), cancel: cancel, ctx: ctx, underlay: conn, config: c, } // websocketWithEarlyDataConn can't correct handle Deadline // it will not apply the already set Deadline after Dial() // so call N.NewDeadlineConn to add a safe wrapper return N.NewDeadlineConn(conn), nil } func streamWebsocketConn(ctx context.Context, conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buffer) (_ net.Conn, err error) { u, err := url.Parse(c.Path) if err != nil { return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) } uri := url.URL{ Scheme: "ws", Host: net.JoinHostPort(c.Host, c.Port), Path: u.Path, RawQuery: u.RawQuery, } if !strings.HasPrefix(uri.Path, "/") { uri.Path = "/" + uri.Path } if c.TLS { uri.Scheme = "wss" config := c.TLSConfig if config == nil { // The config cannot be nil config = &tls.Config{NextProtos: []string{"http/1.1"}} } if config.ServerName == "" && !config.InsecureSkipVerify { // users must set either ServerName or InsecureSkipVerify in the config. config = config.Clone() config.ServerName = c.Host } if clientFingerprint, ok := tlsC.GetFingerprint(c.ClientFingerprint); ok { tlsConfig := tlsC.UConfig(config) err = c.ECHConfig.ClientHandleUTLS(ctx, tlsConfig) if err != nil { return nil, err } tlsConn := tlsC.UClient(conn, tlsConfig, clientFingerprint) if err = tlsC.BuildWebsocketHandshakeState(tlsConn); err != nil { return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) } err = tlsConn.HandshakeContext(ctx) if err != nil { return nil, err } conn = tlsConn } else { err = c.ECHConfig.ClientHandle(ctx, config) if err != nil { return nil, err } tlsConn := tls.Client(conn, config) err = tlsConn.HandshakeContext(ctx) if err != nil { return nil, err } conn = tlsConn } } request := &http.Request{ Method: http.MethodGet, URL: &uri, Header: c.Headers.Clone(), Host: c.Host, } request.Header.Set("Connection", "Upgrade") request.Header.Set("Upgrade", "websocket") if host := request.Header.Get("Host"); host != "" { // For client requests, Host optionally overrides the Host // header to send. If empty, the Request.Write method uses // the value of URL.Host. Host may contain an international // domain name. request.Host = host } request.Header.Del("Host") var secKey string if !c.V2rayHttpUpgrade { const nonceKeySize = 16 // NOTE: bts does not escape. bts := make([]byte, nonceKeySize) if _, err = rand.Read(bts); err != nil { return nil, fmt.Errorf("rand read error: %w", err) } secKey = base64.StdEncoding.EncodeToString(bts) request.Header.Set("Sec-WebSocket-Version", "13") request.Header.Set("Sec-WebSocket-Key", secKey) } if earlyData != nil { earlyDataString := earlyData.String() if c.EarlyDataHeaderName == "" { uri.Path += earlyDataString } else { request.Header.Set(c.EarlyDataHeaderName, earlyDataString) } } if ctx.Done() != nil { done := N.SetupContextForConn(ctx, conn) defer done(&err) } err = request.Write(conn) if err != nil { return nil, err } bufferedConn := N.NewBufferedConn(conn) if c.V2rayHttpUpgrade && c.V2rayHttpUpgradeFastOpen { return N.NewEarlyConn(bufferedConn, func() error { response, err := http.ReadResponse(bufferedConn.Reader(), request) if err != nil { return err } if response.StatusCode != http.StatusSwitchingProtocols || !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { return fmt.Errorf("unexpected status: %s", response.Status) } return nil }), nil } response, err := http.ReadResponse(bufferedConn.Reader(), request) if err != nil { return nil, err } if response.StatusCode != http.StatusSwitchingProtocols || !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { return nil, fmt.Errorf("unexpected status: %s", response.Status) } if c.V2rayHttpUpgrade { return bufferedConn, nil } if log.Level() == log.DEBUG { // we might not check this for performance secAccept := response.Header.Get("Sec-Websocket-Accept") const acceptSize = 28 // base64.StdEncoding.EncodedLen(sha1.Size) if lenSecAccept := len(secAccept); lenSecAccept != acceptSize { return nil, fmt.Errorf("unexpected Sec-Websocket-Accept length: %d", lenSecAccept) } if N.GetWebSocketSecAccept(secKey) != secAccept { return nil, errors.New("unexpected Sec-Websocket-Accept") } } conn = newWebsocketConn(bufferedConn, ws.StateClientSide) // websocketConn can't correct handle ReadDeadline // so call N.NewDeadlineConn to add a safe wrapper return N.NewDeadlineConn(conn), nil } func StreamWebsocketConn(ctx context.Context, conn net.Conn, c *WebsocketConfig) (net.Conn, error) { if u, err := url.Parse(c.Path); err == nil { if q := u.Query(); q.Get("ed") != "" { if ed, err := strconv.Atoi(q.Get("ed")); err == nil { c.MaxEarlyData = ed c.EarlyDataHeaderName = "Sec-WebSocket-Protocol" q.Del("ed") u.RawQuery = q.Encode() c.Path = u.String() } } } if c.MaxEarlyData > 0 { return streamWebsocketWithEarlyDataConn(conn, c) } return streamWebsocketConn(ctx, conn, c, nil) } func newWebsocketConn(conn net.Conn, state ws.State) *websocketConn { controlHandler := wsutil.ControlFrameHandler(conn, state) return &websocketConn{ Conn: conn, state: state, reader: &wsutil.Reader{ Source: conn, State: state, SkipHeaderCheck: true, CheckUTF8: false, OnIntermediate: controlHandler, }, controlHandler: controlHandler, rawWriter: N.NewExtendedWriter(conn), } } var replacer = strings.NewReplacer("+", "-", "/", "_", "=", "") func decodeEd(s string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(replacer.Replace(s)) } func decodeXray0rtt(requestHeader http.Header) []byte { // read inHeader's `Sec-WebSocket-Protocol` for Xray's 0rtt ws if secProtocol := requestHeader.Get("Sec-WebSocket-Protocol"); len(secProtocol) > 0 { if edBuf, err := decodeEd(secProtocol); err == nil { // sure could base64 decode return edBuf } } return nil } func IsWebSocketUpgrade(r *http.Request) bool { return r.Header.Get("Upgrade") == "websocket" } func IsV2rayHttpUpdate(r *http.Request) bool { return IsWebSocketUpgrade(r) && r.Header.Get("Sec-WebSocket-Key") == "" } func StreamUpgradedWebsocketConn(w http.ResponseWriter, r *http.Request) (net.Conn, error) { var conn net.Conn var rw *bufio.ReadWriter var err error isRaw := IsV2rayHttpUpdate(r) w.Header().Set("Connection", "upgrade") w.Header().Set("Upgrade", "websocket") if !isRaw { w.Header().Set("Sec-Websocket-Accept", N.GetWebSocketSecAccept(r.Header.Get("Sec-WebSocket-Key"))) } w.WriteHeader(http.StatusSwitchingProtocols) if flusher, isFlusher := w.(interface{ FlushError() error }); isFlusher && writeHeaderShouldFlush { err = flusher.FlushError() if err != nil { return nil, fmt.Errorf("flush response: %w", err) } } hijacker, canHijack := w.(http.Hijacker) if !canHijack { return nil, errors.New("invalid connection, maybe HTTP/2") } conn, rw, err = hijacker.Hijack() if err != nil { return nil, fmt.Errorf("hijack failed: %w", err) } // rw.Writer was flushed, so we only need warp rw.Reader conn = N.WarpConnWithBioReader(conn, rw.Reader) if !isRaw { conn = newWebsocketConn(conn, ws.StateServerSide) // websocketConn can't correct handle ReadDeadline // so call N.NewDeadlineConn to add a safe wrapper conn = N.NewDeadlineConn(conn) } if edBuf := decodeXray0rtt(r.Header); len(edBuf) > 0 { appendOk := false if bufConn, ok := conn.(*N.BufferedConn); ok { appendOk = bufConn.AppendData(edBuf) } if !appendOk { conn = N.NewCachedConn(conn, edBuf) } } return conn, nil } ================================================ FILE: core/Clash.Meta/transport/vmess/websocket_go120.go ================================================ //go:build !go1.21 package vmess // Golang1.20's net.http Flush will mistakenly call w.WriteHeader(StatusOK) internally after w.WriteHeader(http.StatusSwitchingProtocols) // https://github.com/golang/go/issues/59564 const writeHeaderShouldFlush = false ================================================ FILE: core/Clash.Meta/transport/vmess/websocket_go121.go ================================================ //go:build go1.21 package vmess const writeHeaderShouldFlush = true ================================================ FILE: core/Clash.Meta/transport/xhttp/browser.go ================================================ package xhttp import ( "math" "strconv" "strings" "time" "github.com/metacubex/http" "github.com/metacubex/randv2" ) // The Chrome version generator will suffer from deviation of a normal distribution. func ChromeVersion() int { // Start from Chrome 144, released on 2026.1.13. var startVersion int = 144 var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400 var timeCurrent int64 = time.Now().Unix() / 86400 var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(randv2.Float64(), 2)*105)) return startVersion + (timeDiff / 35) // It's 31.15 currently. } var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6} // The following version generators use deterministic generators, but with the distribution scaled by a curve. func CurlVersion() string { // curl 8.0.0 was released on 20/03/2023. var timeCurrent int64 = time.Now().Unix() / 86400 var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400 var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(randv2.Float64(), 2)*165)) var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days. return "8." + strconv.Itoa(minorValue) + ".0" } func FirefoxVersion() int { // Firefox 128 ESR was released on 09/07/2023. var timeCurrent int64 = time.Now().Unix() / 86400 var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400 var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(randv2.Float64(), 2)*50)) return int(timeDiff/30) + 128 } func SafariVersion() string { var anchoredTime time.Time = time.Now() var releaseYear int = anchoredTime.Year() var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) var delayedDays = int(math.Floor(math.Pow(randv2.Float64(), 3) * 75)) splitPoint = splitPoint.AddDate(0, 0, delayedDays) if anchoredTime.Compare(splitPoint) < 0 { releaseYear-- splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) splitPoint = splitPoint.AddDate(0, 0, delayedDays) } var minorVersion = safariMinorMap[(anchoredTime.Unix()-splitPoint.Unix())/1296000] return strconv.Itoa(releaseYear-1999) + "." + strconv.Itoa(minorVersion) } // The full Chromium brand GREASE implementation var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"} var clientHintVersionNA = []string{"8", "99", "24"} var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} var clientHintShuffle4 = [][4]int{ {0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1}, {1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0}, {2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0}, {3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}} func getGreasedChInvalidBrand(seed int) string { return "\"Not" + clientHintGreaseNA[seed%len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed+1)%len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed%len(clientHintVersionNA)] + "\"" } func getGreasedChOrder(brandLength int, seed int) []int { switch brandLength { case 1: return []int{0} case 2: return []int{seed % brandLength, (seed + 1) % brandLength} case 3: return clientHintShuffle3[seed%len(clientHintShuffle3)][:] default: return clientHintShuffle4[seed%len(clientHintShuffle4)][:] } //return []int{} } func getUngreasedChUa(majorVersion int, forkName string) []string { // Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice baseChUa := make([]string, 0, 4) baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion), "\"Chromium\";v=\""+strconv.Itoa(majorVersion)+"\"") switch forkName { case "chrome": baseChUa = append(baseChUa, "\"Google Chrome\";v=\""+strconv.Itoa(majorVersion)+"\"") case "edge": baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\""+strconv.Itoa(majorVersion)+"\"") } return baseChUa } func getGreasedChUa(majorVersion int, forkName string) string { ungreasedCh := getUngreasedChUa(majorVersion, forkName) shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion) shuffledCh := make([]string, len(ungreasedCh)) for i, e := range shuffleMap { shuffledCh[e] = ungreasedCh[i] } return strings.Join(shuffledCh, ", ") } // The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG. var CurlUA = "curl/" + CurlVersion() var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion()) var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0" var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15" // Chromium browsers. var AnchoredChromeVersion = ChromeVersion() var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36" var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome") var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0" var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge") func applyMasqueradedHeaders(header http.Header, browser string, variant string) { // Browser-specific. switch browser { case "chrome": header["Sec-CH-UA"] = []string{ChromeUACH} header["Sec-CH-UA-Mobile"] = []string{"?0"} header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} header["DNT"] = []string{"1"} header.Set("User-Agent", ChromeUA) header.Set("Accept-Language", "en-US,en;q=0.9") case "edge": header["Sec-CH-UA"] = []string{MSEdgeUACH} header["Sec-CH-UA-Mobile"] = []string{"?0"} header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} header["DNT"] = []string{"1"} header.Set("User-Agent", MSEdgeUA) header.Set("Accept-Language", "en-US,en;q=0.9") case "firefox": header.Set("User-Agent", FirefoxUA) header["DNT"] = []string{"1"} header.Set("Accept-Language", "en-US,en;q=0.5") case "safari": header.Set("User-Agent", SafariUA) header.Set("Accept-Language", "en-US,en;q=0.9") case "golang": // Expose the default net/http header. header.Del("User-Agent") return case "curl": header.Set("User-Agent", CurlUA) return } // Context-specific. switch variant { case "nav": if header.Get("Cache-Control") == "" { switch browser { case "chrome", "edge": header.Set("Cache-Control", "max-age=0") } } header.Set("Upgrade-Insecure-Requests", "1") if header.Get("Accept") == "" { switch browser { case "chrome", "edge": header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") case "firefox", "safari": header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") } } header.Set("Sec-Fetch-Site", "none") header.Set("Sec-Fetch-Mode", "navigate") switch browser { case "safari": default: header.Set("Sec-Fetch-User", "?1") } header.Set("Sec-Fetch-Dest", "document") header.Set("Priority", "u=0, i") case "ws": header.Set("Sec-Fetch-Mode", "websocket") switch browser { case "safari": // Safari is NOT web-compliant here! header.Set("Sec-Fetch-Dest", "websocket") default: header.Set("Sec-Fetch-Dest", "empty") } header.Set("Sec-Fetch-Site", "same-origin") if header.Get("Cache-Control") == "" { header.Set("Cache-Control", "no-cache") } if header.Get("Pragma") == "" { header.Set("Pragma", "no-cache") } if header.Get("Accept") == "" { header.Set("Accept", "*/*") } case "fetch": header.Set("Sec-Fetch-Mode", "cors") header.Set("Sec-Fetch-Dest", "empty") header.Set("Sec-Fetch-Site", "same-origin") if header.Get("Priority") == "" { switch browser { case "chrome", "edge": header.Set("Priority", "u=1, i") case "firefox": header.Set("Priority", "u=4") case "safari": header.Set("Priority", "u=3, i") } } if header.Get("Cache-Control") == "" { header.Set("Cache-Control", "no-cache") } if header.Get("Pragma") == "" { header.Set("Pragma", "no-cache") } if header.Get("Accept") == "" { header.Set("Accept", "*/*") } } } func TryDefaultHeadersWith(header http.Header, variant string) { // The global UA special value handler for transports. Used to be called HandleTransportUASettings. // Just a FYI to whoever needing to fix this piece of code after some spontaneous event, I tried to make the two methods separate to let the code be cleaner and more organized. if len(header.Values("User-Agent")) < 1 { applyMasqueradedHeaders(header, "chrome", variant) } else { switch header.Get("User-Agent") { case "chrome": applyMasqueradedHeaders(header, "chrome", variant) case "firefox": applyMasqueradedHeaders(header, "firefox", variant) case "safari": applyMasqueradedHeaders(header, "safari", variant) case "edge": applyMasqueradedHeaders(header, "edge", variant) case "curl": applyMasqueradedHeaders(header, "curl", variant) case "golang": applyMasqueradedHeaders(header, "golang", variant) } } } ================================================ FILE: core/Clash.Meta/transport/xhttp/client.go ================================================ package xhttp import ( "bytes" "context" "crypto/rand" "encoding/hex" "errors" "fmt" "io" "net" "net/url" "strconv" "sync" "time" "github.com/metacubex/mihomo/common/contextutils" "github.com/metacubex/mihomo/common/httputils" "github.com/metacubex/http" "github.com/metacubex/http/httptrace" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" "github.com/metacubex/tls" ) // ConnIdleTimeout defines the maximum time an idle TCP session can survive in the tunnel, // so it should be consistent across HTTP versions and with other transports. const ConnIdleTimeout = 300 * time.Second // QuicgoH3KeepAlivePeriod consistent with quic-go const QuicgoH3KeepAlivePeriod = 10 * time.Second // ChromeH2KeepAlivePeriod consistent with chrome const ChromeH2KeepAlivePeriod = 45 * time.Second type DialRawFunc func(ctx context.Context) (net.Conn, error) type WrapTLSFunc func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) type DialQUICFunc func(ctx context.Context, cfg *quic.Config) (*quic.Conn, error) type TransportMaker func() http.RoundTripper type PacketUpWriter struct { ctx context.Context cancel context.CancelFunc cfg *Config scMaxEachPostBytes int scMinPostsIntervalMs Range sessionID string transport http.RoundTripper writeMu sync.Mutex writeCond sync.Cond seq uint64 buf []byte timer *time.Timer flushErr error } func (c *PacketUpWriter) Write(b []byte) (int, error) { c.writeMu.Lock() defer c.writeMu.Unlock() if err := c.flushErr; err != nil { return 0, err } data := bytes.NewBuffer(b) for data.Len() > 0 { if c.timer == nil { // start a timer to flush the buffer c.timer = time.AfterFunc(time.Duration(c.scMinPostsIntervalMs.Rand())*time.Millisecond, c.flush) } c.buf = append(c.buf, data.Next(c.scMaxEachPostBytes-len(c.buf))...) // let buffer fill up to scMaxEachPostBytes if len(c.buf) >= c.scMaxEachPostBytes { // too much data in buffer, wait the flush complete c.writeCond.Wait() if err := c.flushErr; err != nil { return 0, err } } } return len(b), nil } func (c *PacketUpWriter) flush() { c.writeMu.Lock() defer c.writeMu.Unlock() defer c.writeCond.Broadcast() // wake up the waited Write() call if c.timer != nil { c.timer.Stop() c.timer = nil } if c.flushErr != nil { return } if len(c.buf) == 0 { return } _, err := c.write(c.buf) c.buf = c.buf[:0] // reset buffer if err != nil { c.flushErr = err return } } func (c *PacketUpWriter) write(b []byte) (int, error) { u := url.URL{ Scheme: "https", Host: c.cfg.Host, Path: c.cfg.NormalizedPath(), } req, err := http.NewRequestWithContext(c.ctx, c.cfg.GetNormalizedUplinkHTTPMethod(), u.String(), nil) if err != nil { return 0, err } seqStr := strconv.FormatUint(c.seq, 10) c.seq++ if err := c.cfg.FillPacketRequest(req, c.sessionID, seqStr, b); err != nil { return 0, err } req.Host = c.cfg.Host resp, err := c.transport.RoundTrip(req) if err != nil { return 0, err } defer resp.Body.Close() _, _ = io.Copy(io.Discard, resp.Body) if resp.StatusCode != http.StatusOK { return 0, fmt.Errorf("xhttp packet-up bad status: %s", resp.Status) } return len(b), nil } func (c *PacketUpWriter) Close() error { ch := make(chan struct{}) go func() { // flush in the background defer close(ch) c.flush() }() select { case <-ch: case <-time.After(time.Second): } c.cancel() httputils.CloseTransport(c.transport) return nil } func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc, dialQUIC DialQUICFunc, alpn []string, keepAlivePeriod time.Duration) http.RoundTripper { if len(alpn) == 1 && alpn[0] == "h3" { // `alpn: [h3]` means using h3 mode if keepAlivePeriod == 0 { keepAlivePeriod = QuicgoH3KeepAlivePeriod } if keepAlivePeriod < 0 { keepAlivePeriod = 0 } return &http3.Transport{ QUICConfig: &quic.Config{ MaxIncomingStreams: -1, // don't allow the server to create bidirectional streams KeepAlivePeriod: keepAlivePeriod, MaxIdleTimeout: ConnIdleTimeout, }, Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { return dialQUIC(ctx, cfg) }, } } if len(alpn) == 1 && alpn[0] == "http/1.1" { // `alpn: [http/1.1]` means using http/1.1 mode dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { raw, err := dialRaw(ctx) if err != nil { return nil, err } wrapped, err := wrapTLS(ctx, raw, false) if err != nil { _ = raw.Close() return nil, err } return wrapped, nil } return &http.Transport{ DialContext: dialContext, DialTLSContext: dialContext, IdleConnTimeout: ConnIdleTimeout, ForceAttemptHTTP2: false, // only http/1.1 } } if keepAlivePeriod == 0 { keepAlivePeriod = ChromeH2KeepAlivePeriod } if keepAlivePeriod < 0 { keepAlivePeriod = 0 } // use h2c mode to disallow the net/http fallback to http1.1 protocols := new(http.Protocols) protocols.SetUnencryptedHTTP2(true) return &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { raw, err := dialRaw(ctx) if err != nil { return nil, err } wrapped, err := wrapTLS(ctx, raw, true) if err != nil { _ = raw.Close() return nil, err } return wrapped, nil }, IdleConnTimeout: ConnIdleTimeout, Protocols: protocols, HTTP2: &http.HTTP2Config{ SendPingTimeout: keepAlivePeriod, }, } } type Client struct { ctx context.Context cancel context.CancelFunc mode string cfg *Config scMaxEachPostBytes Range scMinPostsIntervalMs Range makeTransport TransportMaker makeDownloadTransport TransportMaker uploadManager *ReuseManager downloadManager *ReuseManager } func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport TransportMaker, hasReality bool) (*Client, error) { mode := cfg.EffectiveMode(hasReality) switch mode { case "stream-one", "stream-up", "packet-up": default: return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode) } scMaxEachPostBytes, err := cfg.GetNormalizedScMaxEachPostBytes() if err != nil { return nil, err } scMinPostsIntervalMs, err := cfg.GetNormalizedScMinPostsIntervalMs() if err != nil { return nil, err } ctx, cancel := context.WithCancel(context.Background()) client := &Client{ mode: mode, cfg: cfg, scMaxEachPostBytes: scMaxEachPostBytes, scMinPostsIntervalMs: scMinPostsIntervalMs, makeTransport: makeTransport, makeDownloadTransport: makeDownloadTransport, ctx: ctx, cancel: cancel, } if cfg.ReuseConfig != nil { client.uploadManager, err = NewReuseManager(cfg.ReuseConfig, makeTransport) if err != nil { return nil, err } client.makeTransport = client.uploadManager.GetTransport if cfg.DownloadConfig != nil { if makeDownloadTransport == nil { return nil, fmt.Errorf("xhttp: download manager requires download transport maker") } client.downloadManager, err = NewReuseManager(cfg.DownloadConfig.ReuseConfig, makeDownloadTransport) if err != nil { return nil, err } client.makeDownloadTransport = client.downloadManager.GetTransport } } return client, nil } func (c *Client) Close() error { c.cancel() var errs []error if c.uploadManager != nil { err := c.uploadManager.Close() if err != nil { errs = append(errs, err) } } if c.downloadManager != nil { err := c.downloadManager.Close() if err != nil { errs = append(errs, err) } } return errors.Join(errs...) } func (c *Client) Dial(ctx context.Context) (net.Conn, error) { switch c.mode { case "stream-one": return c.DialStreamOne(ctx) case "stream-up": return c.DialStreamUp(ctx) case "packet-up": return c.DialPacketUp(ctx) default: return nil, fmt.Errorf("xhttp mode %s is not implemented yet", c.mode) } } // onlyRoundTripper is a wrapper that prevents the underlying transport from being closed. type onlyRoundTripper struct { http.RoundTripper } func (c *Client) getTransport() (uploadTransport http.RoundTripper, downloadTransport http.RoundTripper, err error) { uploadTransport = c.makeTransport() downloadTransport = onlyRoundTripper{uploadTransport} if c.makeDownloadTransport != nil { downloadTransport = c.makeDownloadTransport() } return } func (c *Client) DialStreamOne(ctx context.Context) (net.Conn, error) { transport, _, err := c.getTransport() if err != nil { return nil, err } requestURL := url.URL{ Scheme: "https", Host: c.cfg.Host, Path: c.cfg.NormalizedPath(), } pr, pw := io.Pipe() conn := &Conn{writer: pw} // Use gotConn to detect when TCP connection is established, so we can // return the conn immediately without waiting for the HTTP response. // This breaks the deadlock where CDN buffers response headers until the // server sends body data, but the server waits for our request body, // which can't be sent because we haven't returned the conn yet. gotConn := make(chan bool, 1) reqCtx, reqCancel := context.WithCancel(c.ctx) // reqCtx must alive during conn not closed stop := contextutils.AfterFunc(ctx, reqCancel) // temporarily connect ctx with reqCtx when dialing defer stop() // disconnect ctx with reqCtx after dialing addrCtx := httputils.NewAddrContext(&conn.NetAddr, reqCtx) streamCtx := httptrace.WithClientTrace(addrCtx, &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { select { case gotConn <- true: default: // GotConn maybe called multiple times, ignore the second and later calls } }, }) req, err := http.NewRequestWithContext(streamCtx, c.cfg.GetNormalizedUplinkHTTPMethod(), requestURL.String(), pr) if err != nil { _ = pr.Close() _ = pw.Close() httputils.CloseTransport(transport) reqCancel() return nil, err } req.Host = c.cfg.Host if err = c.cfg.FillStreamRequest(req, ""); err != nil { _ = pr.Close() _ = pw.Close() httputils.CloseTransport(transport) reqCancel() return nil, err } wrc := NewWaitReadCloser() go func() { resp, err := transport.RoundTrip(req) if err != nil { wrc.CloseWithError(err) close(gotConn) return } if resp.StatusCode < 200 || resp.StatusCode >= 300 { _ = resp.Body.Close() wrc.CloseWithError(fmt.Errorf("xhttp stream-one bad status: %s", resp.Status)) return } wrc.Set(resp.Body) }() if !<-gotConn { // RoundTrip failed before TCP connected (e.g. DNS failure) _ = pr.Close() _ = pw.Close() httputils.CloseTransport(transport) reqCancel() var buf [0]byte _, err = wrc.Read(buf[:]) return nil, err } conn.reader = wrc conn.onClose = func() { _ = pr.Close() httputils.CloseTransport(transport) reqCancel() } return conn, nil } func (c *Client) DialStreamUp(ctx context.Context) (net.Conn, error) { uploadTransport, downloadTransport, err := c.getTransport() if err != nil { return nil, err } downloadCfg := c.cfg if ds := c.cfg.DownloadConfig; ds != nil { downloadCfg = ds } streamURL := url.URL{ Scheme: "https", Host: c.cfg.Host, Path: c.cfg.NormalizedPath(), } downloadURL := url.URL{ Scheme: "https", Host: downloadCfg.Host, Path: downloadCfg.NormalizedPath(), } pr, pw := io.Pipe() conn := &Conn{writer: pw} sessionID := newSessionID() // Async download: avoid blocking on CDN response header buffering gotConn := make(chan bool, 1) reqCtx, reqCancel := context.WithCancel(c.ctx) // reqCtx must alive during conn not closed stop := contextutils.AfterFunc(ctx, reqCancel) // temporarily connect ctx with reqCtx when dialing defer stop() // disconnect ctx with reqCtx after dialing addrCtx := httputils.NewAddrContext(&conn.NetAddr, reqCtx) downloadCtx := httptrace.WithClientTrace(addrCtx, &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { select { case gotConn <- true: default: // GotConn maybe called multiple times, ignore the second and later calls } }, }) downloadReq, err := http.NewRequestWithContext( downloadCtx, http.MethodGet, downloadURL.String(), nil, ) if err != nil { httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() return nil, err } if err := downloadCfg.FillDownloadRequest(downloadReq, sessionID); err != nil { httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() return nil, err } downloadReq.Host = downloadCfg.Host uploadReq, err := http.NewRequestWithContext( reqCtx, c.cfg.GetNormalizedUplinkHTTPMethod(), streamURL.String(), pr, ) if err != nil { httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() return nil, err } if err = c.cfg.FillStreamRequest(uploadReq, sessionID); err != nil { httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() return nil, err } uploadReq.Host = c.cfg.Host wrc := NewWaitReadCloser() go func() { resp, err := downloadTransport.RoundTrip(downloadReq) if err != nil { wrc.CloseWithError(err) close(gotConn) return } if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() wrc.CloseWithError(fmt.Errorf("xhttp stream-up download bad status: %s", resp.Status)) return } wrc.Set(resp.Body) }() if !<-gotConn { _ = pr.Close() _ = pw.Close() httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() var buf [0]byte _, err = wrc.Read(buf[:]) return nil, err } // Start upload after download TCP is connected, so the server has likely // already processed the GET and created the session. This preserves the // original ordering (download before upload) while still being async. go func() { resp, err := uploadTransport.RoundTrip(uploadReq) if err != nil { _ = pw.CloseWithError(err) return } defer resp.Body.Close() _, _ = io.Copy(io.Discard, resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { _ = pw.CloseWithError(fmt.Errorf("xhttp stream-up upload bad status: %s", resp.Status)) } }() conn.reader = wrc conn.onClose = func() { _ = pr.Close() httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() } return conn, nil } func (c *Client) DialPacketUp(ctx context.Context) (net.Conn, error) { uploadTransport, downloadTransport, err := c.getTransport() if err != nil { return nil, err } downloadCfg := c.cfg if ds := c.cfg.DownloadConfig; ds != nil { downloadCfg = ds } sessionID := newSessionID() downloadURL := url.URL{ Scheme: "https", Host: downloadCfg.Host, Path: downloadCfg.NormalizedPath(), } writerCtx, writerCancel := context.WithCancel(c.ctx) writer := &PacketUpWriter{ ctx: writerCtx, cancel: writerCancel, cfg: c.cfg, scMaxEachPostBytes: c.scMaxEachPostBytes.Rand(), scMinPostsIntervalMs: c.scMinPostsIntervalMs, sessionID: sessionID, transport: uploadTransport, seq: 0, } writer.writeCond = sync.Cond{L: &writer.writeMu} conn := &Conn{writer: writer} // Async download: avoid blocking on CDN response header buffering gotConn := make(chan bool, 1) reqCtx, reqCancel := context.WithCancel(c.ctx) // reqCtx must alive during conn not closed stop := contextutils.AfterFunc(ctx, reqCancel) // temporarily connect ctx with reqCtx when dialing defer stop() // disconnect ctx with reqCtx after dialing addrCtx := httputils.NewAddrContext(&conn.NetAddr, reqCtx) downloadCtx := httptrace.WithClientTrace(addrCtx, &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { select { case gotConn <- true: default: // GotConn maybe called multiple times, ignore the second and later calls } }, }) downloadReq, err := http.NewRequestWithContext( downloadCtx, http.MethodGet, downloadURL.String(), nil, ) if err != nil { httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() return nil, err } if err = downloadCfg.FillDownloadRequest(downloadReq, sessionID); err != nil { httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() return nil, err } downloadReq.Host = downloadCfg.Host wrc := NewWaitReadCloser() go func() { resp, err := downloadTransport.RoundTrip(downloadReq) if err != nil { wrc.CloseWithError(err) close(gotConn) return } if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() wrc.CloseWithError(fmt.Errorf("xhttp packet-up download bad status: %s", resp.Status)) return } wrc.Set(resp.Body) }() if !<-gotConn { httputils.CloseTransport(uploadTransport) httputils.CloseTransport(downloadTransport) reqCancel() var buf [0]byte _, err = wrc.Read(buf[:]) return nil, err } conn.reader = wrc conn.onClose = func() { // uploadTransport already closed by writer httputils.CloseTransport(downloadTransport) reqCancel() } return conn, nil } func newSessionID() string { var b [16]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) } // WaitReadCloser is an io.ReadCloser that blocks on Read() until the underlying // ReadCloser is provided via Set(). This enables returning a reader immediately // while the actual HTTP response body is obtained asynchronously in a goroutine, // breaking the synchronous RoundTrip deadlock with CDN header buffering. type WaitReadCloser struct { wait chan struct{} once sync.Once rc io.ReadCloser err error } func NewWaitReadCloser() *WaitReadCloser { return &WaitReadCloser{wait: make(chan struct{})} } // Set provides the underlying ReadCloser and unblocks any pending Read calls. // Must be called at most once. If Close was already called, rc is closed to // prevent leaks. func (w *WaitReadCloser) Set(rc io.ReadCloser) { w.setup(rc, nil) } // CloseWithError records an error and unblocks any pending Read calls. func (w *WaitReadCloser) CloseWithError(err error) { w.setup(nil, err) } // setup sets the underlying ReadCloser and error. func (w *WaitReadCloser) setup(rc io.ReadCloser, err error) { w.once.Do(func() { w.rc = rc w.err = err close(w.wait) }) if w.err != nil && rc != nil { _ = rc.Close() } } func (w *WaitReadCloser) Read(b []byte) (int, error) { <-w.wait if w.rc == nil { return 0, w.err } return w.rc.Read(b) } func (w *WaitReadCloser) Close() error { w.setup(nil, net.ErrClosed) <-w.wait if w.rc != nil { return w.rc.Close() } return nil } ================================================ FILE: core/Clash.Meta/transport/xhttp/config.go ================================================ package xhttp import ( "bytes" "encoding/base64" "fmt" "io" "math/rand" "strconv" "strings" "github.com/metacubex/http" ) const ( PlacementQueryInHeader = "queryInHeader" PlacementCookie = "cookie" PlacementHeader = "header" PlacementQuery = "query" PlacementPath = "path" PlacementBody = "body" PlacementAuto = "auto" ) type Config struct { Host string Path string Mode string Headers map[string]string NoGRPCHeader bool XPaddingBytes string XPaddingObfsMode bool XPaddingKey string XPaddingHeader string XPaddingPlacement string XPaddingMethod string UplinkHTTPMethod string SessionPlacement string SessionKey string SeqPlacement string SeqKey string UplinkDataPlacement string UplinkDataKey string UplinkChunkSize string NoSSEHeader bool // server only ScStreamUpServerSecs string // server only ScMaxBufferedPosts string // server only ScMaxEachPostBytes string ScMinPostsIntervalMs string ReuseConfig *ReuseConfig DownloadConfig *Config } type ReuseConfig struct { MaxConcurrency string MaxConnections string CMaxReuseTimes string HMaxRequestTimes string HMaxReusableSecs string } func (c *Config) NormalizedMode() string { if c.Mode == "" { return "auto" } return c.Mode } func (c *Config) EffectiveMode(hasReality bool) string { mode := c.NormalizedMode() if mode != "auto" { return mode } if hasReality { if c.DownloadConfig != nil { return "stream-up" } return "stream-one" } return "packet-up" } func (c *Config) NormalizedPath() string { path := c.Path if path == "" { path = "/" } if !strings.HasPrefix(path, "/") { path = "/" + path } if !strings.HasSuffix(path, "/") { path += "/" } return path } func (c *Config) GetRequestHeader() http.Header { h := http.Header{} for k, v := range c.Headers { h.Set(k, v) } TryDefaultHeadersWith(h, "fetch") return h } func (c *Config) GetRequestHeaderWithPayload(payload []byte, uplinkChunkSize Range) http.Header { header := c.GetRequestHeader() key := c.UplinkDataKey encodedData := base64.RawURLEncoding.EncodeToString(payload) for i := 0; len(encodedData) > 0; i++ { chunkSize := uplinkChunkSize.Rand() if len(encodedData) < chunkSize { chunkSize = len(encodedData) } chunk := encodedData[:chunkSize] encodedData = encodedData[chunkSize:] headerKey := fmt.Sprintf("%s-%d", key, i) header.Set(headerKey, chunk) } return header } func (c *Config) GetRequestCookiesWithPayload(payload []byte, uplinkChunkSize Range) []*http.Cookie { cookies := []*http.Cookie{} key := c.UplinkDataKey encodedData := base64.RawURLEncoding.EncodeToString(payload) for i := 0; len(encodedData) > 0; i++ { chunkSize := uplinkChunkSize.Rand() if len(encodedData) < chunkSize { chunkSize = len(encodedData) } chunk := encodedData[:chunkSize] encodedData = encodedData[chunkSize:] cookieName := fmt.Sprintf("%s_%d", key, i) cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk}) } return cookies } func (c *Config) WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header) { if origin := requestHeader.Get("Origin"); origin == "" { writer.Header().Set("Access-Control-Allow-Origin", "*") } else { // Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. writer.Header().Set("Access-Control-Allow-Origin", origin) } if c.GetNormalizedSessionPlacement() == PlacementCookie || c.GetNormalizedSeqPlacement() == PlacementCookie || c.XPaddingPlacement == PlacementCookie || c.GetNormalizedUplinkDataPlacement() == PlacementCookie { writer.Header().Set("Access-Control-Allow-Credentials", "true") } if requestMethod == "OPTIONS" { requestedMethod := requestHeader.Get("Access-Control-Request-Method") if requestedMethod != "" { writer.Header().Set("Access-Control-Allow-Methods", requestedMethod) } else { writer.Header().Set("Access-Control-Allow-Methods", "*") } requestedHeaders := requestHeader.Get("Access-Control-Request-Headers") if requestedHeaders == "" { writer.Header().Set("Access-Control-Allow-Headers", "*") } else { writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders) } } } func (c *Config) GetNormalizedUplinkHTTPMethod() string { if c.UplinkHTTPMethod == "" { return "POST" } return c.UplinkHTTPMethod } func (c *Config) GetNormalizedScStreamUpServerSecs() (Range, error) { r, err := ParseRange(c.ScStreamUpServerSecs, "20-80") if err != nil { return Range{}, fmt.Errorf("invalid sc-stream-up-server-secs: %w", err) } return r, nil } func (c *Config) GetNormalizedScMaxBufferedPosts() (Range, error) { r, err := ParseRange(c.ScMaxBufferedPosts, "30") if err != nil { return Range{}, fmt.Errorf("invalid sc-max-buffered-posts: %w", err) } if r.Max == 0 { return Range{}, fmt.Errorf("invalid sc-max-buffered-posts: must be greater than zero") } return r, nil } func (c *Config) GetNormalizedScMaxEachPostBytes() (Range, error) { r, err := ParseRange(c.ScMaxEachPostBytes, "1000000") if err != nil { return Range{}, fmt.Errorf("invalid sc-max-each-post-bytes: %w", err) } if r.Max == 0 { return Range{}, fmt.Errorf("invalid sc-max-each-post-bytes: must be greater than zero") } return r, nil } func (c *Config) GetNormalizedScMinPostsIntervalMs() (Range, error) { r, err := ParseRange(c.ScMinPostsIntervalMs, "30") if err != nil { return Range{}, fmt.Errorf("invalid sc-min-posts-interval-ms: %w", err) } if r.Max == 0 { return Range{}, fmt.Errorf("invalid sc-min-posts-interval-ms: must be greater than zero") } return r, nil } func (c *Config) GetNormalizedUplinkChunkSize() (Range, error) { uplinkChunkSize, err := ParseRange(c.UplinkChunkSize, "") if err != nil { return Range{}, fmt.Errorf("invalid uplink-chunk-size: %w", err) } if uplinkChunkSize.Max == 0 { switch c.GetNormalizedUplinkDataPlacement() { case PlacementCookie: return Range{ Min: 2 * 1024, // 2 KiB Max: 3 * 1024, // 3 KiB }, nil case PlacementHeader: return Range{ Min: 3 * 1024, // 3 KiB Max: 4 * 1024, // 4 KiB }, nil default: return c.GetNormalizedScMaxEachPostBytes() } } else if uplinkChunkSize.Min < 64 { uplinkChunkSize.Min = 64 if uplinkChunkSize.Max < 64 { uplinkChunkSize.Max = 64 } } return uplinkChunkSize, nil } func (c *Config) GetNormalizedSessionPlacement() string { if c.SessionPlacement == "" { return PlacementPath } return c.SessionPlacement } func (c *Config) GetNormalizedSeqPlacement() string { if c.SeqPlacement == "" { return PlacementPath } return c.SeqPlacement } func (c *Config) GetNormalizedUplinkDataPlacement() string { if c.UplinkDataPlacement == "" { return PlacementBody } return c.UplinkDataPlacement } func (c *Config) GetNormalizedSessionKey() string { if c.SessionKey != "" { return c.SessionKey } switch c.GetNormalizedSessionPlacement() { case PlacementHeader: return "X-Session" case PlacementCookie, PlacementQuery: return "x_session" default: return "" } } func (c *Config) GetNormalizedSeqKey() string { if c.SeqKey != "" { return c.SeqKey } switch c.GetNormalizedSeqPlacement() { case PlacementHeader: return "X-Seq" case PlacementCookie, PlacementQuery: return "x_seq" default: return "" } } type Range struct { Min int Max int } func (r Range) Rand() int { if r.Min == r.Max { return r.Min } return r.Min + rand.Intn(r.Max-r.Min+1) } func ParseRange(s string, fallback string) (Range, error) { if strings.TrimSpace(s) == "" { return parseRange(fallback) } return parseRange(s) } func parseRange(s string) (Range, error) { parts := strings.Split(strings.TrimSpace(s), "-") if len(parts) == 1 { v, err := strconv.Atoi(parts[0]) if err != nil { return Range{}, err } return Range{v, v}, nil } if len(parts) != 2 { return Range{}, fmt.Errorf("invalid range: %s", s) } minVal, err := strconv.Atoi(strings.TrimSpace(parts[0])) if err != nil { return Range{}, err } maxVal, err := strconv.Atoi(strings.TrimSpace(parts[1])) if err != nil { return Range{}, err } if minVal < 0 || maxVal < minVal { return Range{}, fmt.Errorf("invalid range: %s", s) } return Range{minVal, maxVal}, nil } func (c *ReuseConfig) ResolveManagerConfig() (Range, Range, error) { if c == nil { return Range{}, Range{}, nil } maxConcurrency, err := ParseRange(c.MaxConcurrency, "0") if err != nil { return Range{}, Range{}, fmt.Errorf("invalid max-concurrency: %w", err) } maxConnections, err := ParseRange(c.MaxConnections, "0") if err != nil { return Range{}, Range{}, fmt.Errorf("invalid max-connections: %w", err) } return maxConcurrency, maxConnections, nil } func (c *ReuseConfig) ResolveEntryConfig() (Range, Range, Range, error) { if c == nil { return Range{}, Range{}, Range{}, nil } cMaxReuseTimes, err := ParseRange(c.CMaxReuseTimes, "0") if err != nil { return Range{}, Range{}, Range{}, fmt.Errorf("invalid c-max-reuse-times: %w", err) } hMaxRequestTimes, err := ParseRange(c.HMaxRequestTimes, "0") if err != nil { return Range{}, Range{}, Range{}, fmt.Errorf("invalid h-max-request-times: %w", err) } hMaxReusableSecs, err := ParseRange(c.HMaxReusableSecs, "0") if err != nil { return Range{}, Range{}, Range{}, fmt.Errorf("invalid h-max-reusable-secs: %w", err) } return cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, nil } func appendToPath(path, value string) string { if strings.HasSuffix(path, "/") { return path + value } return path + "/" + value } func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) { sessionPlacement := c.GetNormalizedSessionPlacement() seqPlacement := c.GetNormalizedSeqPlacement() sessionKey := c.GetNormalizedSessionKey() seqKey := c.GetNormalizedSeqKey() if sessionId != "" { switch sessionPlacement { case PlacementPath: req.URL.Path = appendToPath(req.URL.Path, sessionId) case PlacementQuery: q := req.URL.Query() q.Set(sessionKey, sessionId) req.URL.RawQuery = q.Encode() case PlacementHeader: req.Header.Set(sessionKey, sessionId) case PlacementCookie: req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId}) } } if seqStr != "" { switch seqPlacement { case PlacementPath: req.URL.Path = appendToPath(req.URL.Path, seqStr) case PlacementQuery: q := req.URL.Query() q.Set(seqKey, seqStr) req.URL.RawQuery = q.Encode() case PlacementHeader: req.Header.Set(seqKey, seqStr) case PlacementCookie: req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr}) } } } func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) { sessionPlacement := c.GetNormalizedSessionPlacement() seqPlacement := c.GetNormalizedSeqPlacement() sessionKey := c.GetNormalizedSessionKey() seqKey := c.GetNormalizedSeqKey() var subpath []string pathPart := 0 if sessionPlacement == PlacementPath || seqPlacement == PlacementPath { subpath = strings.Split(req.URL.Path[len(path):], "/") } switch sessionPlacement { case PlacementPath: if len(subpath) > pathPart { sessionId = subpath[pathPart] pathPart += 1 } case PlacementQuery: sessionId = req.URL.Query().Get(sessionKey) case PlacementHeader: sessionId = req.Header.Get(sessionKey) case PlacementCookie: if cookie, e := req.Cookie(sessionKey); e == nil { sessionId = cookie.Value } } switch seqPlacement { case PlacementPath: if len(subpath) > pathPart { seqStr = subpath[pathPart] pathPart += 1 } case PlacementQuery: seqStr = req.URL.Query().Get(seqKey) case PlacementHeader: seqStr = req.Header.Get(seqKey) case PlacementCookie: if cookie, e := req.Cookie(seqKey); e == nil { seqStr = cookie.Value } } return sessionId, seqStr } func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error { req.Header = c.GetRequestHeader() xPaddingBytes, err := c.GetNormalizedXPaddingBytes() if err != nil { return err } length := xPaddingBytes.Rand() config := XPaddingConfig{Length: length} if c.XPaddingObfsMode { config.Placement = XPaddingPlacement{ Placement: c.XPaddingPlacement, Key: c.XPaddingKey, Header: c.XPaddingHeader, RawURL: req.URL.String(), } config.Method = PaddingMethod(c.XPaddingMethod) } else { config.Placement = XPaddingPlacement{ Placement: PlacementQueryInHeader, Key: "x_padding", Header: "Referer", RawURL: req.URL.String(), } } c.ApplyXPaddingToRequest(req, config) c.ApplyMetaToRequest(req, sessionID, "") if req.Body != nil && !c.NoGRPCHeader { // stream-up/one req.Header.Set("Content-Type", "application/grpc") } return nil } func (c *Config) FillDownloadRequest(req *http.Request, sessionID string) error { return c.FillStreamRequest(req, sessionID) } func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, data []byte) error { dataPlacement := c.GetNormalizedUplinkDataPlacement() if dataPlacement == PlacementBody || dataPlacement == PlacementAuto { request.Header = c.GetRequestHeader() request.Body = io.NopCloser(bytes.NewReader(data)) request.ContentLength = int64(len(data)) } else { request.Body = nil request.ContentLength = 0 switch dataPlacement { case PlacementHeader: uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize() if err != nil { return err } request.Header = c.GetRequestHeaderWithPayload(data, uplinkChunkSize) case PlacementCookie: request.Header = c.GetRequestHeader() uplinkChunkSize, err := c.GetNormalizedUplinkChunkSize() if err != nil { return err } for _, cookie := range c.GetRequestCookiesWithPayload(data, uplinkChunkSize) { request.AddCookie(cookie) } } } xPaddingBytes, err := c.GetNormalizedXPaddingBytes() if err != nil { return err } length := xPaddingBytes.Rand() config := XPaddingConfig{Length: length} if c.XPaddingObfsMode { config.Placement = XPaddingPlacement{ Placement: c.XPaddingPlacement, Key: c.XPaddingKey, Header: c.XPaddingHeader, RawURL: request.URL.String(), } config.Method = PaddingMethod(c.XPaddingMethod) } else { config.Placement = XPaddingPlacement{ Placement: PlacementQueryInHeader, Key: "x_padding", Header: "Referer", RawURL: request.URL.String(), } } c.ApplyXPaddingToRequest(request, config) c.ApplyMetaToRequest(request, sessionId, seqStr) return nil } ================================================ FILE: core/Clash.Meta/transport/xhttp/conn.go ================================================ package xhttp import ( "errors" "io" "time" "github.com/metacubex/mihomo/common/httputils" ) type Conn struct { writer io.WriteCloser reader io.ReadCloser onClose func() httputils.NetAddr // deadlines deadline *time.Timer } func (c *Conn) Write(b []byte) (int, error) { return c.writer.Write(b) } func (c *Conn) Read(b []byte) (int, error) { return c.reader.Read(b) } func (c *Conn) Close() error { err := c.writer.Close() err2 := c.reader.Close() if c.onClose != nil { c.onClose() } return errors.Join(err, err2) } func (c *Conn) SetReadDeadline(t time.Time) error { return c.SetDeadline(t) } func (c *Conn) SetWriteDeadline(t time.Time) error { return c.SetDeadline(t) } func (c *Conn) SetDeadline(t time.Time) error { if t.IsZero() { if c.deadline != nil { c.deadline.Stop() c.deadline = nil } return nil } d := time.Until(t) if c.deadline != nil { c.deadline.Reset(d) return nil } c.deadline = time.AfterFunc(d, func() { c.Close() }) return nil } ================================================ FILE: core/Clash.Meta/transport/xhttp/reuse.go ================================================ package xhttp import ( "sync" "sync/atomic" "time" "github.com/metacubex/mihomo/common/httputils" "github.com/metacubex/http" ) type reuseEntry struct { transport http.RoundTripper openUsage atomic.Int32 leftRequests atomic.Int32 reuseCount atomic.Int32 maxReuseTimes int32 unreusableAt time.Time closed atomic.Bool } func (entry *reuseEntry) isClosed() bool { return entry.closed.Load() } func (entry *reuseEntry) close() { if !entry.closed.CompareAndSwap(false, true) { return } httputils.CloseTransport(entry.transport) } type ReuseTransport struct { entry *reuseEntry removed atomic.Bool } func (rt *ReuseTransport) RoundTrip(req *http.Request) (*http.Response, error) { return rt.entry.transport.RoundTrip(req) } func (rt *ReuseTransport) Close() error { if !rt.removed.CompareAndSwap(false, true) { return nil } rt.entry.release() return nil } var _ http.RoundTripper = (*ReuseTransport)(nil) type ReuseManager struct { maxConcurrency int maxConnections int cMaxReuseTimes Range hMaxRequestTimes Range hMaxReusableSecs Range maker TransportMaker mu sync.Mutex entries []*reuseEntry } func NewReuseManager(cfg *ReuseConfig, makeTransport TransportMaker) (*ReuseManager, error) { if cfg == nil { return nil, nil } concurrency, connections, err := cfg.ResolveManagerConfig() if err != nil { return nil, err } cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, err := cfg.ResolveEntryConfig() if err != nil { return nil, err } return &ReuseManager{ maxConcurrency: concurrency.Rand(), maxConnections: connections.Rand(), cMaxReuseTimes: cMaxReuseTimes, hMaxRequestTimes: hMaxRequestTimes, hMaxReusableSecs: hMaxReusableSecs, maker: makeTransport, entries: make([]*reuseEntry, 0), }, nil } func (m *ReuseManager) Close() error { if m == nil { return nil } m.mu.Lock() defer m.mu.Unlock() for _, entry := range m.entries { entry.close() } m.entries = nil return nil } func (m *ReuseManager) cleanupLocked(now time.Time) { kept := m.entries[:0] for _, entry := range m.entries { if entry.isClosed() { continue } if entry.leftRequests.Load() <= 0 && entry.openUsage.Load() == 0 { entry.close() continue } if !entry.unreusableAt.IsZero() && now.After(entry.unreusableAt) && entry.openUsage.Load() == 0 { entry.close() continue } kept = append(kept, entry) } m.entries = kept } func (entry *reuseEntry) release() { if entry == nil { return } remaining := entry.openUsage.Add(-1) if remaining < 0 { entry.openUsage.Store(0) remaining = 0 } if remaining == 0 { now := time.Now() if entry.leftRequests.Load() <= 0 || (entry.maxReuseTimes > 0 && entry.reuseCount.Load() >= entry.maxReuseTimes) || (!entry.unreusableAt.IsZero() && now.After(entry.unreusableAt)) { entry.close() } } } func (m *ReuseManager) pickLocked() *reuseEntry { var best *reuseEntry for _, entry := range m.entries { if entry.isClosed() { continue } if entry.leftRequests.Load() <= 0 { continue } if entry.maxReuseTimes > 0 && entry.reuseCount.Load() >= entry.maxReuseTimes { continue } if m.maxConcurrency > 0 && int(entry.openUsage.Load()) >= m.maxConcurrency { continue } if best == nil || entry.openUsage.Load() < best.openUsage.Load() { best = entry } } return best } func (m *ReuseManager) shouldCreateLocked() bool { if len(m.entries) == 0 { return true } if m.maxConnections > 0 { return len(m.entries) < m.maxConnections } return false } func (m *ReuseManager) newEntryLocked(transport http.RoundTripper, now time.Time) *reuseEntry { entry := &reuseEntry{transport: transport} if m.hMaxRequestTimes.Max > 0 { entry.leftRequests.Store(int32(m.hMaxRequestTimes.Rand())) } else { entry.leftRequests.Store(1<<30 - 1) } if m.hMaxReusableSecs.Max > 0 { entry.unreusableAt = now.Add(time.Duration(m.hMaxReusableSecs.Rand()) * time.Second) } if m.cMaxReuseTimes.Max > 0 { entry.maxReuseTimes = int32(m.cMaxReuseTimes.Rand()) } m.entries = append(m.entries, entry) return entry } func (m *ReuseManager) GetTransport() http.RoundTripper { now := time.Now() m.mu.Lock() defer m.mu.Unlock() m.cleanupLocked(now) var entry *reuseEntry if !m.shouldCreateLocked() { entry = m.pickLocked() } reused := entry != nil if entry == nil { transport := m.maker() entry = m.newEntryLocked(transport, now) } if reused { entry.reuseCount.Add(1) } entry.openUsage.Add(1) if entry.leftRequests.Load() > 0 { entry.leftRequests.Add(-1) } return &ReuseTransport{entry: entry} } ================================================ FILE: core/Clash.Meta/transport/xhttp/reuse_test.go ================================================ package xhttp import ( "sync/atomic" "testing" "time" "github.com/metacubex/http" ) type testRoundTripper struct { id int64 } func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { panic("not used in reuse manager unit tests") } func makeTestTransportFactory(counter *atomic.Int64) TransportMaker { return func() http.RoundTripper { id := counter.Add(1) return &testRoundTripper{id: id} } } func transportID(rt *ReuseTransport) int64 { return rt.entry.transport.(*testRoundTripper).id } func TestManagerReuseSameEntry(t *testing.T) { var created atomic.Int64 manager, err := NewReuseManager(&ReuseConfig{ MaxConcurrency: "1", MaxConnections: "1", HMaxRequestTimes: "10", }, makeTestTransportFactory(&created)) if err != nil { t.Fatal(err) } transport1 := manager.GetTransport().(*ReuseTransport) id1 := transportID(transport1) transport1.Close() transport2 := manager.GetTransport().(*ReuseTransport) id2 := transportID(transport2) if id1 != id2 { t.Fatalf("expected same transport to be reused, got %d and %d", id1, id2) } transport2.Close() manager.Close() } func TestManagerRespectMaxConnections(t *testing.T) { var created atomic.Int64 manager, err := NewReuseManager(&ReuseConfig{ MaxConcurrency: "2", MaxConnections: "2", HMaxRequestTimes: "100", }, makeTestTransportFactory(&created)) if err != nil { t.Fatal(err) } transport1 := manager.GetTransport().(*ReuseTransport) id1 := transportID(transport1) transport2 := manager.GetTransport().(*ReuseTransport) id2 := transportID(transport2) transport3 := manager.GetTransport().(*ReuseTransport) id3 := transportID(transport3) transport4 := manager.GetTransport().(*ReuseTransport) id4 := transportID(transport4) transport5 := manager.GetTransport().(*ReuseTransport) id5 := transportID(transport5) if id1 == id2 { t.Fatal("expected the second transport to be new") } if id3 != id1 && id3 != id2 { t.Fatal("expected the third transport to be reused") } if id4 != id1 && id4 != id2 { t.Fatal("expected the fourth transport to be reused") } if id5 == id1 || id5 == id2 { t.Fatal("expected the fifth transport to be new") } transport1.Close() transport2.Close() transport3.Close() transport4.Close() transport5.Close() manager.Close() } func TestManagerRotateOnRequestLimit(t *testing.T) { var created atomic.Int64 manager, err := NewReuseManager(&ReuseConfig{ MaxConcurrency: "1", MaxConnections: "1", HMaxRequestTimes: "1", }, makeTestTransportFactory(&created)) if err != nil { t.Fatal(err) } transport1 := manager.GetTransport().(*ReuseTransport) id1 := transportID(transport1) transport1.Close() transport2 := manager.GetTransport().(*ReuseTransport) id2 := transportID(transport2) if id1 == id2 { t.Fatalf("expected new transport after request limit, got same id %d", id1) } transport2.Close() manager.Close() } func TestManagerRotateOnReusableSecs(t *testing.T) { var created atomic.Int64 manager, err := NewReuseManager(&ReuseConfig{ MaxConcurrency: "1", MaxConnections: "1", HMaxRequestTimes: "100", HMaxReusableSecs: "1", }, makeTestTransportFactory(&created)) if err != nil { t.Fatal(err) } transport1 := manager.GetTransport().(*ReuseTransport) id1 := transportID(transport1) time.Sleep(1100 * time.Millisecond) transport1.Close() transport2 := manager.GetTransport().(*ReuseTransport) id2 := transportID(transport2) if id1 == id2 { t.Fatalf("expected new transport after reusable timeout, got same id %d", id1) } transport2.Close() manager.Close() } func TestManagerRotateOnConnReuseLimit(t *testing.T) { var created atomic.Int64 manager, err := NewReuseManager(&ReuseConfig{ MaxConcurrency: "1", MaxConnections: "1", CMaxReuseTimes: "1", HMaxRequestTimes: "100", }, makeTestTransportFactory(&created)) if err != nil { t.Fatal(err) } transport1 := manager.GetTransport().(*ReuseTransport) id1 := transportID(transport1) transport1.Close() transport2 := manager.GetTransport().(*ReuseTransport) id2 := transportID(transport2) if id1 != id2 { t.Fatalf("expected first reuse to use same transport, got %d and %d", id1, id2) } transport2.Close() transport3 := manager.GetTransport().(*ReuseTransport) id3 := transportID(transport3) if id3 == id2 { t.Fatalf("expected new transport after c-max-reuse-times limit, got same id %d", id3) } transport3.Close() manager.Close() } ================================================ FILE: core/Clash.Meta/transport/xhttp/server.go ================================================ package xhttp import ( "bytes" "encoding/base64" "fmt" "io" "net" "strconv" "strings" "sync" "time" "github.com/metacubex/mihomo/common/httputils" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/http" ) type ServerOption struct { Config ConnHandler func(net.Conn) HttpHandler http.Handler } type httpServerConn struct { mu sync.Mutex w http.ResponseWriter flusher http.Flusher reader io.ReadCloser closed bool done chan struct{} once sync.Once } func newHTTPServerConn(w http.ResponseWriter, r io.ReadCloser) *httpServerConn { flusher, _ := w.(http.Flusher) return &httpServerConn{ w: w, flusher: flusher, reader: r, done: make(chan struct{}), } } func (c *httpServerConn) Read(b []byte) (int, error) { return c.reader.Read(b) } func (c *httpServerConn) Write(b []byte) (int, error) { c.mu.Lock() defer c.mu.Unlock() if c.closed { return 0, io.ErrClosedPipe } n, err := c.w.Write(b) if err == nil && c.flusher != nil { c.flusher.Flush() } return n, err } func (c *httpServerConn) Close() error { c.once.Do(func() { c.mu.Lock() c.closed = true c.mu.Unlock() close(c.done) }) return c.reader.Close() } func (c *httpServerConn) Wait() <-chan struct{} { return c.done } type httpSession struct { uploadQueue *UploadQueue connected chan struct{} once sync.Once } func newHTTPSession(maxPackets int) *httpSession { return &httpSession{ uploadQueue: NewUploadQueue(maxPackets), connected: make(chan struct{}), } } func (s *httpSession) markConnected() { s.once.Do(func() { close(s.connected) }) } type requestHandler struct { config Config connHandler func(net.Conn) httpHandler http.Handler xPaddingBytes Range scMaxEachPostBytes Range scStreamUpServerSecs Range scMaxBufferedPosts Range mu sync.Mutex sessions map[string]*httpSession } func NewServerHandler(opt ServerOption) (http.Handler, error) { xPaddingBytes, err := opt.Config.GetNormalizedXPaddingBytes() if err != nil { return nil, err } scMaxEachPostBytes, err := opt.Config.GetNormalizedScMaxEachPostBytes() if err != nil { return nil, err } scStreamUpServerSecs, err := opt.Config.GetNormalizedScStreamUpServerSecs() if err != nil { return nil, err } scMaxBufferedPosts, err := opt.Config.GetNormalizedScMaxBufferedPosts() if err != nil { return nil, err } return &requestHandler{ config: opt.Config, connHandler: opt.ConnHandler, httpHandler: opt.HttpHandler, xPaddingBytes: xPaddingBytes, scMaxEachPostBytes: scMaxEachPostBytes, scStreamUpServerSecs: scStreamUpServerSecs, scMaxBufferedPosts: scMaxBufferedPosts, sessions: map[string]*httpSession{}, }, nil } func (h *requestHandler) upsertSession(sessionID string) *httpSession { h.mu.Lock() defer h.mu.Unlock() s, ok := h.sessions[sessionID] if ok { return s } s = newHTTPSession(h.scMaxBufferedPosts.Max) h.sessions[sessionID] = s // Reap orphan sessions that never become fully connected (e.g. from probing). // Matches Xray-core's 30-second reaper in upsertSession. go func() { timer := time.NewTimer(30 * time.Second) defer timer.Stop() select { case <-timer.C: h.deleteSession(sessionID) case <-s.connected: } }() return s } func (h *requestHandler) deleteSession(sessionID string) { h.mu.Lock() defer h.mu.Unlock() if s, ok := h.sessions[sessionID]; ok { _ = s.uploadQueue.Close() delete(h.sessions, sessionID) } } func (h *requestHandler) getSession(sessionID string) *httpSession { h.mu.Lock() defer h.mu.Unlock() return h.sessions[sessionID] } func (h *requestHandler) normalizedMode() string { if h.config.Mode == "" { return "auto" } return h.config.Mode } func (h *requestHandler) allowStreamOne() bool { switch h.normalizedMode() { case "auto", "stream-one", "stream-up": return true default: return false } } func (h *requestHandler) allowSessionDownload() bool { switch h.normalizedMode() { case "auto", "stream-up", "packet-up": return true default: return false } } func (h *requestHandler) allowStreamUpUpload() bool { switch h.normalizedMode() { case "auto", "stream-up": return true default: return false } } func (h *requestHandler) allowPacketUpUpload() bool { switch h.normalizedMode() { case "auto", "packet-up": return true default: return false } } func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := h.config.NormalizedPath() if h.httpHandler != nil && !strings.HasPrefix(r.URL.Path, path) { h.httpHandler.ServeHTTP(w, r) return } if h.config.Host != "" && !equalHost(r.Host, h.config.Host) { http.NotFound(w, r) return } if !strings.HasPrefix(r.URL.Path, path) { http.NotFound(w, r) return } h.config.WriteResponseHeader(w, r.Method, r.Header) length := h.xPaddingBytes.Rand() config := XPaddingConfig{Length: length} if h.config.XPaddingObfsMode { config.Placement = XPaddingPlacement{ Placement: h.config.XPaddingPlacement, Key: h.config.XPaddingKey, Header: h.config.XPaddingHeader, } config.Method = PaddingMethod(h.config.XPaddingMethod) } else { config.Placement = XPaddingPlacement{ Placement: PlacementHeader, Header: "X-Padding", } } h.config.ApplyXPaddingToResponse(w, config) if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } paddingValue, _ := h.config.ExtractXPaddingFromRequest(r, h.config.XPaddingObfsMode) if !h.config.IsPaddingValid(paddingValue, h.xPaddingBytes.Min, h.xPaddingBytes.Max, PaddingMethod(h.config.XPaddingMethod)) { http.Error(w, "invalid xpadding", http.StatusBadRequest) return } sessionId, seqStr := h.config.ExtractMetaFromRequest(r, path) var currentSession *httpSession if sessionId != "" { currentSession = h.upsertSession(sessionId) } // stream-up upload: POST /path/{session} if r.Method != http.MethodGet && sessionId != "" && seqStr == "" && h.allowStreamUpUpload() { httpSC := newHTTPServerConn(w, r.Body) err := currentSession.uploadQueue.Push(Packet{ Reader: httpSC, }) if err != nil { http.Error(w, err.Error(), http.StatusConflict) return } // magic header instructs nginx + apache to not buffer response body w.Header().Set("X-Accel-Buffering", "no") // A web-compliant header telling all middleboxes to disable caching. // Should be able to prevent overloading the cache, or stop CDNs from // teeing the response stream into their cache, causing slowdowns. w.Header().Set("Cache-Control", "no-store") if !h.config.NoSSEHeader { // magic header to make the HTTP middle box consider this as SSE to disable buffer w.Header().Set("Content-Type", "text/event-stream") } w.WriteHeader(http.StatusOK) rc := http.NewResponseController(w) _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually _ = rc.Flush() // force flush the response header referrer := r.Header.Get("Referer") if referrer != "" && h.scStreamUpServerSecs.Max > 0 { go func() { for { _, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(h.xPaddingBytes.Rand()))) if err != nil { break } time.Sleep(time.Duration(h.scStreamUpServerSecs.Rand()) * time.Second) } }() } select { case <-r.Context().Done(): case <-httpSC.Wait(): } _ = httpSC.Close() return } // packet-up upload: POST /path/{session}/{seq} if r.Method != http.MethodGet && sessionId != "" && seqStr != "" && h.allowPacketUpUpload() { scMaxEachPostBytes := h.scMaxEachPostBytes.Max dataPlacement := h.config.GetNormalizedUplinkDataPlacement() uplinkDataKey := h.config.UplinkDataKey var headerPayload []byte var err error if dataPlacement == PlacementAuto || dataPlacement == PlacementHeader { var headerPayloadChunks []string for i := 0; true; i++ { chunk := r.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i)) if chunk == "" { break } headerPayloadChunks = append(headerPayloadChunks, chunk) } headerPayloadEncoded := strings.Join(headerPayloadChunks, "") headerPayload, err = base64.RawURLEncoding.DecodeString(headerPayloadEncoded) if err != nil { http.Error(w, "invalid base64 in header's payload", http.StatusBadRequest) return } } var cookiePayload []byte if dataPlacement == PlacementAuto || dataPlacement == PlacementCookie { var cookiePayloadChunks []string for i := 0; true; i++ { cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i) if c, _ := r.Cookie(cookieName); c != nil { cookiePayloadChunks = append(cookiePayloadChunks, c.Value) } else { break } } cookiePayloadEncoded := strings.Join(cookiePayloadChunks, "") cookiePayload, err = base64.RawURLEncoding.DecodeString(cookiePayloadEncoded) if err != nil { http.Error(w, "invalid base64 in cookies' payload", http.StatusBadRequest) return } } var bodyPayload []byte if dataPlacement == PlacementAuto || dataPlacement == PlacementBody { if r.ContentLength > int64(scMaxEachPostBytes) { http.Error(w, "body too large", http.StatusRequestEntityTooLarge) return } bodyPayload, err = io.ReadAll(io.LimitReader(r.Body, int64(scMaxEachPostBytes)+1)) if err != nil { http.Error(w, "failed to read body", http.StatusBadRequest) return } } var payload []byte switch dataPlacement { case PlacementHeader: payload = headerPayload case PlacementCookie: payload = cookiePayload case PlacementBody: payload = bodyPayload case PlacementAuto: payload = headerPayload payload = append(payload, cookiePayload...) payload = append(payload, bodyPayload...) } if len(payload) > h.scMaxEachPostBytes.Max { http.Error(w, "body too large", http.StatusRequestEntityTooLarge) return } seq, err := strconv.ParseUint(seqStr, 10, 64) if err != nil { http.Error(w, "invalid xhttp seq", http.StatusBadRequest) return } err = currentSession.uploadQueue.Push(Packet{ Seq: seq, Payload: payload, }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if len(payload) == 0 { // Methods without a body are usually cached by default. w.Header().Set("Cache-Control", "no-store") } w.WriteHeader(http.StatusOK) return } // stream-up/packet-up download: GET /path/{session} if r.Method == http.MethodGet && sessionId != "" && seqStr == "" && h.allowSessionDownload() { currentSession.markConnected() // magic header instructs nginx + apache to not buffer response body w.Header().Set("X-Accel-Buffering", "no") // A web-compliant header telling all middleboxes to disable caching. // Should be able to prevent overloading the cache, or stop CDNs from // teeing the response stream into their cache, causing slowdowns. w.Header().Set("Cache-Control", "no-store") if !h.config.NoSSEHeader { // magic header to make the HTTP middle box consider this as SSE to disable buffer w.Header().Set("Content-Type", "text/event-stream") } w.WriteHeader(http.StatusOK) rc := http.NewResponseController(w) _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually _ = rc.Flush() // force flush the response header httpSC := newHTTPServerConn(w, r.Body) conn := &Conn{ writer: httpSC, reader: currentSession.uploadQueue, onClose: func() { h.deleteSession(sessionId) }, } httputils.SetAddrFromRequest(&conn.NetAddr, r) go h.connHandler(N.NewDeadlineConn(conn)) select { case <-r.Context().Done(): case <-httpSC.Wait(): } _ = conn.Close() return } // stream-one: POST /path if r.Method != http.MethodGet && sessionId == "" && seqStr == "" && h.allowStreamOne() { w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) rc := http.NewResponseController(w) _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually _ = rc.Flush() // force flush the response header httpSC := newHTTPServerConn(w, r.Body) conn := &Conn{ writer: httpSC, reader: httpSC, } httputils.SetAddrFromRequest(&conn.NetAddr, r) go h.connHandler(N.NewDeadlineConn(conn)) select { case <-r.Context().Done(): case <-httpSC.Wait(): } _ = conn.Close() return } http.NotFound(w, r) } func splitNonEmpty(s string) []string { raw := strings.Split(s, "/") out := make([]string, 0, len(raw)) for _, v := range raw { if v != "" { out = append(out, v) } } return out } func equalHost(a, b string) bool { a = strings.ToLower(a) b = strings.ToLower(b) if ah, _, err := net.SplitHostPort(a); err == nil { a = ah } if bh, _, err := net.SplitHostPort(b); err == nil { b = bh } return a == b } ================================================ FILE: core/Clash.Meta/transport/xhttp/server_test.go ================================================ package xhttp import ( "io" "net" "testing" "github.com/metacubex/http" "github.com/metacubex/http/httptest" "github.com/stretchr/testify/assert" ) func TestServerHandlerModeRestrictions(t *testing.T) { testCases := []struct { name string mode string method string target string wantStatus int }{ { name: "StreamOneAcceptsStreamOne", mode: "stream-one", method: http.MethodPost, target: "https://example.com/xhttp/", wantStatus: http.StatusOK, }, { name: "StreamOneRejectsSessionDownload", mode: "stream-one", method: http.MethodGet, target: "https://example.com/xhttp/session", wantStatus: http.StatusNotFound, }, { name: "StreamUpAcceptsStreamOne", mode: "stream-up", method: http.MethodPost, target: "https://example.com/xhttp/", wantStatus: http.StatusOK, }, { name: "StreamUpAllowsDownloadEndpoint", mode: "stream-up", method: http.MethodGet, target: "https://example.com/xhttp/session", wantStatus: http.StatusOK, }, { name: "StreamUpRejectsPacketUpload", mode: "stream-up", method: http.MethodPost, target: "https://example.com/xhttp/session/0", wantStatus: http.StatusNotFound, }, { name: "PacketUpAllowsDownloadEndpoint", mode: "packet-up", method: http.MethodGet, target: "https://example.com/xhttp/session", wantStatus: http.StatusOK, }, { name: "PacketUpRejectsStreamOne", mode: "packet-up", method: http.MethodPost, target: "https://example.com/xhttp/", wantStatus: http.StatusNotFound, }, { name: "PacketUpRejectsStreamUpUpload", mode: "packet-up", method: http.MethodPost, target: "https://example.com/xhttp/session", wantStatus: http.StatusNotFound, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { config := Config{ Path: "/xhttp", Mode: testCase.mode, } handler, err := NewServerHandler(ServerOption{ Config: config, ConnHandler: func(conn net.Conn) { _ = conn.Close() }, }) assert.NoError(t, err) req := httptest.NewRequest(testCase.method, testCase.target, io.NopCloser(http.NoBody)) recorder := httptest.NewRecorder() err = config.FillStreamRequest(req, "") assert.NoError(t, err) handler.ServeHTTP(recorder, req) assert.Equal(t, testCase.wantStatus, recorder.Result().StatusCode) }) } } ================================================ FILE: core/Clash.Meta/transport/xhttp/upload_queue.go ================================================ package xhttp import ( "errors" "io" "sync" ) var ErrQueueTooLarge = errors.New("packet queue is too large") type Packet struct { Seq uint64 Payload []byte // UploadQueue will hold Payload, so never reuse it after UploadQueue.Push Reader io.ReadCloser } type UploadQueue struct { mu sync.Mutex condPushed sync.Cond condPopped sync.Cond packets map[uint64][]byte nextSeq uint64 buf []byte closed bool maxPackets int reader io.ReadCloser } func NewUploadQueue(maxPackets int) *UploadQueue { q := &UploadQueue{ packets: make(map[uint64][]byte, maxPackets), maxPackets: maxPackets, } q.condPushed = sync.Cond{L: &q.mu} q.condPopped = sync.Cond{L: &q.mu} return q } func (q *UploadQueue) Push(p Packet) error { q.mu.Lock() defer q.mu.Unlock() if q.closed { return io.ErrClosedPipe } if q.reader != nil { return errors.New("uploadQueue.reader already exists") } if p.Reader != nil { q.reader = p.Reader q.condPushed.Broadcast() return nil } for len(q.packets) > q.maxPackets { q.condPopped.Wait() // wait for the reader to read the packets if q.closed { return io.ErrClosedPipe } } q.packets[p.Seq] = p.Payload q.condPushed.Broadcast() return nil } func (q *UploadQueue) Read(b []byte) (int, error) { q.mu.Lock() for { if len(q.buf) > 0 { n := copy(b, q.buf) q.buf = q.buf[n:] q.mu.Unlock() return n, nil } if payload, ok := q.packets[q.nextSeq]; ok { delete(q.packets, q.nextSeq) q.nextSeq++ q.buf = payload q.condPopped.Broadcast() continue } if reader := q.reader; reader != nil { q.mu.Unlock() // unlock before calling q.reader.Read return reader.Read(b) } if q.closed { q.mu.Unlock() return 0, io.EOF } if len(q.packets) > q.maxPackets { q.mu.Unlock() // the "reassembly buffer" is too large, and we want to constrain memory usage somehow. // let's tear down the connection and hope the application retries. return 0, ErrQueueTooLarge } q.condPushed.Wait() } } func (q *UploadQueue) Close() error { q.mu.Lock() defer q.mu.Unlock() var err error if q.reader != nil { err = q.reader.Close() } q.closed = true q.condPushed.Broadcast() q.condPopped.Broadcast() return err } ================================================ FILE: core/Clash.Meta/transport/xhttp/upload_queue_test.go ================================================ package xhttp import ( "io" "testing" "github.com/stretchr/testify/assert" ) func TestUploadQueueMaxPackets(t *testing.T) { q := NewUploadQueue(2) ch := make(chan struct{}) go func() { err := q.Push(Packet{Seq: 0, Payload: []byte{'0'}}) assert.NoError(t, err) err = q.Push(Packet{Seq: 1, Payload: []byte{'1'}}) assert.NoError(t, err) err = q.Push(Packet{Seq: 2, Payload: []byte{'2'}}) assert.NoError(t, err) err = q.Push(Packet{Seq: 4, Payload: []byte{'4'}}) assert.NoError(t, err) err = q.Push(Packet{Seq: 5, Payload: []byte{'5'}}) assert.NoError(t, err) err = q.Push(Packet{Seq: 6, Payload: []byte{'6'}}) assert.NoError(t, err) err = q.Push(Packet{Seq: 7, Payload: []byte{'7'}}) assert.ErrorIs(t, err, io.ErrClosedPipe) close(ch) }() buf := make([]byte, 20) n, err := q.Read(buf) assert.Equal(t, 1, n) assert.Equal(t, []byte{'0'}, buf[:n]) assert.NoError(t, err) n, err = q.Read(buf) assert.Equal(t, 1, n) assert.Equal(t, []byte{'1'}, buf[:n]) n, err = q.Read(buf) assert.Equal(t, 1, n) assert.Equal(t, []byte{'2'}, buf[:n]) n, err = q.Read(buf) assert.Equal(t, 0, n) assert.ErrorIs(t, err, ErrQueueTooLarge) err = q.Close() assert.NoError(t, err) <-ch } ================================================ FILE: core/Clash.Meta/transport/xhttp/xpadding.go ================================================ package xhttp import ( "crypto/rand" "fmt" "math" "net/url" "strings" "github.com/metacubex/http" "golang.org/x/net/http2/hpack" ) type PaddingMethod string const ( PaddingMethodRepeatX PaddingMethod = "repeat-x" PaddingMethodTokenish PaddingMethod = "tokenish" ) const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" // Huffman encoding gives ~20% size reduction for base62 sequences const avgHuffmanBytesPerCharBase62 = 0.8 const validationTolerance = 2 type XPaddingPlacement struct { Placement string Key string Header string RawURL string } type XPaddingConfig struct { Length int Placement XPaddingPlacement Method PaddingMethod } func randStringFromCharset(n int, charset string) (string, bool) { if n <= 0 || len(charset) == 0 { return "", false } m := len(charset) limit := byte(256 - (256 % m)) result := make([]byte, n) i := 0 buf := make([]byte, 256) for i < n { if _, err := rand.Read(buf); err != nil { return "", false } for _, rb := range buf { if rb >= limit { continue } result[i] = charset[int(rb)%m] i++ if i == n { break } } } return string(result), true } func absInt(x int) int { if x < 0 { return -x } return x } func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string { n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62)) if n < 1 { n = 1 } randBase62Str, ok := randStringFromCharset(n, charsetBase62) if !ok { return "" } const maxIter = 150 adjustChar := byte('X') // Adjust until close enough for iter := 0; iter < maxIter; iter++ { currentLength := int(hpack.HuffmanEncodeLength(randBase62Str)) diff := currentLength - targetHuffmanBytes if absInt(diff) <= validationTolerance { return randBase62Str } if diff < 0 { // Too small -> append padding char(s) randBase62Str += string(adjustChar) // Avoid a long run of identical chars if adjustChar == 'X' { adjustChar = 'Z' } else { adjustChar = 'X' } } else { // Too big -> remove from the end if len(randBase62Str) <= 1 { return randBase62Str } randBase62Str = randBase62Str[:len(randBase62Str)-1] } } return randBase62Str } func GeneratePadding(method PaddingMethod, length int) string { if length <= 0 { return "" } // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B // h2's HPACK Header Compression feature employs a huffman encoding using a static table. // 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire. // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2 // h3's similar QPACK feature uses the same huffman table. switch method { case PaddingMethodRepeatX: return strings.Repeat("X", length) case PaddingMethodTokenish: paddingValue := GenerateTokenishPaddingBase62(length) if paddingValue == "" { return strings.Repeat("X", length) } return paddingValue default: return strings.Repeat("X", length) } } func ApplyPaddingToCookie(req *http.Request, name, value string) { if req == nil || name == "" || value == "" { return } req.AddCookie(&http.Cookie{ Name: name, Value: value, Path: "/", }) } func ApplyPaddingToResponseCookie(writer http.ResponseWriter, name, value string) { if name == "" || value == "" { return } http.SetCookie(writer, &http.Cookie{ Name: name, Value: value, Path: "/", }) } func ApplyPaddingToQuery(u *url.URL, key, value string) { if u == nil || key == "" || value == "" { return } q := u.Query() q.Set(key, value) u.RawQuery = q.Encode() } func (c *Config) GetNormalizedXPaddingBytes() (Range, error) { r, err := ParseRange(c.XPaddingBytes, "100-1000") if err != nil { return Range{}, fmt.Errorf("invalid x-padding-bytes: %w", err) } return r, nil } func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) { if h == nil { return } paddingValue := GeneratePadding(config.Method, config.Length) switch p := config.Placement; p.Placement { case PlacementHeader: h.Set(p.Header, paddingValue) case PlacementQueryInHeader: u, err := url.Parse(p.RawURL) if err != nil || u == nil { return } u.RawQuery = p.Key + "=" + paddingValue h.Set(p.Header, u.String()) } } func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) { if req == nil { return } if req.Header == nil { req.Header = make(http.Header) } placement := config.Placement.Placement if placement == PlacementHeader || placement == PlacementQueryInHeader { c.ApplyXPaddingToHeader(req.Header, config) return } paddingValue := GeneratePadding(config.Method, config.Length) switch placement { case PlacementCookie: ApplyPaddingToCookie(req, config.Placement.Key, paddingValue) case PlacementQuery: ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue) } } func (c *Config) ApplyXPaddingToResponse(writer http.ResponseWriter, config XPaddingConfig) { placement := config.Placement.Placement if placement == PlacementHeader || placement == PlacementQueryInHeader { c.ApplyXPaddingToHeader(writer.Header(), config) return } paddingValue := GeneratePadding(config.Method, config.Length) switch placement { case PlacementCookie: ApplyPaddingToResponseCookie(writer, config.Placement.Key, paddingValue) } } func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) { if req == nil { return "", "" } if !obfsMode { referrer := req.Header.Get("Referer") if referrer != "" { if referrerURL, err := url.Parse(referrer); err == nil { paddingValue := referrerURL.Query().Get("x_padding") paddingPlacement := PlacementQueryInHeader + "=Referer, key=x_padding" return paddingValue, paddingPlacement } } else { paddingValue := req.URL.Query().Get("x_padding") return paddingValue, PlacementQuery + ", key=x_padding" } } key := c.XPaddingKey header := c.XPaddingHeader if cookie, err := req.Cookie(key); err == nil { if cookie != nil && cookie.Value != "" { paddingValue := cookie.Value paddingPlacement := PlacementCookie + ", key=" + key return paddingValue, paddingPlacement } } headerValue := req.Header.Get(header) if headerValue != "" { if c.XPaddingPlacement == PlacementHeader { paddingPlacement := PlacementHeader + "=" + header return headerValue, paddingPlacement } if parsedURL, err := url.Parse(headerValue); err == nil { paddingPlacement := PlacementQueryInHeader + "=" + header + ", key=" + key return parsedURL.Query().Get(key), paddingPlacement } } queryValue := req.URL.Query().Get(key) if queryValue != "" { paddingPlacement := PlacementQuery + ", key=" + key return queryValue, paddingPlacement } return "", "" } func (c *Config) IsPaddingValid(paddingValue string, from, to int, method PaddingMethod) bool { if paddingValue == "" { return false } if to <= 0 { if r, err := c.GetNormalizedXPaddingBytes(); err == nil { from, to = r.Min, r.Max } } switch method { case PaddingMethodRepeatX: n := len(paddingValue) return n >= from && n <= to case PaddingMethodTokenish: const tolerance = validationTolerance n := int(hpack.HuffmanEncodeLength(paddingValue)) f := from - tolerance t := to + tolerance if f < 0 { f = 0 } return n >= f && n <= t default: n := len(paddingValue) return n >= from && n <= to } } ================================================ FILE: core/Clash.Meta/tunnel/connection.go ================================================ package tunnel import ( "context" "errors" "net" "net/netip" "sync" "time" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) type packetSender struct { ctx context.Context cancel context.CancelFunc ch chan C.PacketAdapter // destination NAT mapping originToTarget map[string]netip.Addr targetToOrigin map[netip.Addr]netip.Addr mappingMutex sync.RWMutex } // newPacketSender return a chan based C.PacketSender // It ensures that packets can be sent sequentially and without blocking func newPacketSender() C.PacketSender { ctx, cancel := context.WithCancel(context.Background()) ch := make(chan C.PacketAdapter, senderCapacity) return &packetSender{ ctx: ctx, cancel: cancel, ch: ch, originToTarget: make(map[string]netip.Addr), targetToOrigin: make(map[netip.Addr]netip.Addr), } } func (s *packetSender) AddMapping(originMetadata *C.Metadata, metadata *C.Metadata) { s.mappingMutex.Lock() defer s.mappingMutex.Unlock() originKey := originMetadata.String() originAddr := originMetadata.DstIP targetAddr := metadata.DstIP if addr := s.originToTarget[originKey]; !addr.IsValid() { // overwrite only if the record is illegal s.originToTarget[originKey] = targetAddr } if addr := s.targetToOrigin[targetAddr]; !addr.IsValid() { // overwrite only if the record is illegal s.targetToOrigin[targetAddr] = originAddr } } func (s *packetSender) RestoreReadFrom(addr netip.Addr) netip.Addr { s.mappingMutex.RLock() defer s.mappingMutex.RUnlock() if originAddr := s.targetToOrigin[addr]; originAddr.IsValid() { return originAddr } return addr } func (s *packetSender) processPacket(pc C.PacketConn, packet C.PacketAdapter) { defer packet.Drop() metadata := packet.Metadata() var addr *net.UDPAddr s.mappingMutex.RLock() targetAddr := s.originToTarget[metadata.String()] s.mappingMutex.RUnlock() if targetAddr.IsValid() { addr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(targetAddr, metadata.DstPort)) } if addr == nil { originMetadata := metadata // save origin metadata metadata = metadata.Clone() // don't modify PacketAdapter's metadata _ = preHandleMetadata(metadata) // error was pre-checked metadata = metadata.Pure() if metadata.Host != "" { // TODO: ResolveUDP may take a long time to block the Process loop // but we want keep sequence sending so can't open a new goroutine if err := pc.ResolveUDP(s.ctx, metadata); err != nil { log.Warnln("[UDP] Resolve Ip error: %s", err) return } } if !metadata.DstIP.IsValid() { log.Warnln("[UDP] Destination ip not valid: %#v", metadata) return } s.AddMapping(originMetadata, metadata) addr = metadata.UDPAddr() } _ = handleUDPToRemote(packet, pc, addr) } func (s *packetSender) Process(pc C.PacketConn, proxy C.WriteBackProxy) { for { select { case <-s.ctx.Done(): return // sender closed case packet := <-s.ch: if proxy != nil { proxy.UpdateWriteBack(packet) } s.processPacket(pc, packet) } } } func (s *packetSender) dropAll() { for { select { case data := <-s.ch: data.Drop() // drop all data still in chan default: return // no data, exit goroutine } } } func (s *packetSender) Send(packet C.PacketAdapter) { select { case <-s.ctx.Done(): packet.Drop() // sender closed before Send() return default: } select { case s.ch <- packet: // put ok, so don't drop packet, will process by other side of chan case <-s.ctx.Done(): packet.Drop() // sender closed when putting data to chan default: packet.Drop() // chan is full } } func (s *packetSender) Close() { s.cancel() s.dropAll() } func (s *packetSender) DoSniff(metadata *C.Metadata) error { return nil } func handleUDPToRemote(packet C.UDPPacket, pc C.PacketConn, addr *net.UDPAddr) error { if addr == nil { return errors.New("udp addr invalid") } if _, err := pc.WriteTo(packet.Data(), addr); err != nil { return err } // reset timeout _ = pc.SetReadDeadline(time.Now().Add(udpTimeout)) return nil } func handleUDPToLocal(writeBack C.WriteBack, pc C.PacketConn, sender C.PacketSender, key string, oAddrPort netip.AddrPort) { defer func() { sender.Close() _ = pc.Close() closeAllLocalCoon(key) natTable.Delete(key) }() for { _ = pc.SetReadDeadline(time.Now().Add(udpTimeout)) data, put, from, err := pc.WaitReadFrom() if err != nil { return } fromUDPAddr, isUDPAddr := from.(*net.UDPAddr) if !isUDPAddr { fromUDPAddr = net.UDPAddrFromAddrPort(oAddrPort) // oAddrPort was Unmapped log.Warnln("server return a [%T](%s) which isn't a *net.UDPAddr, force replace to (%s), this may be caused by a wrongly implemented server", from, from, oAddrPort) } else if fromUDPAddr == nil { fromUDPAddr = net.UDPAddrFromAddrPort(oAddrPort) // oAddrPort was Unmapped log.Warnln("server return a nil *net.UDPAddr, force replace to (%s), this may be caused by a wrongly implemented server", oAddrPort) } fromAddrPort := fromUDPAddr.AddrPort() fromAddr := fromAddrPort.Addr().Unmap() // restore DestinationNAT fromAddr = sender.RestoreReadFrom(fromAddr).Unmap() fromAddrPort = netip.AddrPortFrom(fromAddr, fromAddrPort.Port()) _, err = writeBack.WriteBack(data, net.UDPAddrFromAddrPort(fromAddrPort)) if put != nil { put() } if err != nil { return } } } func closeAllLocalCoon(lAddr string) { natTable.RangeForLocalConn(lAddr, func(key string, value *net.UDPConn) bool { conn := value conn.Close() log.Debugln("Closing TProxy local conn... lAddr=%s rAddr=%s", lAddr, key) return true }) } func handleSocket(inbound, outbound net.Conn) { N.Relay(inbound, outbound) } ================================================ FILE: core/Clash.Meta/tunnel/dns_dialer.go ================================================ package tunnel // WARNING: all function in this file should only be using in dns module import ( "context" "fmt" "net" "strings" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/tunnel/statistic" ) const DnsRespectRules = "RULES" type DNSDialer struct { r resolver.Resolver proxyAdapter C.ProxyAdapter proxyName string } func NewDNSDialer(r resolver.Resolver, proxyAdapter C.ProxyAdapter, proxyName string) *DNSDialer { return &DNSDialer{r: r, proxyAdapter: proxyAdapter, proxyName: proxyName} } func (d *DNSDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { r := d.r proxyName := d.proxyName proxyAdapter := d.proxyAdapter var opts []dialer.Option var rule C.Rule metadata := &C.Metadata{ NetWork: C.TCP, Type: C.INNER, } err := metadata.SetRemoteAddress(addr) // tcp can resolve host by remote if err != nil { return nil, err } if !strings.Contains(network, "tcp") { metadata.NetWork = C.UDP if !metadata.Resolved() { // udp must resolve host first dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) if err != nil { return nil, err } metadata.DstIP = dstIP } } if proxyAdapter == nil && len(proxyName) != 0 { if proxyName == DnsRespectRules { if !metadata.Resolved() { // resolve here before resolveMetadata to avoid its inner resolver.ResolveIP dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) if err != nil { return nil, err } metadata.DstIP = dstIP } proxyAdapter, rule, err = resolveMetadata(metadata) if err != nil { return nil, err } } else { var ok bool proxyAdapter, ok = Proxies()[proxyName] if ok { metadata.SpecialProxy = proxyName // just for log } else { opts = append(opts, dialer.WithInterface(proxyName)) } } } if metadata.NetWork == C.TCP { if proxyAdapter == nil { opts = append(opts, dialer.WithResolver(r)) return dialer.DialContext(ctx, network, addr, opts...) } if proxyAdapter.IsL3Protocol(metadata) { // L3 proxy should resolve domain before to avoid loopback if !metadata.Resolved() { dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) if err != nil { return nil, err } metadata.DstIP = dstIP } metadata.Host = "" // clear host to avoid double resolve in proxy } conn, err := proxyAdapter.DialContext(ctx, metadata) if err != nil { logMetadataErr(metadata, rule, proxyAdapter, err) return nil, err } logMetadata(metadata, rule, conn) conn = statistic.NewTCPTracker(conn, statistic.DefaultManager, metadata, rule, 0, 0, false) return conn, nil } else { if proxyAdapter == nil { return dialer.DialContext(ctx, network, metadata.AddrPort().String(), opts...) } if !proxyAdapter.SupportUDP() { return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) } packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata) if err != nil { logMetadataErr(metadata, rule, proxyAdapter, err) return nil, err } logMetadata(metadata, rule, packetConn) packetConn = statistic.NewUDPTracker(packetConn, statistic.DefaultManager, metadata, rule, 0, 0, false) return N.NewBindPacketConn(packetConn, metadata.UDPAddr()), nil } } func (d *DNSDialer) ListenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) { r := d.r proxyAdapter := d.proxyAdapter proxyName := d.proxyName var opts []dialer.Option metadata := &C.Metadata{ NetWork: C.UDP, Type: C.INNER, } err := metadata.SetRemoteAddress(addr) if err != nil { return nil, err } if !metadata.Resolved() { // udp must resolve host first dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) if err != nil { return nil, err } metadata.DstIP = dstIP } var rule C.Rule if proxyAdapter == nil { if proxyName == DnsRespectRules { proxyAdapter, rule, err = resolveMetadata(metadata) if err != nil { return nil, err } } else { var ok bool proxyAdapter, ok = Proxies()[proxyName] if ok { metadata.SpecialProxy = proxyName // just for log } else { opts = append(opts, dialer.WithInterface(proxyName)) } } } if proxyAdapter == nil { return dialer.NewDialer(opts...).ListenPacket(ctx, network, "", metadata.AddrPort()) } if !proxyAdapter.SupportUDP() { return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) } packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata) if err != nil { logMetadataErr(metadata, rule, proxyAdapter, err) return nil, err } logMetadata(metadata, rule, packetConn) packetConn = statistic.NewUDPTracker(packetConn, statistic.DefaultManager, metadata, rule, 0, 0, false) return packetConn, nil } ================================================ FILE: core/Clash.Meta/tunnel/mode.go ================================================ package tunnel import ( "errors" "strings" ) type TunnelMode int32 // ModeMapping is a mapping for Mode enum var ModeMapping = map[string]TunnelMode{ Global.String(): Global, Rule.String(): Rule, Direct.String(): Direct, } const ( Global TunnelMode = iota Rule Direct ) // UnmarshalText unserialize Mode func (m *TunnelMode) UnmarshalText(data []byte) error { mode, exist := ModeMapping[strings.ToLower(string(data))] if !exist { return errors.New("invalid mode") } *m = mode return nil } // MarshalText serialize Mode func (m TunnelMode) MarshalText() ([]byte, error) { return []byte(m.String()), nil } func (m TunnelMode) String() string { switch m { case Global: return "global" case Rule: return "rule" case Direct: return "direct" default: return "Unknown" } } ================================================ FILE: core/Clash.Meta/tunnel/statistic/manager.go ================================================ package statistic import ( "os" "runtime" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/xsync" ) var DefaultManager *Manager func init() { DefaultManager = &Manager{ uploadTemp: atomic.NewInt64(0), downloadTemp: atomic.NewInt64(0), uploadBlip: atomic.NewInt64(0), downloadBlip: atomic.NewInt64(0), uploadTotal: atomic.NewInt64(0), downloadTotal: atomic.NewInt64(0), proxyUploadTemp: atomic.NewInt64(0), proxyDownloadTemp: atomic.NewInt64(0), proxyUploadBlip: atomic.NewInt64(0), proxyDownloadBlip: atomic.NewInt64(0), proxyUploadTotal: atomic.NewInt64(0), proxyDownloadTotal: atomic.NewInt64(0), pid: int32(os.Getpid()), } go DefaultManager.handle() } type Manager struct { connections xsync.Map[string, Tracker] uploadTemp atomic.Int64 downloadTemp atomic.Int64 uploadBlip atomic.Int64 downloadBlip atomic.Int64 uploadTotal atomic.Int64 downloadTotal atomic.Int64 proxyUploadTemp atomic.Int64 proxyDownloadTemp atomic.Int64 proxyUploadBlip atomic.Int64 proxyDownloadBlip atomic.Int64 proxyUploadTotal atomic.Int64 proxyDownloadTotal atomic.Int64 pid int32 memory uint64 } func (m *Manager) Join(c Tracker) { m.connections.Store(c.ID(), c) } func (m *Manager) Leave(c Tracker) { m.connections.Delete(c.ID()) if DefaultRequestNotify != nil { DefaultRequestNotify(c) } } func (m *Manager) Get(id string) (c Tracker) { if value, ok := m.connections.Load(id); ok { c = value } return } func (m *Manager) Range(f func(c Tracker) bool) { m.connections.Range(func(key string, value Tracker) bool { return f(value) }) } func (m *Manager) PushUploaded(lastChain string, size int64) { if lastChain != "DIRECT" { m.proxyUploadTemp.Add(size) m.proxyUploadTotal.Add(size) } m.uploadTemp.Add(size) m.uploadTotal.Add(size) } func (m *Manager) PushDownloaded(lastChain string, size int64) { if lastChain != "DIRECT" { m.proxyDownloadTemp.Add(size) m.proxyDownloadTotal.Add(size) } m.downloadTemp.Add(size) m.downloadTotal.Add(size) } func (m *Manager) Now() (up int64, down int64) { return m.uploadBlip.Load(), m.downloadBlip.Load() } func (m *Manager) Total() (up, down int64) { return m.uploadTotal.Load(), m.downloadTotal.Load() } func (m *Manager) Memory() uint64 { m.updateMemory() return m.memory } func (m *Manager) ResetStatistic() { m.uploadTemp.Store(0) m.uploadBlip.Store(0) m.uploadTotal.Store(0) m.downloadTemp.Store(0) m.downloadBlip.Store(0) m.downloadTotal.Store(0) m.proxyUploadTemp.Store(0) m.proxyUploadBlip.Store(0) m.proxyUploadTotal.Store(0) m.proxyDownloadTemp.Store(0) m.proxyDownloadBlip.Store(0) m.proxyDownloadTotal.Store(0) } func (m *Manager) updateMemory() { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) m.memory = memStats.StackInuse + memStats.HeapInuse } func (m *Manager) handle() { ticker := time.NewTicker(time.Second) for range ticker.C { m.uploadBlip.Store(m.uploadTemp.Swap(0)) m.downloadBlip.Store(m.downloadTemp.Swap(0)) m.proxyUploadBlip.Store(m.proxyUploadTemp.Swap(0)) m.proxyDownloadBlip.Store(m.proxyDownloadTemp.Swap(0)) } } type Snapshot struct { DownloadTotal int64 `json:"downloadTotal"` UploadTotal int64 `json:"uploadTotal"` Connections []TrackerInfo `json:"connections"` Memory uint64 `json:"memory"` } func (m *Manager) Snapshot() *Snapshot { m.updateMemory() connections := make([]TrackerInfo, 0) m.connections.Range(func(key string, value Tracker) bool { connections = append(connections, *value.Info()) return true }) return &Snapshot{ DownloadTotal: m.downloadTotal.Load(), UploadTotal: m.uploadTotal.Load(), Connections: connections, Memory: m.memory, } } ================================================ FILE: core/Clash.Meta/tunnel/statistic/patch.go ================================================ package statistic type RequestNotify func(c Tracker) var DefaultRequestNotify RequestNotify func (m *Manager) TotalTraffic(onlyProxy bool) (up, down int64) { if onlyProxy { return m.proxyUploadTotal.Load(), m.proxyDownloadTotal.Load() } return m.uploadTotal.Load(), m.downloadTotal.Load() } func (m *Manager) NowTraffic(onlyProxy bool) (up, down int64) { if onlyProxy { return m.proxyUploadBlip.Load(), m.proxyDownloadBlip.Load() } return m.uploadBlip.Load(), m.downloadBlip.Load() } ================================================ FILE: core/Clash.Meta/tunnel/statistic/patch_android.go ================================================ //go:build android package statistic ================================================ FILE: core/Clash.Meta/tunnel/statistic/tracker.go ================================================ package statistic import ( "io" "net" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/gofrs/uuid/v5" ) type Tracker interface { ID() string Close() error Info() *TrackerInfo C.Connection } type TrackerInfo struct { UUID uuid.UUID `json:"id"` Metadata *C.Metadata `json:"metadata"` UploadTotal atomic.Int64 `json:"upload"` DownloadTotal atomic.Int64 `json:"download"` Start time.Time `json:"start"` Chain C.Chain `json:"chains"` ProviderChain C.Chain `json:"providerChains"` Rule string `json:"rule"` RulePayload string `json:"rulePayload"` } type tcpTracker struct { C.Conn `json:"-"` *TrackerInfo manager *Manager pushToManager bool `json:"-"` closed atomic.Bool `json:"-"` } func (tt *tcpTracker) ID() string { return tt.UUID.String() } func (tt *tcpTracker) Info() *TrackerInfo { return tt.TrackerInfo } func (tt *tcpTracker) Read(b []byte) (int, error) { n, err := tt.Conn.Read(b) download := int64(n) if tt.pushToManager { tt.manager.PushDownloaded(tt.Chain.Last(), download) } tt.DownloadTotal.Add(download) return n, err } func (tt *tcpTracker) ReadBuffer(buffer *buf.Buffer) (err error) { err = tt.Conn.ReadBuffer(buffer) download := int64(buffer.Len()) if tt.pushToManager { tt.manager.PushDownloaded(tt.Chain.Last(), download) } tt.DownloadTotal.Add(download) return } func (tt *tcpTracker) UnwrapReader() (io.Reader, []N.CountFunc) { return tt.Conn, []N.CountFunc{func(download int64) { if tt.pushToManager { tt.manager.PushDownloaded(tt.Chain.Last(), download) } tt.DownloadTotal.Add(download) }} } func (tt *tcpTracker) Write(b []byte) (int, error) { n, err := tt.Conn.Write(b) upload := int64(n) if tt.pushToManager { tt.manager.PushUploaded(tt.Chain.Last(), upload) } tt.UploadTotal.Add(upload) return n, err } func (tt *tcpTracker) WriteBuffer(buffer *buf.Buffer) (err error) { upload := int64(buffer.Len()) err = tt.Conn.WriteBuffer(buffer) if tt.pushToManager { tt.manager.PushUploaded(tt.Chain.Last(), upload) } tt.UploadTotal.Add(upload) return } func (tt *tcpTracker) UnwrapWriter() (io.Writer, []N.CountFunc) { return tt.Conn, []N.CountFunc{func(upload int64) { if tt.pushToManager { tt.manager.PushUploaded(tt.Chain.Last(), upload) } tt.UploadTotal.Add(upload) }} } func (tt *tcpTracker) Close() error { if tt.closed.CompareAndSwap(false, true) { tt.manager.Leave(tt) } return tt.Conn.Close() } func (tt *tcpTracker) Upstream() any { return tt.Conn } func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64, pushToManager bool) *tcpTracker { metadata.RemoteDst = conn.RemoteDestination() chains := conn.Chains() tt := &tcpTracker{ Conn: conn, manager: manager, TrackerInfo: &TrackerInfo{ UUID: utils.NewUUIDV4(), Start: time.Now(), Metadata: metadata, Chain: chains, ProviderChain: conn.ProviderChains(), Rule: "", UploadTotal: atomic.NewInt64(uploadTotal), DownloadTotal: atomic.NewInt64(downloadTotal), }, pushToManager: pushToManager, } if pushToManager { if uploadTotal > 0 { manager.PushUploaded(chains.Last(), uploadTotal) } if downloadTotal > 0 { manager.PushDownloaded(chains.Last(), downloadTotal) } } if rule != nil { tt.TrackerInfo.Rule = rule.RuleType().String() tt.TrackerInfo.RulePayload = rule.Payload() } manager.Join(tt) return tt } type udpTracker struct { C.PacketConn `json:"-"` *TrackerInfo manager *Manager pushToManager bool `json:"-"` closed atomic.Bool `json:"-"` } func (ut *udpTracker) ID() string { return ut.UUID.String() } func (ut *udpTracker) Info() *TrackerInfo { return ut.TrackerInfo } func (ut *udpTracker) ReadFrom(b []byte) (int, net.Addr, error) { n, addr, err := ut.PacketConn.ReadFrom(b) download := int64(n) if ut.pushToManager { ut.manager.PushDownloaded(ut.Chain.Last(), download) } ut.DownloadTotal.Add(download) return n, addr, err } func (ut *udpTracker) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { data, put, addr, err = ut.PacketConn.WaitReadFrom() download := int64(len(data)) if ut.pushToManager { ut.manager.PushDownloaded(ut.Chain.Last(), download) } ut.DownloadTotal.Add(download) return } func (ut *udpTracker) WriteTo(b []byte, addr net.Addr) (int, error) { n, err := ut.PacketConn.WriteTo(b, addr) upload := int64(n) if ut.pushToManager { ut.manager.PushUploaded(ut.Chain.Last(), upload) } ut.UploadTotal.Add(upload) return n, err } func (ut *udpTracker) Close() error { if ut.closed.CompareAndSwap(false, true) { ut.manager.Leave(ut) } return ut.PacketConn.Close() } func (ut *udpTracker) Upstream() any { return ut.PacketConn } func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64, pushToManager bool) *udpTracker { metadata.RemoteDst = conn.RemoteDestination() chains := conn.Chains() ut := &udpTracker{ PacketConn: conn, manager: manager, TrackerInfo: &TrackerInfo{ UUID: utils.NewUUIDV4(), Start: time.Now(), Metadata: metadata, Chain: chains, ProviderChain: conn.ProviderChains(), Rule: "", UploadTotal: atomic.NewInt64(uploadTotal), DownloadTotal: atomic.NewInt64(downloadTotal), }, pushToManager: pushToManager, } if pushToManager { if uploadTotal > 0 { manager.PushUploaded(chains.Last(), uploadTotal) } if downloadTotal > 0 { manager.PushDownloaded(chains.Last(), downloadTotal) } } if rule != nil { ut.TrackerInfo.Rule = rule.RuleType().String() ut.TrackerInfo.RulePayload = rule.Payload() } manager.Join(ut) return ut } ================================================ FILE: core/Clash.Meta/tunnel/status.go ================================================ package tunnel import ( "errors" "strings" ) type TunnelStatus int32 // StatusMapping is a mapping for Status enum var StatusMapping = map[string]TunnelStatus{ Suspend.String(): Suspend, Inner.String(): Inner, Running.String(): Running, } const ( Suspend TunnelStatus = iota Inner Running ) // UnmarshalText unserialize Status func (s *TunnelStatus) UnmarshalText(data []byte) error { status, exist := StatusMapping[strings.ToLower(string(data))] if !exist { return errors.New("invalid status") } *s = status return nil } // MarshalText serialize Status func (s TunnelStatus) MarshalText() ([]byte, error) { return []byte(s.String()), nil } func (s TunnelStatus) String() string { switch s { case Suspend: return "suspend" case Inner: return "inner" case Running: return "running" default: return "Unknown" } } ================================================ FILE: core/Clash.Meta/tunnel/tunnel.go ================================================ package tunnel import ( "context" "errors" "fmt" "net" "net/netip" "path/filepath" "runtime" "strings" "sync" "time" "github.com/metacubex/mihomo/common/atomic" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/loopback" "github.com/metacubex/mihomo/component/nat" "github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/slowdown" "github.com/metacubex/mihomo/component/sniffer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" P "github.com/metacubex/mihomo/constant/provider" icontext "github.com/metacubex/mihomo/context" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel/statistic" ) const ( queueCapacity = 64 // chan capacity tcpQueue and udpQueue senderCapacity = 128 // chan capacity of PacketSender ) var ( status = atomic.NewInt32Enum(Suspend) udpInit sync.Once udpQueues []chan C.PacketAdapter natTable = nat.New() rules []C.Rule listeners = make(map[string]C.InboundListener) subRules map[string][]C.Rule proxies = make(map[string]C.Proxy) providers map[string]P.ProxyProvider ruleProviders map[string]P.RuleProvider configMux sync.RWMutex // for compatibility, lazy init tcpQueue chan C.ConnContext tcpInOnce sync.Once udpQueue chan C.PacketAdapter udpInOnce sync.Once // Outbound Rule mode = Rule // default timeout for UDP session udpTimeout = 60 * time.Second findProcessMode = atomic.NewInt32Enum(process.FindProcessStrict) snifferDispatcher *sniffer.Dispatcher sniffingEnable = false ruleUpdateCallback = utils.NewCallback[P.RuleProvider]() ) type tunnel struct{} var Tunnel = tunnel{} var _ C.Tunnel = Tunnel var _ P.Tunnel = Tunnel func (t tunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { connCtx := icontext.NewConnContext(conn, metadata) handleTCPConn(connCtx) } func initUDP() { numUDPWorkers := 4 if num := runtime.GOMAXPROCS(0); num > numUDPWorkers { numUDPWorkers = num } udpQueues = make([]chan C.PacketAdapter, numUDPWorkers) for i := 0; i < numUDPWorkers; i++ { queue := make(chan C.PacketAdapter, queueCapacity) udpQueues[i] = queue go processUDP(queue) } } func (t tunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) { udpInit.Do(initUDP) packetAdapter := C.NewPacketAdapter(packet, metadata) key := packetAdapter.Key() hash := utils.MapHash(key) queueNo := uint(hash) % uint(len(udpQueues)) select { case udpQueues[queueNo] <- packetAdapter: default: packet.Drop() } } func (t tunnel) NatTable() C.NatTable { return natTable } func (t tunnel) Providers() map[string]P.ProxyProvider { return providers } func (t tunnel) RuleProviders() map[string]P.RuleProvider { return ruleProviders } func (t tunnel) RuleUpdateCallback() *utils.Callback[P.RuleProvider] { return ruleUpdateCallback } func OnSuspend() { status.Store(Suspend) } func OnInnerLoading() { status.Store(Inner) } func OnRunning() { status.Store(Running) } func Status() TunnelStatus { return status.Load() } func SetSniffing(b bool) { if snifferDispatcher.Enable() { configMux.Lock() sniffingEnable = b configMux.Unlock() } } func IsSniffing() bool { return sniffingEnable } // TCPIn return fan-in queue // Deprecated: using Tunnel instead func TCPIn() chan<- C.ConnContext { tcpInOnce.Do(func() { tcpQueue = make(chan C.ConnContext, queueCapacity) go func() { for connCtx := range tcpQueue { go handleTCPConn(connCtx) } }() }) return tcpQueue } // UDPIn return fan-in udp queue // Deprecated: using Tunnel instead func UDPIn() chan<- C.PacketAdapter { udpInOnce.Do(func() { udpQueue = make(chan C.PacketAdapter, queueCapacity) go func() { for packet := range udpQueue { Tunnel.HandleUDPPacket(packet, packet.Metadata()) } }() }) return udpQueue } // NatTable return nat table func NatTable() C.NatTable { return natTable } // Rules return all rules func Rules() []C.Rule { return rules } func Listeners() map[string]C.InboundListener { return listeners } // UpdateRules handle update rules func UpdateRules(newRules []C.Rule, newSubRule map[string][]C.Rule, rp map[string]P.RuleProvider) { configMux.Lock() rules = newRules ruleProviders = rp subRules = newSubRule configMux.Unlock() } // Proxies return all proxies func Proxies() map[string]C.Proxy { return proxies } // Providers return all compatible providers func Providers() map[string]P.ProxyProvider { return providers } // ProxiesWithProviders return all proxies and providers func ProxiesWithProviders() map[string]C.Proxy { allProxies := make(map[string]C.Proxy) for name, proxy := range proxies { allProxies[name] = proxy } for _, p := range providers { for _, proxy := range p.Proxies() { name := proxy.Name() allProxies[name] = proxy } } return allProxies } // RuleProviders return all loaded rule providers func RuleProviders() map[string]P.RuleProvider { return ruleProviders } // UpdateProxies handle update proxies func UpdateProxies(newProxies map[string]C.Proxy, newProviders map[string]P.ProxyProvider) { configMux.Lock() proxies = newProxies providers = newProviders configMux.Unlock() } func UpdateListeners(newListeners map[string]C.InboundListener) { configMux.Lock() defer configMux.Unlock() listeners = newListeners } func UpdateSniffer(dispatcher *sniffer.Dispatcher) { configMux.Lock() snifferDispatcher = dispatcher sniffingEnable = dispatcher.Enable() configMux.Unlock() } // Mode return current mode func Mode() TunnelMode { return mode } // SetMode change the mode of tunnel func SetMode(m TunnelMode) { mode = m } func FindProcessMode() process.FindProcessMode { return findProcessMode.Load() } // SetFindProcessMode replace SetAlwaysFindProcess // always find process info if legacyAlways = true or mode.Always() = true, may be increase many memory func SetFindProcessMode(mode process.FindProcessMode) { findProcessMode.Store(mode) } func isHandle(t C.Type) bool { status := status.Load() return status == Running || (status == Inner && t == C.INNER) } func fixMetadata(metadata *C.Metadata) { // first unmap dstIP metadata.DstIP = metadata.DstIP.Unmap() // handle IP string on host if ip, err := netip.ParseAddr(metadata.Host); err == nil { metadata.DstIP = ip.Unmap() metadata.Host = "" } } func needLookupIP(metadata *C.Metadata) bool { return resolver.MappingEnabled() && metadata.Host == "" && metadata.DstIP.IsValid() } func preHandleMetadata(metadata *C.Metadata) error { // preprocess enhanced-mode metadata if needLookupIP(metadata) { host, exist := resolver.FindHostByIP(metadata.DstIP) if exist { metadata.Host = host metadata.DNSMode = C.DNSMapping if resolver.IsFakeIP(metadata.DstIP) { // only clear dstIP if it is confirmed to be a fake IP metadata.DstIP = netip.Addr{} metadata.DNSMode = C.DNSFakeIP } else if node, ok := resolver.DefaultHosts.Search(host, false); ok { // redir-host should lookup the hosts metadata.DstIP, _ = node.RandIP() } else if node != nil && node.IsDomain { metadata.Host = node.Domain } } else if resolver.IsFakeIP(metadata.DstIP) { return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) } } else if node, ok := resolver.DefaultHosts.Search(metadata.Host, true); ok { // try use domain mapping metadata.Host = node.Domain } return nil } func resolveMetadata(metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { if metadata.SpecialProxy != "" { var exist bool proxy, exist = proxies[metadata.SpecialProxy] if !exist { err = fmt.Errorf("proxy %s not found", metadata.SpecialProxy) } return } var ( resolved bool attemptProcessLookup = metadata.Type != C.INNER ) if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { metadata.DstIP, _ = node.RandIP() resolved = true } helper := C.RuleMatchHelper{ ResolveIP: func() { if !resolved && metadata.Host != "" && !metadata.Resolved() { ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) defer cancel() ip, err := resolver.ResolveIP(ctx, metadata.Host) if err != nil { log.Debugln("[DNS] resolve %s error: %s", metadata.Host, err.Error()) } else { log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String()) metadata.DstIP = ip } resolved = true } }, FindProcess: func() { if attemptProcessLookup { attemptProcessLookup = false if !features.Android { // normal check for process uid, path, err := process.FindProcessName(metadata.NetWork.String(), metadata.SrcIP, int(metadata.SrcPort)) if err != nil { log.Debugln("[Process] find process error for %s: %v", metadata.String(), err) } else { metadata.Process = filepath.Base(path) metadata.ProcessPath = path metadata.Uid = uid if pkg, err := process.FindPackageName(metadata); err == nil { // for android (not CMFA) package names metadata.Process = pkg } } } else { // check package names pkg, err := process.FindPackageName(metadata) if err != nil { log.Debugln("[Process] find process error for %s: %v", metadata.String(), err) } else { metadata.Process = pkg } } } }, } switch FindProcessMode() { case process.FindProcessAlways: helper.FindProcess() helper.FindProcess = nil case process.FindProcessOff: helper.FindProcess = nil } switch mode { case Direct: proxy = proxies["DIRECT"] case Global: proxy = proxies["GLOBAL"] // Rule default: proxy, rule, err = match(metadata, helper) } return } // processUDP starts a loop to handle udp packet func processUDP(queue chan C.PacketAdapter) { for conn := range queue { handleUDPConn(conn) } } func handleUDPConn(packet C.PacketAdapter) { if !isHandle(packet.Metadata().Type) { packet.Drop() return } metadata := packet.Metadata() if !metadata.Valid() { packet.Drop() log.Warnln("[Metadata] not valid: %#v", metadata) return } fixMetadata(metadata) // fix some metadata not set via metadata.SetRemoteAddr or metadata.SetRemoteAddress if err := preHandleMetadata(metadata.Clone()); err != nil { // precheck without modify metadata packet.Drop() log.Debugln("[Metadata PreHandle] error: %s", err) return } key := packet.Key() sender, loaded := natTable.GetOrCreate(key, func() C.PacketSender { sender := newPacketSender() if sniffingEnable && snifferDispatcher.Enable() { return snifferDispatcher.UDPSniff(packet, sender) } return sender }) if !loaded { dial := func() (C.PacketConn, C.WriteBackProxy, error) { originMetadata := metadata // save origin metadata metadata = metadata.Clone() // don't modify PacketAdapter's metadata if err := sender.DoSniff(metadata); err != nil { log.Warnln("[UDP] DoSniff error: %s", err.Error()) return nil, nil, err } _ = preHandleMetadata(metadata) // error was pre-checked proxy, rule, err := resolveMetadata(metadata) if err != nil { log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) return nil, nil, err } dialMetadata := metadata.Pure() ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) defer cancel() rawPc, err := retry(ctx, func(ctx context.Context) (C.PacketConn, error) { return proxy.ListenPacketContext(ctx, dialMetadata) }, func(err error) { logMetadataErr(metadata, rule, proxy, err) }) if err != nil { return nil, nil, err } logMetadata(metadata, rule, rawPc) pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule, 0, 0, true) sender.AddMapping(originMetadata, dialMetadata) oAddrPort := dialMetadata.AddrPort() writeBackProxy := nat.NewWriteBackProxy(packet) go handleUDPToLocal(writeBackProxy, pc, sender, key, oAddrPort) return pc, writeBackProxy, nil } go func() { pc, proxy, err := dial() if err != nil { sender.Close() natTable.Delete(key) return } sender.Process(pc, proxy) }() } sender.Send(packet) // nonblocking } func handleTCPConn(connCtx C.ConnContext) { if !isHandle(connCtx.Metadata().Type) { _ = connCtx.Conn().Close() return } defer func(conn net.Conn) { _ = conn.Close() }(connCtx.Conn()) metadata := connCtx.Metadata() if !metadata.Valid() { log.Warnln("[Metadata] not valid: %#v", metadata) return } fixMetadata(metadata) // fix some metadata not set via metadata.SetRemoteAddr or metadata.SetRemoteAddress preHandleFailed := false if err := preHandleMetadata(metadata); err != nil { log.Debugln("[Metadata PreHandle] error: %s", err) preHandleFailed = true } conn := connCtx.Conn() conn.ResetPeeked() // reset before sniffer if sniffingEnable && snifferDispatcher.Enable() { // Try to sniff a domain when `preHandleMetadata` failed, this is usually // caused by a "Fake DNS record missing" error when enhanced-mode is fake-ip. if snifferDispatcher.TCPSniff(conn, metadata) { // we now have a domain name preHandleFailed = false } } // If both trials have failed, we can do nothing but give up if preHandleFailed { log.Debugln("[Metadata PreHandle] failed to sniff a domain for connection %s --> %s, give up", metadata.SourceDetail(), metadata.RemoteAddress()) return } peekMutex := sync.Mutex{} if !conn.Peeked() { peekMutex.Lock() go func() { defer peekMutex.Unlock() _ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) _, _ = conn.Peek(1) _ = conn.SetReadDeadline(time.Time{}) }() } proxy, rule, err := resolveMetadata(metadata) if err != nil { log.Warnln("[Metadata] parse failed: %s", err.Error()) return } dialMetadata := metadata if len(metadata.Host) > 0 { if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { if dstIp, _ := node.RandIP(); !resolver.IsFakeIP(dstIp) { dialMetadata.DstIP = dstIp dialMetadata.DNSMode = C.DNSHosts dialMetadata = dialMetadata.Pure() } } } var peekBytes []byte var peekLen int ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) defer cancel() remoteConn, err := retry(ctx, func(ctx context.Context) (remoteConn C.Conn, err error) { remoteConn, err = proxy.DialContext(ctx, dialMetadata) if err != nil { return } if N.NeedHandshake(remoteConn) { defer func() { for _, chain := range remoteConn.Chains() { if chain == "REJECT" { err = nil return } } if err != nil { remoteConn = nil } }() peekMutex.Lock() defer peekMutex.Unlock() peekBytes, _ = conn.Peek(conn.Buffered()) _, err = remoteConn.Write(peekBytes) if err != nil { return } if peekLen = len(peekBytes); peekLen > 0 { _, _ = conn.Discard(peekLen) } } return }, func(err error) { logMetadataErr(metadata, rule, proxy, err) }) if err != nil { return } logMetadata(metadata, rule, remoteConn) remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, int64(peekLen), 0, true) defer func(remoteConn C.Conn) { _ = remoteConn.Close() }(remoteConn) _ = conn.SetReadDeadline(time.Now()) // stop unfinished peek peekMutex.Lock() defer peekMutex.Unlock() _ = conn.SetReadDeadline(time.Time{}) // reset handleSocket(conn, remoteConn) } func logMetadataErr(metadata *C.Metadata, rule C.Rule, proxy C.ProxyAdapter, err error) { if rule == nil { log.Warnln("[%s] dial %s %s --> %s error: %s", strings.ToUpper(metadata.NetWork.String()), proxy.Name(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) } else { log.Warnln("[%s] dial %s (match %s/%s) %s --> %s error: %s", strings.ToUpper(metadata.NetWork.String()), proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) } } func logMetadata(metadata *C.Metadata, rule C.Rule, remoteConn C.Connection) { switch { case metadata.SpecialProxy != "": log.Infoln("[%s] %s --> %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), remoteConn.Chains().String()) case rule != nil: if rule.Payload() != "" { log.Infoln("[%s] %s --> %s match %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), fmt.Sprintf("%s(%s)", rule.RuleType().String(), rule.Payload()), remoteConn.Chains().String()) } else { log.Infoln("[%s] %s --> %s match %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), rule.RuleType().String(), remoteConn.Chains().String()) } case mode == Global: log.Infoln("[%s] %s --> %s using GLOBAL", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress()) case mode == Direct: log.Infoln("[%s] %s --> %s using DIRECT", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress()) default: log.Infoln("[%s] %s --> %s doesn't match any rule using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), remoteConn.Chains().String()) } } func match(metadata *C.Metadata, helper C.RuleMatchHelper) (C.Proxy, C.Rule, error) { configMux.RLock() defer configMux.RUnlock() for _, rule := range getRules(metadata) { if matched, ada := rule.Match(metadata, helper); matched { adapter, ok := proxies[ada] if !ok { continue } // parse multi-layer nesting passed := false for adapter := adapter; adapter != nil; adapter = adapter.Unwrap(metadata, false) { if adapter.Type() == C.Pass { passed = true break } } if passed { log.Debugln("%s match Pass rule", adapter.Name()) continue } if metadata.NetWork == C.UDP && !adapter.SupportUDP() { log.Debugln("%s UDP is not supported", adapter.Name()) continue } return adapter, rule, nil } } return proxies["DIRECT"], nil, nil } func getRules(metadata *C.Metadata) []C.Rule { if sr, ok := subRules[metadata.SpecialRules]; ok { log.Debugln("[Rule] use %s rules", metadata.SpecialRules) return sr } else { log.Debugln("[Rule] use default rules") return rules } } func shouldStopRetry(err error) bool { if errors.Is(err, resolver.ErrIPNotFound) { return true } if errors.Is(err, resolver.ErrIPVersion) { return true } if errors.Is(err, resolver.ErrIPv6Disabled) { return true } if errors.Is(err, loopback.ErrReject) { return true } return false } func retry[T any](ctx context.Context, ft func(context.Context) (T, error), fe func(err error)) (t T, err error) { s := slowdown.New() for i := 0; i < 10; i++ { t, err = ft(ctx) if err != nil { if fe != nil { fe(err) } if shouldStopRetry(err) { return } if s.Wait(ctx) == nil { continue } else { return } } else { break } } return } ================================================ FILE: core/action.go ================================================ package main import ( "encoding/json" ) type Action struct { Id string `json:"id"` Method Method `json:"method"` Data interface{} `json:"data"` } type ActionResult struct { Id string `json:"id"` Method Method `json:"method"` Data interface{} `json:"data"` Code int `json:"code"` Port int64 } func (result ActionResult) Json() ([]byte, error) { data, err := json.Marshal(result) return data, err } func (result ActionResult) success(data interface{}) { result.Code = 0 result.Data = data result.send() } func (result ActionResult) error(data interface{}) { result.Code = -1 result.Data = data result.send() } func handleAction(action *Action, result ActionResult) { switch action.Method { case initClashMethod: paramsString := action.Data.(string) result.success(handleInitClash(paramsString)) return case getIsInitMethod: result.success(handleGetIsInit()) return case forceGcMethod: handleForceGc() result.success(true) return case shutdownMethod: result.success(handleShutdown()) return case validateConfigMethod: data := []byte(action.Data.(string)) result.success(handleValidateConfig(data)) return case updateConfigMethod: data := []byte(action.Data.(string)) result.success(handleUpdateConfig(data)) return case setupConfigMethod: data := []byte(action.Data.(string)) result.success(handleSetupConfig(data)) return case getProxiesMethod: result.success(handleGetProxies()) return case changeProxyMethod: data := action.Data.(string) handleChangeProxy(data, func(value string) { result.success(value) }) return case getTrafficMethod: result.success(handleGetTraffic()) return case getTotalTrafficMethod: result.success(handleGetTotalTraffic()) return case resetTrafficMethod: handleResetTraffic() result.success(true) return case asyncTestDelayMethod: data := action.Data.(string) handleAsyncTestDelay(data, func(value string) { result.success(value) }) return case getConnectionsMethod: result.success(handleGetConnections()) return case closeConnectionsMethod: result.success(handleCloseConnections()) return case resetConnectionsMethod: result.success(handleResetConnections()) return case getConfigMethod: path := action.Data.(string) config, err := handleGetConfig(path) if err != nil { result.error(err) return } result.success(config) return case closeConnectionMethod: id := action.Data.(string) result.success(handleCloseConnection(id)) return case getExternalProvidersMethod: result.success(handleGetExternalProviders()) return case getExternalProviderMethod: externalProviderName := action.Data.(string) result.success(handleGetExternalProvider(externalProviderName)) case updateGeoDataMethod: paramsString := action.Data.(string) var params = map[string]string{} err := json.Unmarshal([]byte(paramsString), ¶ms) if err != nil { result.success(err.Error()) return } geoType := params["geo-type"] geoName := params["geo-name"] handleUpdateGeoData(geoType, geoName, func(value string) { result.success(value) }) return case updateExternalProviderMethod: providerName := action.Data.(string) handleUpdateExternalProvider(providerName, func(value string) { result.success(value) }) return case sideLoadExternalProviderMethod: paramsString := action.Data.(string) var params = map[string]string{} err := json.Unmarshal([]byte(paramsString), ¶ms) if err != nil { result.success(err.Error()) return } providerName := params["providerName"] data := params["data"] handleSideLoadExternalProvider(providerName, []byte(data), func(value string) { result.success(value) }) return case startLogMethod: handleStartLog() result.success(true) return case stopLogMethod: handleStopLog() result.success(true) return case startListenerMethod: result.success(handleStartListener()) return case stopListenerMethod: result.success(handleStopListener()) return case getCountryCodeMethod: ip := action.Data.(string) handleGetCountryCode(ip, func(value string) { result.success(value) }) return case getMemoryMethod: handleGetMemory(func(value string) { result.success(value) }) return case setStateMethod: data := action.Data.(string) handleSetState(data) result.success(true) case flushFakeIPMethod: result.success(handleFlushFakeIP()) return case flushDnsCacheMethod: handleFlushDnsCache() result.success(true) return case crashMethod: result.success(true) handleCrash() default: nextHandle(action, result) } } ================================================ FILE: core/android_bride.go ================================================ //go:build android && cgo package main /* #include typedef void (*release_object_func)(void *obj); typedef void (*protect_func)(void *tun_interface, int fd); typedef const char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid); static void protect(protect_func fn, void *tun_interface, int fd) { if (fn) { fn(tun_interface, fd); } } static const char* resolve_process(resolve_process_func fn, void *tun_interface, int protocol, const char *source, const char *target, int uid) { if (fn) { return fn(tun_interface, protocol, source, target, uid); } return ""; } static void release_object(release_object_func fn, void *obj) { if (fn) { return fn(obj); } } */ import "C" import ( "unsafe" ) var ( globalCallbacks struct { releaseObjectFunc C.release_object_func protectFunc C.protect_func resolveProcessFunc C.resolve_process_func } ) func Protect(callback unsafe.Pointer, fd int) { if globalCallbacks.protectFunc != nil { C.protect(globalCallbacks.protectFunc, callback, C.int(fd)) } } func ResolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string { if globalCallbacks.resolveProcessFunc == nil { return "" } s := C.CString(source) defer C.free(unsafe.Pointer(s)) t := C.CString(target) defer C.free(unsafe.Pointer(t)) res := C.resolve_process(globalCallbacks.resolveProcessFunc, callback, C.int(protocol), s, t, C.int(uid)) defer C.free(unsafe.Pointer(res)) return C.GoString(res) } func releaseObject(callback unsafe.Pointer) { if globalCallbacks.releaseObjectFunc == nil { return } C.release_object(globalCallbacks.releaseObjectFunc, callback) } //export registerCallbacks func registerCallbacks(markSocketFunc C.protect_func, resolveProcessFunc C.resolve_process_func, releaseObjectFunc C.release_object_func) { globalCallbacks.protectFunc = markSocketFunc globalCallbacks.resolveProcessFunc = resolveProcessFunc globalCallbacks.releaseObjectFunc = releaseObjectFunc } ================================================ FILE: core/common.go ================================================ package main import ( b "bytes" "context" "encoding/json" "errors" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/common/batch" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" cp "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub/route" "github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/log" rp "github.com/metacubex/mihomo/rules/provider" "github.com/metacubex/mihomo/tunnel" "os" "sync" ) var ( currentConfig *config.Config version = 0 isRunning = false runLock sync.Mutex mBatch, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) ) type ExternalProviders []ExternalProvider func (a ExternalProviders) Len() int { return len(a) } func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name } func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func getExternalProvidersRaw() map[string]cp.Provider { eps := make(map[string]cp.Provider) for n, p := range tunnel.Providers() { if p.VehicleType() != cp.Compatible { eps[n] = p } } for n, p := range tunnel.RuleProviders() { if p.VehicleType() != cp.Compatible { eps[n] = p } } return eps } func toExternalProvider(p cp.Provider) (*ExternalProvider, error) { switch p.(type) { case *provider.ProxySetProvider: psp := p.(*provider.ProxySetProvider) return &ExternalProvider{ Name: psp.Name(), Type: psp.Type().String(), VehicleType: psp.VehicleType().String(), Count: psp.Count(), UpdateAt: psp.UpdatedAt(), Path: psp.Vehicle().Path(), SubscriptionInfo: psp.GetSubscriptionInfo(), }, nil case *rp.RuleSetProvider: rsp := p.(*rp.RuleSetProvider) return &ExternalProvider{ Name: rsp.Name(), Type: rsp.Type().String(), VehicleType: rsp.VehicleType().String(), Count: rsp.Count(), UpdateAt: rsp.UpdatedAt(), Path: rsp.Vehicle().Path(), }, nil default: return nil, errors.New("not external provider") } } func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error { switch p.(type) { case *provider.ProxySetProvider: psp := p.(*provider.ProxySetProvider) _, _, err := psp.SideUpdate(bytes) if err == nil { return err } return nil case rp.RuleSetProvider: rsp := p.(*rp.RuleSetProvider) _, _, err := rsp.SideUpdate(bytes) if err == nil { return err } return nil default: return errors.New("not external provider") } } func updateListeners() { if !isRunning { return } if currentConfig == nil { return } listeners := currentConfig.Listeners general := currentConfig.General listener.PatchInboundListeners(listeners, tunnel.Tunnel, true) listener.SetAllowLan(general.AllowLan) inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) inbound.SetAllowedIPs(general.LanAllowedIPs) inbound.SetDisAllowedIPs(general.LanDisAllowedIPs) listener.SetBindAddress(general.BindAddress) listener.ReCreateHTTP(general.Port, tunnel.Tunnel) listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel) listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel) listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel) listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel) listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) if !features.Android { listener.ReCreateTun(general.Tun, tunnel.Tunnel) } } func stopListeners() { listener.StopListener() } func patchSelectGroup(mapping map[string]string) { for name, proxy := range tunnel.ProxiesWithProviders() { outbound, ok := proxy.(*adapter.Proxy) if !ok { continue } selector, ok := outbound.ProxyAdapter.(outboundgroup.SelectAble) if !ok { continue } selected, exist := mapping[name] if !exist { continue } selector.ForceSet(selected) } } func defaultSetupParams() *SetupParams { return &SetupParams{ Config: config.DefaultRawConfig(), TestURL: "https://g.cn/generate_204", SelectedMap: map[string]string{}, } } func readFile(path string) ([]byte, error) { if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } data, err := os.ReadFile(path) if err != nil { return nil, err } return data, err } func updateConfig(params *UpdateParams) { runLock.Lock() defer runLock.Unlock() general := currentConfig.General if params.MixedPort != nil { general.MixedPort = *params.MixedPort } if params.Sniffing != nil { general.Sniffing = *params.Sniffing tunnel.SetSniffing(general.Sniffing) } if params.FindProcessMode != nil { general.FindProcessMode = *params.FindProcessMode tunnel.SetFindProcessMode(general.FindProcessMode) } if params.TCPConcurrent != nil { general.TCPConcurrent = *params.TCPConcurrent dialer.SetTcpConcurrent(general.TCPConcurrent) } if params.Interface != nil { general.Interface = *params.Interface dialer.DefaultInterface.Store(general.Interface) } if params.UnifiedDelay != nil { general.UnifiedDelay = *params.UnifiedDelay adapter.UnifiedDelay.Store(general.UnifiedDelay) } if params.Mode != nil { general.Mode = *params.Mode tunnel.SetMode(general.Mode) } if params.LogLevel != nil { general.LogLevel = *params.LogLevel log.SetLevel(general.LogLevel) } if params.IPv6 != nil { general.IPv6 = *params.IPv6 resolver.DisableIPv6 = !general.IPv6 } if params.ExternalController != nil { currentConfig.Controller.ExternalController = *params.ExternalController route.ReCreateServer(&route.Config{ Addr: currentConfig.Controller.ExternalController, }) } if params.Tun != nil { general.Tun.Enable = params.Tun.Enable general.Tun.AutoRoute = *params.Tun.AutoRoute general.Tun.Device = *params.Tun.Device general.Tun.RouteAddress = *params.Tun.RouteAddress if params.Tun.RouteExcludeAddress != nil { general.Tun.RouteExcludeAddress = *params.Tun.RouteExcludeAddress } if params.Tun.StrictRoute != nil { general.Tun.StrictRoute = *params.Tun.StrictRoute } general.Tun.DNSHijack = *params.Tun.DNSHijack general.Tun.Stack = *params.Tun.Stack general.Tun.DisableICMPForwarding = *params.Tun.DisableICMPForwarding } updateListeners() } func setupConfig(params *SetupParams) error { runLock.Lock() defer runLock.Unlock() if params.Config != nil && params.Config.ProxyGroup != nil { for _, group := range params.Config.ProxyGroup { if elm, ok := group["tolerance"]; ok { switch v := elm.(type) { case json.Number: if i, err := v.Int64(); err == nil { group["tolerance"] = int(i) } case float64: group["tolerance"] = int(v) case float32: group["tolerance"] = int(v) } } } } constant.DefaultTestURL = params.TestURL if params.OverrideTestUrl && params.Config != nil { if params.Config.ProxyGroup != nil { for _, group := range params.Config.ProxyGroup { group["url"] = params.TestURL } } } var err error currentConfig, err = config.ParseRawConfig(params.Config) if err != nil { currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig()) } hub.ApplyConfig(currentConfig) patchSelectGroup(params.SelectedMap) updateListeners() return err } func UnmarshalJson(data []byte, v any) error { decoder := json.NewDecoder(b.NewReader(data)) decoder.UseNumber() err := decoder.Decode(v) return err } ================================================ FILE: core/constant.go ================================================ package main import ( "encoding/json" "net/netip" "time" "github.com/metacubex/mihomo/adapter/provider" P "github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel" ) type InitParams struct { HomeDir string `json:"home-dir"` Version int `json:"version"` } type SetupParams struct { Config *config.RawConfig `json:"config"` SelectedMap map[string]string `json:"selected-map"` TestURL string `json:"test-url"` OverrideTestUrl bool `json:"override-test-url"` } type UpdateParams struct { Tun *tunSchema `json:"tun"` AllowLan *bool `json:"allow-lan"` MixedPort *int `json:"mixed-port"` FindProcessMode *P.FindProcessMode `json:"find-process-mode"` Mode *tunnel.TunnelMode `json:"mode"` LogLevel *log.LogLevel `json:"log-level"` IPv6 *bool `json:"ipv6"` Sniffing *bool `json:"sniffing"` TCPConcurrent *bool `json:"tcp-concurrent"` ExternalController *string `json:"external-controller"` Interface *string `json:"interface-name"` UnifiedDelay *bool `json:"unified-delay"` } type tunSchema struct { Enable bool `yaml:"enable" json:"enable"` Device *string `yaml:"device" json:"device"` Stack *constant.TUNStack `yaml:"stack" json:"stack"` DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"` AutoRoute *bool `yaml:"auto-route" json:"auto-route"` RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` RouteExcludeAddress *[]netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` DisableICMPForwarding *bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"` } type ChangeProxyParams struct { GroupName *string `json:"group-name"` ProxyName *string `json:"proxy-name"` } type TestDelayParams struct { ProxyName string `json:"proxy-name"` TestUrl string `json:"test-url"` Timeout int64 `json:"timeout"` } type ExternalProvider struct { Name string `json:"name"` Type string `json:"type"` VehicleType string `json:"vehicle-type"` Count int `json:"count"` Path string `json:"path"` UpdateAt time.Time `json:"update-at"` SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"` } const ( messageMethod Method = "message" initClashMethod Method = "initClash" getIsInitMethod Method = "getIsInit" forceGcMethod Method = "forceGc" shutdownMethod Method = "shutdown" validateConfigMethod Method = "validateConfig" updateConfigMethod Method = "updateConfig" getProxiesMethod Method = "getProxies" changeProxyMethod Method = "changeProxy" getTrafficMethod Method = "getTraffic" getTotalTrafficMethod Method = "getTotalTraffic" resetTrafficMethod Method = "resetTraffic" asyncTestDelayMethod Method = "asyncTestDelay" getConnectionsMethod Method = "getConnections" closeConnectionsMethod Method = "closeConnections" resetConnectionsMethod Method = "resetConnectionsMethod" closeConnectionMethod Method = "closeConnection" getExternalProvidersMethod Method = "getExternalProviders" getExternalProviderMethod Method = "getExternalProvider" getCountryCodeMethod Method = "getCountryCode" getMemoryMethod Method = "getMemory" updateGeoDataMethod Method = "updateGeoData" updateExternalProviderMethod Method = "updateExternalProvider" sideLoadExternalProviderMethod Method = "sideLoadExternalProvider" startLogMethod Method = "startLog" stopLogMethod Method = "stopLog" startListenerMethod Method = "startListener" stopListenerMethod Method = "stopListener" updateDnsMethod Method = "updateDns" setStateMethod Method = "setState" getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions" getRunTimeMethod Method = "getRunTime" getCurrentProfileNameMethod Method = "getCurrentProfileName" crashMethod Method = "crash" setupConfigMethod Method = "setupConfig" getConfigMethod Method = "getConfig" flushFakeIPMethod Method = "flushFakeIP" flushDnsCacheMethod Method = "flushDnsCache" ) type Method string type MessageType string type Delay struct { Url string `json:"url"` Name string `json:"name"` Value int32 `json:"value"` } type Message struct { Type MessageType `json:"type"` Data interface{} `json:"data"` } const ( LogMessage MessageType = "log" DelayMessage MessageType = "delay" RequestMessage MessageType = "request" LoadedMessage MessageType = "loaded" ) func (message *Message) Json() (string, error) { data, err := json.Marshal(message) return string(data), err } ================================================ FILE: core/dart-bridge/include/dart_api.h ================================================ /* * Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file * for details. All rights reserved. Use of this source code is governed by a * BSD-style license that can be found in the LICENSE file. */ #ifndef RUNTIME_INCLUDE_DART_API_H_ #define RUNTIME_INCLUDE_DART_API_H_ /** \mainpage Dart Embedding API Reference * * This reference describes the Dart Embedding API, which is used to embed the * Dart Virtual Machine within C/C++ applications. * * This reference is generated from the header include/dart_api.h. */ /* __STDC_FORMAT_MACROS has to be defined before including to * enable platform independent printf format specifiers. */ #ifndef __STDC_FORMAT_MACROS #define __STDC_FORMAT_MACROS #endif #include #include #include #if defined(__Fuchsia__) #include #endif #ifdef __cplusplus #define DART_EXTERN_C extern "C" #else #define DART_EXTERN_C extern #endif #if defined(__CYGWIN__) #error Tool chain and platform not supported. #elif defined(_WIN32) #if defined(DART_SHARED_LIB) #define DART_EXPORT DART_EXTERN_C __declspec(dllexport) #else #define DART_EXPORT DART_EXTERN_C #endif #else #if __GNUC__ >= 4 #if defined(DART_SHARED_LIB) #define DART_EXPORT \ DART_EXTERN_C __attribute__((visibility("default"))) __attribute((used)) #else #define DART_EXPORT DART_EXTERN_C #endif #else #error Tool chain not supported. #endif #endif #if __GNUC__ #define DART_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) #elif _MSC_VER #define DART_WARN_UNUSED_RESULT _Check_return_ #else #define DART_WARN_UNUSED_RESULT #endif /* * ======= * Handles * ======= */ /** * An isolate is the unit of concurrency in Dart. Each isolate has * its own memory and thread of control. No state is shared between * isolates. Instead, isolates communicate by message passing. * * Each thread keeps track of its current isolate, which is the * isolate which is ready to execute on the current thread. The * current isolate may be NULL, in which case no isolate is ready to * execute. Most of the Dart apis require there to be a current * isolate in order to function without error. The current isolate is * set by any call to Dart_CreateIsolateGroup or Dart_EnterIsolate. */ typedef struct _Dart_Isolate* Dart_Isolate; typedef struct _Dart_IsolateGroup* Dart_IsolateGroup; /** * An object reference managed by the Dart VM garbage collector. * * Because the garbage collector may move objects, it is unsafe to * refer to objects directly. Instead, we refer to objects through * handles, which are known to the garbage collector and updated * automatically when the object is moved. Handles should be passed * by value (except in cases like out-parameters) and should never be * allocated on the heap. * * Most functions in the Dart Embedding API return a handle. When a * function completes normally, this will be a valid handle to an * object in the Dart VM heap. This handle may represent the result of * the operation or it may be a special valid handle used merely to * indicate successful completion. Note that a valid handle may in * some cases refer to the null object. * * --- Error handles --- * * When a function encounters a problem that prevents it from * completing normally, it returns an error handle (See Dart_IsError). * An error handle has an associated error message that gives more * details about the problem (See Dart_GetError). * * There are four kinds of error handles that can be produced, * depending on what goes wrong: * * - Api error handles are produced when an api function is misused. * This happens when a Dart embedding api function is called with * invalid arguments or in an invalid context. * * - Unhandled exception error handles are produced when, during the * execution of Dart code, an exception is thrown but not caught. * Prototypically this would occur during a call to Dart_Invoke, but * it can occur in any function which triggers the execution of Dart * code (for example, Dart_ToString). * * An unhandled exception error provides access to an exception and * stacktrace via the functions Dart_ErrorGetException and * Dart_ErrorGetStackTrace. * * - Compilation error handles are produced when, during the execution * of Dart code, a compile-time error occurs. As above, this can * occur in any function which triggers the execution of Dart code. * * - Fatal error handles are produced when the system wants to shut * down the current isolate. * * --- Propagating errors --- * * When an error handle is returned from the top level invocation of * Dart code in a program, the embedder must handle the error as they * see fit. Often, the embedder will print the error message produced * by Dart_Error and exit the program. * * When an error is returned while in the body of a native function, * it can be propagated up the call stack by calling * Dart_PropagateError, Dart_SetReturnValue, or Dart_ThrowException. * Errors should be propagated unless there is a specific reason not * to. If an error is not propagated then it is ignored. For * example, if an unhandled exception error is ignored, that * effectively "catches" the unhandled exception. Fatal errors must * always be propagated. * * When an error is propagated, any current scopes created by * Dart_EnterScope will be exited. * * Using Dart_SetReturnValue to propagate an exception is somewhat * more convenient than using Dart_PropagateError, and should be * preferred for reasons discussed below. * * Dart_PropagateError and Dart_ThrowException do not return. Instead * they transfer control non-locally using a setjmp-like mechanism. * This can be inconvenient if you have resources that you need to * clean up before propagating the error. * * When relying on Dart_PropagateError, we often return error handles * rather than propagating them from helper functions. Consider the * following contrived example: * * 1 Dart_Handle isLongStringHelper(Dart_Handle arg) { * 2 intptr_t* length = 0; * 3 result = Dart_StringLength(arg, &length); * 4 if (Dart_IsError(result)) { * 5 return result; * 6 } * 7 return Dart_NewBoolean(length > 100); * 8 } * 9 * 10 void NativeFunction_isLongString(Dart_NativeArguments args) { * 11 Dart_EnterScope(); * 12 AllocateMyResource(); * 13 Dart_Handle arg = Dart_GetNativeArgument(args, 0); * 14 Dart_Handle result = isLongStringHelper(arg); * 15 if (Dart_IsError(result)) { * 16 FreeMyResource(); * 17 Dart_PropagateError(result); * 18 abort(); // will not reach here * 19 } * 20 Dart_SetReturnValue(result); * 21 FreeMyResource(); * 22 Dart_ExitScope(); * 23 } * * In this example, we have a native function which calls a helper * function to do its work. On line 5, the helper function could call * Dart_PropagateError, but that would not give the native function a * chance to call FreeMyResource(), causing a leak. Instead, the * helper function returns the error handle to the caller, giving the * caller a chance to clean up before propagating the error handle. * * When an error is propagated by calling Dart_SetReturnValue, the * native function will be allowed to complete normally and then the * exception will be propagated only once the native call * returns. This can be convenient, as it allows the C code to clean * up normally. * * The example can be written more simply using Dart_SetReturnValue to * propagate the error. * * 1 Dart_Handle isLongStringHelper(Dart_Handle arg) { * 2 intptr_t* length = 0; * 3 result = Dart_StringLength(arg, &length); * 4 if (Dart_IsError(result)) { * 5 return result * 6 } * 7 return Dart_NewBoolean(length > 100); * 8 } * 9 * 10 void NativeFunction_isLongString(Dart_NativeArguments args) { * 11 Dart_EnterScope(); * 12 AllocateMyResource(); * 13 Dart_Handle arg = Dart_GetNativeArgument(args, 0); * 14 Dart_SetReturnValue(isLongStringHelper(arg)); * 15 FreeMyResource(); * 16 Dart_ExitScope(); * 17 } * * In this example, the call to Dart_SetReturnValue on line 14 will * either return the normal return value or the error (potentially * generated on line 3). The call to FreeMyResource on line 15 will * execute in either case. * * --- Local and persistent handles --- * * Local handles are allocated within the current scope (see * Dart_EnterScope) and go away when the current scope exits. Unless * otherwise indicated, callers should assume that all functions in * the Dart embedding api return local handles. * * Persistent handles are allocated within the current isolate. They * can be used to store objects across scopes. Persistent handles have * the lifetime of the current isolate unless they are explicitly * deallocated (see Dart_DeletePersistentHandle). * The type Dart_Handle represents a handle (both local and persistent). * The type Dart_PersistentHandle is a Dart_Handle and it is used to * document that a persistent handle is expected as a parameter to a call * or the return value from a call is a persistent handle. * * FinalizableHandles are persistent handles which are auto deleted when * the object is garbage collected. It is never safe to use these handles * unless you know the object is still reachable. * * WeakPersistentHandles are persistent handles which are automatically set * to point Dart_Null when the object is garbage collected. They are not auto * deleted, so it is safe to use them after the object has become unreachable. */ typedef struct _Dart_Handle* Dart_Handle; typedef Dart_Handle Dart_PersistentHandle; typedef struct _Dart_WeakPersistentHandle* Dart_WeakPersistentHandle; typedef struct _Dart_FinalizableHandle* Dart_FinalizableHandle; // These structs are versioned by DART_API_DL_MAJOR_VERSION, bump the // version when changing this struct. typedef void (*Dart_HandleFinalizer)(void* isolate_callback_data, void* peer); /** * Is this an error handle? * * Requires there to be a current isolate. */ DART_EXPORT bool Dart_IsError(Dart_Handle handle); /** * Is this an api error handle? * * Api error handles are produced when an api function is misused. * This happens when a Dart embedding api function is called with * invalid arguments or in an invalid context. * * Requires there to be a current isolate. */ DART_EXPORT bool Dart_IsApiError(Dart_Handle handle); /** * Is this an unhandled exception error handle? * * Unhandled exception error handles are produced when, during the * execution of Dart code, an exception is thrown but not caught. * This can occur in any function which triggers the execution of Dart * code. * * See Dart_ErrorGetException and Dart_ErrorGetStackTrace. * * Requires there to be a current isolate. */ DART_EXPORT bool Dart_IsUnhandledExceptionError(Dart_Handle handle); /** * Is this a compilation error handle? * * Compilation error handles are produced when, during the execution * of Dart code, a compile-time error occurs. This can occur in any * function which triggers the execution of Dart code. * * Requires there to be a current isolate. */ DART_EXPORT bool Dart_IsCompilationError(Dart_Handle handle); /** * Is this a fatal error handle? * * Fatal error handles are produced when the system wants to shut down * the current isolate. * * Requires there to be a current isolate. */ DART_EXPORT bool Dart_IsFatalError(Dart_Handle handle); /** * Gets the error message from an error handle. * * Requires there to be a current isolate. * * \return A C string containing an error message if the handle is * error. An empty C string ("") if the handle is valid. This C * String is scope allocated and is only valid until the next call * to Dart_ExitScope. */ DART_EXPORT const char* Dart_GetError(Dart_Handle handle); /** * Is this an error handle for an unhandled exception? */ DART_EXPORT bool Dart_ErrorHasException(Dart_Handle handle); /** * Gets the exception Object from an unhandled exception error handle. */ DART_EXPORT Dart_Handle Dart_ErrorGetException(Dart_Handle handle); /** * Gets the stack trace Object from an unhandled exception error handle. */ DART_EXPORT Dart_Handle Dart_ErrorGetStackTrace(Dart_Handle handle); /** * Produces an api error handle with the provided error message. * * Requires there to be a current isolate. * * \param error the error message. */ DART_EXPORT Dart_Handle Dart_NewApiError(const char* error); DART_EXPORT Dart_Handle Dart_NewCompilationError(const char* error); /** * Produces a new unhandled exception error handle. * * Requires there to be a current isolate. * * \param exception An instance of a Dart object to be thrown or * an ApiError or CompilationError handle. * When an ApiError or CompilationError handle is passed in * a string object of the error message is created and it becomes * the Dart object to be thrown. */ DART_EXPORT Dart_Handle Dart_NewUnhandledExceptionError(Dart_Handle exception); /** * Propagates an error. * * If the provided handle is an unhandled exception error, this * function will cause the unhandled exception to be rethrown. This * will proceed in the standard way, walking up Dart frames until an * appropriate 'catch' block is found, executing 'finally' blocks, * etc. * * If the error is not an unhandled exception error, we will unwind * the stack to the next C frame. Intervening Dart frames will be * discarded; specifically, 'finally' blocks will not execute. This * is the standard way that compilation errors (and the like) are * handled by the Dart runtime. * * In either case, when an error is propagated any current scopes * created by Dart_EnterScope will be exited. * * See the additional discussion under "Propagating Errors" at the * beginning of this file. * * \param handle An error handle (See Dart_IsError) * * On success, this function does not return. On failure, the * process is terminated. */ DART_EXPORT void Dart_PropagateError(Dart_Handle handle); /** * Converts an object to a string. * * May generate an unhandled exception error. * * \return The converted string if no error occurs during * the conversion. If an error does occur, an error handle is * returned. */ DART_EXPORT Dart_Handle Dart_ToString(Dart_Handle object); /** * Checks to see if two handles refer to identically equal objects. * * If both handles refer to instances, this is equivalent to using the top-level * function identical() from dart:core. Otherwise, returns whether the two * argument handles refer to the same object. * * \param obj1 An object to be compared. * \param obj2 An object to be compared. * * \return True if the objects are identically equal. False otherwise. */ DART_EXPORT bool Dart_IdentityEquals(Dart_Handle obj1, Dart_Handle obj2); /** * Allocates a handle in the current scope from a persistent handle. */ DART_EXPORT Dart_Handle Dart_HandleFromPersistent(Dart_PersistentHandle object); /** * Allocates a handle in the current scope from a weak persistent handle. * * This will be a handle to Dart_Null if the object has been garbage collected. */ DART_EXPORT Dart_Handle Dart_HandleFromWeakPersistent(Dart_WeakPersistentHandle object); /** * Allocates a persistent handle for an object. * * This handle has the lifetime of the current isolate unless it is * explicitly deallocated by calling Dart_DeletePersistentHandle. * * Requires there to be a current isolate. */ DART_EXPORT Dart_PersistentHandle Dart_NewPersistentHandle(Dart_Handle object); /** * Assign value of local handle to a persistent handle. * * Requires there to be a current isolate. * * \param obj1 A persistent handle whose value needs to be set. * \param obj2 An object whose value needs to be set to the persistent handle. */ DART_EXPORT void Dart_SetPersistentHandle(Dart_PersistentHandle obj1, Dart_Handle obj2); /** * Deallocates a persistent handle. * * Requires there to be a current isolate group. */ DART_EXPORT void Dart_DeletePersistentHandle(Dart_PersistentHandle object); /** * Allocates a weak persistent handle for an object. * * This handle has the lifetime of the current isolate. The handle can also be * explicitly deallocated by calling Dart_DeleteWeakPersistentHandle. * * If the object becomes unreachable the callback is invoked with the peer as * argument. The callback can be executed on any thread, will have a current * isolate group, but will not have a current isolate. The callback can only * call Dart_DeletePersistentHandle or Dart_DeleteWeakPersistentHandle. This * gives the embedder the ability to cleanup data associated with the object. * The handle will point to the Dart_Null object after the finalizer has been * run. It is illegal to call into the VM with any other Dart_* functions from * the callback. If the handle is deleted before the object becomes * unreachable, the callback is never invoked. * * Requires there to be a current isolate. * * \param object An object with identity. * \param peer A pointer to a native object or NULL. This value is * provided to callback when it is invoked. * \param external_allocation_size The number of externally allocated * bytes for peer. Used to inform the garbage collector. * \param callback A function pointer that will be invoked sometime * after the object is garbage collected, unless the handle has been deleted. * A valid callback needs to be specified it cannot be NULL. * * \return The weak persistent handle or NULL. NULL is returned in case of bad * parameters. */ DART_EXPORT Dart_WeakPersistentHandle Dart_NewWeakPersistentHandle(Dart_Handle object, void* peer, intptr_t external_allocation_size, Dart_HandleFinalizer callback); /** * Deletes the given weak persistent [object] handle. * * Requires there to be a current isolate group. */ DART_EXPORT void Dart_DeleteWeakPersistentHandle( Dart_WeakPersistentHandle object); /** * Updates the external memory size for the given weak persistent handle. * * May trigger garbage collection. */ DART_EXPORT void Dart_UpdateExternalSize(Dart_WeakPersistentHandle object, intptr_t external_allocation_size); /** * Allocates a finalizable handle for an object. * * This handle has the lifetime of the current isolate group unless the object * pointed to by the handle is garbage collected, in this case the VM * automatically deletes the handle after invoking the callback associated * with the handle. The handle can also be explicitly deallocated by * calling Dart_DeleteFinalizableHandle. * * If the object becomes unreachable the callback is invoked with the * the peer as argument. The callback can be executed on any thread, will have * an isolate group, but will not have a current isolate. The callback can only * call Dart_DeletePersistentHandle or Dart_DeleteWeakPersistentHandle. * This gives the embedder the ability to cleanup data associated with the * object and clear out any cached references to the handle. All references to * this handle after the callback will be invalid. It is illegal to call into * the VM with any other Dart_* functions from the callback. If the handle is * deleted before the object becomes unreachable, the callback is never * invoked. * * Requires there to be a current isolate. * * \param object An object with identity. * \param peer A pointer to a native object or NULL. This value is * provided to callback when it is invoked. * \param external_allocation_size The number of externally allocated * bytes for peer. Used to inform the garbage collector. * \param callback A function pointer that will be invoked sometime * after the object is garbage collected, unless the handle has been deleted. * A valid callback needs to be specified it cannot be NULL. * * \return The finalizable handle or NULL. NULL is returned in case of bad * parameters. */ DART_EXPORT Dart_FinalizableHandle Dart_NewFinalizableHandle(Dart_Handle object, void* peer, intptr_t external_allocation_size, Dart_HandleFinalizer callback); /** * Deletes the given finalizable [object] handle. * * The caller has to provide the actual Dart object the handle was created from * to prove the object (and therefore the finalizable handle) is still alive. * * Requires there to be a current isolate. */ DART_EXPORT void Dart_DeleteFinalizableHandle(Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object); /** * Updates the external memory size for the given finalizable handle. * * The caller has to provide the actual Dart object the handle was created from * to prove the object (and therefore the finalizable handle) is still alive. * * May trigger garbage collection. */ DART_EXPORT void Dart_UpdateFinalizableExternalSize( Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object, intptr_t external_allocation_size); /* * ========================== * Initialization and Globals * ========================== */ /** * Gets the version string for the Dart VM. * * The version of the Dart VM can be accessed without initializing the VM. * * \return The version string for the embedded Dart VM. */ DART_EXPORT const char* Dart_VersionString(void); /** * Isolate specific flags are set when creating a new isolate using the * Dart_IsolateFlags structure. * * Current version of flags is encoded in a 32-bit integer with 16 bits used * for each part. */ #define DART_FLAGS_CURRENT_VERSION (0x0000000c) typedef struct { int32_t version; bool enable_asserts; bool use_field_guards; bool use_osr; bool obfuscate; bool load_vmservice_library; bool copy_parent_code; bool null_safety; bool is_system_isolate; bool snapshot_is_dontneed_safe; bool branch_coverage; } Dart_IsolateFlags; /** * Initialize Dart_IsolateFlags with correct version and default values. */ DART_EXPORT void Dart_IsolateFlagsInitialize(Dart_IsolateFlags* flags); /** * An isolate creation and initialization callback function. * * This callback, provided by the embedder, is called when the VM * needs to create an isolate. The callback should create an isolate * by calling Dart_CreateIsolateGroup and load any scripts required for * execution. * * This callback may be called on a different thread than the one * running the parent isolate. * * When the function returns NULL, it is the responsibility of this * function to ensure that Dart_ShutdownIsolate has been called if * required (for example, if the isolate was created successfully by * Dart_CreateIsolateGroup() but the root library fails to load * successfully, then the function should call Dart_ShutdownIsolate * before returning). * * When the function returns NULL, the function should set *error to * a malloc-allocated buffer containing a useful error message. The * caller of this function (the VM) will make sure that the buffer is * freed. * * \param script_uri The uri of the main source file or snapshot to load. * Either the URI of the parent isolate set in Dart_CreateIsolateGroup for * Isolate.spawn, or the argument to Isolate.spawnUri canonicalized by the * library tag handler of the parent isolate. * The callback is responsible for loading the program by a call to * Dart_LoadScriptFromKernel. * \param main The name of the main entry point this isolate will * eventually run. This is provided for advisory purposes only to * improve debugging messages. The main function is not invoked by * this function. * \param package_root Ignored. * \param package_config Uri of the package configuration file (either in format * of .packages or .dart_tool/package_config.json) for this isolate * to resolve package imports against. If this parameter is not passed the * package resolution of the parent isolate should be used. * \param flags Default flags for this isolate being spawned. Either inherited * from the spawning isolate or passed as parameters when spawning the * isolate from Dart code. * \param isolate_data The isolate data which was passed to the * parent isolate when it was created by calling Dart_CreateIsolateGroup(). * \param error A structure into which the embedder can place a * C string containing an error message in the case of failures. * * \return The embedder returns NULL if the creation and * initialization was not successful and the isolate if successful. */ typedef Dart_Isolate (*Dart_IsolateGroupCreateCallback)( const char* script_uri, const char* main, const char* package_root, const char* package_config, Dart_IsolateFlags* flags, void* isolate_data, char** error); /** * An isolate initialization callback function. * * This callback, provided by the embedder, is called when the VM has created an * isolate within an existing isolate group (i.e. from the same source as an * existing isolate). * * The callback should setup native resolvers and might want to set a custom * message handler via [Dart_SetMessageNotifyCallback] and mark the isolate as * runnable. * * This callback may be called on a different thread than the one * running the parent isolate. * * When the function returns `false`, it is the responsibility of this * function to ensure that `Dart_ShutdownIsolate` has been called. * * When the function returns `false`, the function should set *error to * a malloc-allocated buffer containing a useful error message. The * caller of this function (the VM) will make sure that the buffer is * freed. * * \param child_isolate_data The callback data to associate with the new * child isolate. * \param error A structure into which the embedder can place a * C string containing an error message in the case the initialization fails. * * \return The embedder returns true if the initialization was successful and * false otherwise (in which case the VM will terminate the isolate). */ typedef bool (*Dart_InitializeIsolateCallback)(void** child_isolate_data, char** error); /** * An isolate shutdown callback function. * * This callback, provided by the embedder, is called before the vm * shuts down an isolate. The isolate being shutdown will be the current * isolate. It is safe to run Dart code. * * This function should be used to dispose of native resources that * are allocated to an isolate in order to avoid leaks. * * \param isolate_group_data The same callback data which was passed to the * isolate group when it was created. * \param isolate_data The same callback data which was passed to the isolate * when it was created. */ typedef void (*Dart_IsolateShutdownCallback)(void* isolate_group_data, void* isolate_data); /** * An isolate cleanup callback function. * * This callback, provided by the embedder, is called after the vm * shuts down an isolate. There will be no current isolate and it is *not* * safe to run Dart code. * * This function should be used to dispose of native resources that * are allocated to an isolate in order to avoid leaks. * * \param isolate_group_data The same callback data which was passed to the * isolate group when it was created. * \param isolate_data The same callback data which was passed to the isolate * when it was created. */ typedef void (*Dart_IsolateCleanupCallback)(void* isolate_group_data, void* isolate_data); /** * An isolate group cleanup callback function. * * This callback, provided by the embedder, is called after the vm * shuts down an isolate group. * * This function should be used to dispose of native resources that * are allocated to an isolate in order to avoid leaks. * * \param isolate_group_data The same callback data which was passed to the * isolate group when it was created. * */ typedef void (*Dart_IsolateGroupCleanupCallback)(void* isolate_group_data); /** * A thread start callback function. * This callback, provided by the embedder, is called after a thread in the * vm thread pool starts. * This function could be used to adjust thread priority or attach native * resources to the thread. */ typedef void (*Dart_ThreadStartCallback)(void); /** * A thread death callback function. * This callback, provided by the embedder, is called before a thread in the * vm thread pool exits. * This function could be used to dispose of native resources that * are associated and attached to the thread, in order to avoid leaks. */ typedef void (*Dart_ThreadExitCallback)(void); /** * Opens a file for reading or writing. * * Callback provided by the embedder for file operations. If the * embedder does not allow file operations this callback can be * NULL. * * \param name The name of the file to open. * \param write A boolean variable which indicates if the file is to * opened for writing. If there is an existing file it needs to truncated. */ typedef void* (*Dart_FileOpenCallback)(const char* name, bool write); /** * Read contents of file. * * Callback provided by the embedder for file operations. If the * embedder does not allow file operations this callback can be * NULL. * * \param data Buffer allocated in the callback into which the contents * of the file are read into. It is the responsibility of the caller to * free this buffer. * \param file_length A variable into which the length of the file is returned. * In the case of an error this value would be -1. * \param stream Handle to the opened file. */ typedef void (*Dart_FileReadCallback)(uint8_t** data, intptr_t* file_length, void* stream); /** * Write data into file. * * Callback provided by the embedder for file operations. If the * embedder does not allow file operations this callback can be * NULL. * * \param data Buffer which needs to be written into the file. * \param length Length of the buffer. * \param stream Handle to the opened file. */ typedef void (*Dart_FileWriteCallback)(const void* data, intptr_t length, void* stream); /** * Closes the opened file. * * Callback provided by the embedder for file operations. If the * embedder does not allow file operations this callback can be * NULL. * * \param stream Handle to the opened file. */ typedef void (*Dart_FileCloseCallback)(void* stream); typedef bool (*Dart_EntropySource)(uint8_t* buffer, intptr_t length); /** * Callback provided by the embedder that is used by the vmservice isolate * to request the asset archive. The asset archive must be an uncompressed tar * archive that is stored in a Uint8List. * * If the embedder has no vmservice isolate assets, the callback can be NULL. * * \return The embedder must return a handle to a Uint8List containing an * uncompressed tar archive or null. */ typedef Dart_Handle (*Dart_GetVMServiceAssetsArchive)(void); /** * The current version of the Dart_InitializeFlags. Should be incremented every * time Dart_InitializeFlags changes in a binary incompatible way. */ #define DART_INITIALIZE_PARAMS_CURRENT_VERSION (0x00000008) /** Forward declaration */ struct Dart_CodeObserver; /** * Callback provided by the embedder that is used by the VM to notify on code * object creation, *before* it is invoked the first time. * This is useful for embedders wanting to e.g. keep track of PCs beyond * the lifetime of the garbage collected code objects. * Note that an address range may be used by more than one code object over the * lifecycle of a process. Clients of this function should record timestamps for * these compilation events and when collecting PCs to disambiguate reused * address ranges. */ typedef void (*Dart_OnNewCodeCallback)(struct Dart_CodeObserver* observer, const char* name, uintptr_t base, uintptr_t size); typedef struct Dart_CodeObserver { void* data; Dart_OnNewCodeCallback on_new_code; } Dart_CodeObserver; /** * Optional callback provided by the embedder that is used by the VM to * implement registration of kernel blobs for the subsequent Isolate.spawnUri * If no callback is provided, the registration of kernel blobs will throw * an error. * * \param kernel_buffer A buffer which contains a kernel program. Callback * should copy the contents of `kernel_buffer` as * it may be freed immediately after registration. * \param kernel_buffer_size The size of `kernel_buffer`. * * \return A C string representing URI which can be later used * to spawn a new isolate. This C String should be scope allocated * or owned by the embedder. * Returns NULL if embedder runs out of memory. */ typedef const char* (*Dart_RegisterKernelBlobCallback)( const uint8_t* kernel_buffer, intptr_t kernel_buffer_size); /** * Optional callback provided by the embedder that is used by the VM to * unregister kernel blobs. * If no callback is provided, the unregistration of kernel blobs will throw * an error. * * \param kernel_blob_uri URI of the kernel blob to unregister. */ typedef void (*Dart_UnregisterKernelBlobCallback)(const char* kernel_blob_uri); /** * Describes how to initialize the VM. Used with Dart_Initialize. */ typedef struct { /** * Identifies the version of the struct used by the client. * should be initialized to DART_INITIALIZE_PARAMS_CURRENT_VERSION. */ int32_t version; /** * A buffer containing snapshot data, or NULL if no snapshot is provided. * * If provided, the buffer must remain valid until Dart_Cleanup returns. */ const uint8_t* vm_snapshot_data; /** * A buffer containing a snapshot of precompiled instructions, or NULL if * no snapshot is provided. * * If provided, the buffer must remain valid until Dart_Cleanup returns. */ const uint8_t* vm_snapshot_instructions; /** * A function to be called during isolate group creation. * See Dart_IsolateGroupCreateCallback. */ Dart_IsolateGroupCreateCallback create_group; /** * A function to be called during isolate * initialization inside an existing isolate group. * See Dart_InitializeIsolateCallback. */ Dart_InitializeIsolateCallback initialize_isolate; /** * A function to be called right before an isolate is shutdown. * See Dart_IsolateShutdownCallback. */ Dart_IsolateShutdownCallback shutdown_isolate; /** * A function to be called after an isolate was shutdown. * See Dart_IsolateCleanupCallback. */ Dart_IsolateCleanupCallback cleanup_isolate; /** * A function to be called after an isolate group is * shutdown. See Dart_IsolateGroupCleanupCallback. */ Dart_IsolateGroupCleanupCallback cleanup_group; Dart_ThreadStartCallback thread_start; Dart_ThreadExitCallback thread_exit; Dart_FileOpenCallback file_open; Dart_FileReadCallback file_read; Dart_FileWriteCallback file_write; Dart_FileCloseCallback file_close; Dart_EntropySource entropy_source; /** * A function to be called by the service isolate when it requires the * vmservice assets archive. See Dart_GetVMServiceAssetsArchive. */ Dart_GetVMServiceAssetsArchive get_service_assets; bool start_kernel_isolate; /** * An external code observer callback function. The observer can be invoked * as early as during the Dart_Initialize() call. */ Dart_CodeObserver* code_observer; /** * Kernel blob registration callback function. See Dart_RegisterKernelBlobCallback. */ Dart_RegisterKernelBlobCallback register_kernel_blob; /** * Kernel blob unregistration callback function. See Dart_UnregisterKernelBlobCallback. */ Dart_UnregisterKernelBlobCallback unregister_kernel_blob; #if defined(__Fuchsia__) /** * The resource needed to use zx_vmo_replace_as_executable. Can be * ZX_HANDLE_INVALID if the process has ambient-replace-as-executable or if * executable memory is not needed (e.g., this is an AOT runtime). */ zx_handle_t vmex_resource; #endif } Dart_InitializeParams; /** * Initializes the VM. * * \param params A struct containing initialization information. The version * field of the struct must be DART_INITIALIZE_PARAMS_CURRENT_VERSION. * * \return NULL if initialization is successful. Returns an error message * otherwise. The caller is responsible for freeing the error message. */ DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_Initialize( Dart_InitializeParams* params); /** * Cleanup state in the VM before process termination. * * \return NULL if cleanup is successful. Returns an error message otherwise. * The caller is responsible for freeing the error message. * * NOTE: This function must not be called on a thread that was created by the VM * itself. */ DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_Cleanup(void); /** * Sets command line flags. Should be called before Dart_Initialize. * * \param argc The length of the arguments array. * \param argv An array of arguments. * * \return NULL if successful. Returns an error message otherwise. * The caller is responsible for freeing the error message. * * NOTE: This call does not store references to the passed in c-strings. */ DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_SetVMFlags(int argc, const char** argv); /** * Returns true if the named VM flag is of boolean type, specified, and set to * true. * * \param flag_name The name of the flag without leading punctuation * (example: "enable_asserts"). */ DART_EXPORT bool Dart_IsVMFlagSet(const char* flag_name); /* * ======== * Isolates * ======== */ /** * Creates a new isolate. The new isolate becomes the current isolate. * * A snapshot can be used to restore the VM quickly to a saved state * and is useful for fast startup. If snapshot data is provided, the * isolate will be started using that snapshot data. Requires a core snapshot or * an app snapshot created by Dart_CreateSnapshot or * Dart_CreatePrecompiledSnapshot* from a VM with the same version. * * Requires there to be no current isolate. * * \param script_uri The main source file or snapshot this isolate will load. * The VM will provide this URI to the Dart_IsolateGroupCreateCallback when a * child isolate is created by Isolate.spawn. The embedder should use a URI * that allows it to load the same program into such a child isolate. * \param name A short name for the isolate to improve debugging messages. * Typically of the format 'foo.dart:main()'. * \param isolate_snapshot_data Buffer containing the snapshot data of the * isolate or NULL if no snapshot is provided. If provided, the buffer must * remain valid until the isolate shuts down. * \param isolate_snapshot_instructions Buffer containing the snapshot * instructions of the isolate or NULL if no snapshot is provided. If * provided, the buffer must remain valid until the isolate shuts down. * \param flags Pointer to VM specific flags or NULL for default flags. * \param isolate_group_data Embedder group data. This data can be obtained * by calling Dart_IsolateGroupData and will be passed to the * Dart_IsolateShutdownCallback, Dart_IsolateCleanupCallback, and * Dart_IsolateGroupCleanupCallback. * \param isolate_data Embedder data. This data will be passed to * the Dart_IsolateGroupCreateCallback when new isolates are spawned from * this parent isolate. * \param error Returns NULL if creation is successful, an error message * otherwise. The caller is responsible for calling free() on the error * message. * * \return The new isolate on success, or NULL if isolate creation failed. */ DART_EXPORT Dart_Isolate Dart_CreateIsolateGroup(const char* script_uri, const char* name, const uint8_t* isolate_snapshot_data, const uint8_t* isolate_snapshot_instructions, Dart_IsolateFlags* flags, void* isolate_group_data, void* isolate_data, char** error); /** * Creates a new isolate inside the isolate group of [group_member]. * * Requires there to be no current isolate. * * \param group_member An isolate from the same group into which the newly created * isolate should be born into. Other threads may not have entered / enter this * member isolate. * \param name A short name for the isolate for debugging purposes. * \param shutdown_callback A callback to be called when the isolate is being * shutdown (may be NULL). * \param cleanup_callback A callback to be called when the isolate is being * cleaned up (may be NULL). * \param child_isolate_data The embedder-specific data associated with this isolate. * \param error Set to NULL if creation is successful, set to an error * message otherwise. The caller is responsible for calling free() on the * error message. * * \return The newly created isolate on success, or NULL if isolate creation * failed. * * If successful, the newly created isolate will become the current isolate. */ DART_EXPORT Dart_Isolate Dart_CreateIsolateInGroup(Dart_Isolate group_member, const char* name, Dart_IsolateShutdownCallback shutdown_callback, Dart_IsolateCleanupCallback cleanup_callback, void* child_isolate_data, char** error); /* TODO(turnidge): Document behavior when there is already a current * isolate. */ /** * Creates a new isolate from a Dart Kernel file. The new isolate * becomes the current isolate. * * Requires there to be no current isolate. * * \param script_uri The main source file or snapshot this isolate will load. * The VM will provide this URI to the Dart_IsolateGroupCreateCallback when a * child isolate is created by Isolate.spawn. The embedder should use a URI that * allows it to load the same program into such a child isolate. * \param name A short name for the isolate to improve debugging messages. * Typically of the format 'foo.dart:main()'. * \param kernel_buffer A buffer which contains a kernel/DIL program. Must * remain valid until isolate shutdown. * \param kernel_buffer_size The size of `kernel_buffer`. * \param flags Pointer to VM specific flags or NULL for default flags. * \param isolate_group_data Embedder group data. This data can be obtained * by calling Dart_IsolateGroupData and will be passed to the * Dart_IsolateShutdownCallback, Dart_IsolateCleanupCallback, and * Dart_IsolateGroupCleanupCallback. * \param isolate_data Embedder data. This data will be passed to * the Dart_IsolateGroupCreateCallback when new isolates are spawned from * this parent isolate. * \param error Returns NULL if creation is successful, an error message * otherwise. The caller is responsible for calling free() on the error * message. * * \return The new isolate on success, or NULL if isolate creation failed. */ DART_EXPORT Dart_Isolate Dart_CreateIsolateGroupFromKernel(const char* script_uri, const char* name, const uint8_t* kernel_buffer, intptr_t kernel_buffer_size, Dart_IsolateFlags* flags, void* isolate_group_data, void* isolate_data, char** error); /** * Shuts down the current isolate. After this call, the current isolate is NULL. * Any current scopes created by Dart_EnterScope will be exited. Invokes the * shutdown callback and any callbacks of remaining weak persistent handles. * * Requires there to be a current isolate. */ DART_EXPORT void Dart_ShutdownIsolate(void); /* TODO(turnidge): Document behavior when there is no current isolate. */ /** * Returns the current isolate. Will return NULL if there is no * current isolate. */ DART_EXPORT Dart_Isolate Dart_CurrentIsolate(void); /** * Returns the callback data associated with the current isolate. This * data was set when the isolate got created or initialized. */ DART_EXPORT void* Dart_CurrentIsolateData(void); /** * Returns the callback data associated with the given isolate. This * data was set when the isolate got created or initialized. */ DART_EXPORT void* Dart_IsolateData(Dart_Isolate isolate); /** * Returns the current isolate group. Will return NULL if there is no * current isolate group. */ DART_EXPORT Dart_IsolateGroup Dart_CurrentIsolateGroup(void); /** * Returns the callback data associated with the current isolate group. This * data was passed to the isolate group when it was created. */ DART_EXPORT void* Dart_CurrentIsolateGroupData(void); /** * Gets an id that uniquely identifies current isolate group. * * It is the responsibility of the caller to free the returned ID. */ typedef int64_t Dart_IsolateGroupId; DART_EXPORT Dart_IsolateGroupId Dart_CurrentIsolateGroupId(void); /** * Returns the callback data associated with the specified isolate group. This * data was passed to the isolate when it was created. * The embedder is responsible for ensuring the consistency of this data * with respect to the lifecycle of an isolate group. */ DART_EXPORT void* Dart_IsolateGroupData(Dart_Isolate isolate); /** * Returns the debugging name for the current isolate. * * This name is unique to each isolate and should only be used to make * debugging messages more comprehensible. */ DART_EXPORT Dart_Handle Dart_DebugName(void); /** * Returns the debugging name for the current isolate. * * This name is unique to each isolate and should only be used to make * debugging messages more comprehensible. * * The returned string is scope allocated and is only valid until the next call * to Dart_ExitScope. */ DART_EXPORT const char* Dart_DebugNameToCString(void); /** * Returns the ID for an isolate which is used to query the service protocol. * * It is the responsibility of the caller to free the returned ID. */ DART_EXPORT const char* Dart_IsolateServiceId(Dart_Isolate isolate); /** * Enters an isolate. After calling this function, * the current isolate will be set to the provided isolate. * * Requires there to be no current isolate. Multiple threads may not be in * the same isolate at once. */ DART_EXPORT void Dart_EnterIsolate(Dart_Isolate isolate); /** * Kills the given isolate. * * This function has the same effect as dart:isolate's * Isolate.kill(priority:immediate). * It can interrupt ordinary Dart code but not native code. If the isolate is * in the middle of a long running native function, the isolate will not be * killed until control returns to Dart. * * Does not require a current isolate. It is safe to kill the current isolate if * there is one. */ DART_EXPORT void Dart_KillIsolate(Dart_Isolate isolate); /** * Notifies the VM that the embedder expects to be idle until |deadline|. The VM * may use this time to perform garbage collection or other tasks to avoid * delays during execution of Dart code in the future. * * |deadline| is measured in microseconds against the system's monotonic time. * This clock can be accessed via Dart_TimelineGetMicros(). * * Requires there to be a current isolate. */ DART_EXPORT void Dart_NotifyIdle(int64_t deadline); typedef void (*Dart_HeapSamplingReportCallback)(void* context, void* data); typedef void* (*Dart_HeapSamplingCreateCallback)( Dart_Isolate isolate, Dart_IsolateGroup isolate_group, const char* cls_name, intptr_t allocation_size); typedef void (*Dart_HeapSamplingDeleteCallback)(void* data); /** * Starts the heap sampling profiler for each thread in the VM. */ DART_EXPORT void Dart_EnableHeapSampling(void); /* * Stops the heap sampling profiler for each thread in the VM. */ DART_EXPORT void Dart_DisableHeapSampling(void); /* Registers callbacks are invoked once per sampled allocation upon object * allocation and garbage collection. * * |create_callback| can be used to associate additional data with the sampled * allocation, such as a stack trace. This data pointer will be passed to * |delete_callback| to allow for proper disposal when the object associated * with the allocation sample is collected. * * The provided callbacks must not call into the VM and should do as little * work as possible to avoid performance penalities during object allocation and * garbage collection. * * NOTE: It is a fatal error to set either callback to null once they have been * initialized. */ DART_EXPORT void Dart_RegisterHeapSamplingCallback( Dart_HeapSamplingCreateCallback create_callback, Dart_HeapSamplingDeleteCallback delete_callback); /* * Reports the surviving allocation samples for all live isolate groups in the * VM. * * When the callback is invoked: * - |context| will be the context object provided when invoking * |Dart_ReportSurvivingAllocations|. This can be safely set to null if not * required. * - |heap_size| will be equal to the size of the allocated object associated * with the sample. * - |cls_name| will be a C String representing * the class name of the allocated object. This string is valid for the * duration of the call to Dart_ReportSurvivingAllocations and can be * freed by the VM at any point after the method returns. * - |data| will be set to the data associated with the sample by * |Dart_HeapSamplingCreateCallback|. * * If |force_gc| is true, a full GC will be performed before reporting the * allocations. */ DART_EXPORT void Dart_ReportSurvivingAllocations( Dart_HeapSamplingReportCallback callback, void* context, bool force_gc); /* * Sets the average heap sampling rate based on a number of |bytes| for each * thread. * * In other words, approximately every |bytes| allocated will create a sample. * Defaults to 512 KiB. */ DART_EXPORT void Dart_SetHeapSamplingPeriod(intptr_t bytes); /** * Notifies the VM that the embedder expects the application's working set has * recently shrunk significantly and is not expected to rise in the near future. * The VM may spend O(heap-size) time performing clean up work. * * Requires there to be a current isolate. */ DART_EXPORT void Dart_NotifyDestroyed(void); /** * Notifies the VM that the system is running low on memory. * * Does not require a current isolate. Only valid after calling Dart_Initialize. */ DART_EXPORT void Dart_NotifyLowMemory(void); typedef enum { /** * Balanced */ Dart_PerformanceMode_Default, /** * Optimize for low latency, at the expense of throughput and memory overhead * by performing work in smaller batches (requiring more overhead) or by * delaying work (requiring more memory). An embedder should not remain in * this mode indefinitely. */ Dart_PerformanceMode_Latency, /** * Optimize for high throughput, at the expense of latency and memory overhead * by performing work in larger batches with more intervening growth. */ Dart_PerformanceMode_Throughput, /** * Optimize for low memory, at the expensive of throughput and latency by more * frequently performing work. */ Dart_PerformanceMode_Memory, } Dart_PerformanceMode; /** * Set the desired performance trade-off. * * Requires a current isolate. * * Returns the previous performance mode. */ DART_EXPORT Dart_PerformanceMode Dart_SetPerformanceMode(Dart_PerformanceMode mode); /** * Starts the CPU sampling profiler. */ DART_EXPORT void Dart_StartProfiling(void); /** * Stops the CPU sampling profiler. * * Note that some profile samples might still be taken after this function * returns due to the asynchronous nature of the implementation on some * platforms. */ DART_EXPORT void Dart_StopProfiling(void); /** * Notifies the VM that the current thread should not be profiled until a * matching call to Dart_ThreadEnableProfiling is made. * * NOTE: By default, if a thread has entered an isolate it will be profiled. * This function should be used when an embedder knows a thread is about * to make a blocking call and wants to avoid unnecessary interrupts by * the profiler. */ DART_EXPORT void Dart_ThreadDisableProfiling(void); /** * Notifies the VM that the current thread should be profiled. * * NOTE: It is only legal to call this function *after* calling * Dart_ThreadDisableProfiling. * * NOTE: By default, if a thread has entered an isolate it will be profiled. */ DART_EXPORT void Dart_ThreadEnableProfiling(void); /** * Register symbol information for the Dart VM's profiler and crash dumps. * * This consumes the output of //topaz/runtime/dart/profiler_symbols, which * should be treated as opaque. */ DART_EXPORT void Dart_AddSymbols(const char* dso_name, void* buffer, intptr_t buffer_size); /** * Exits an isolate. After this call, Dart_CurrentIsolate will * return NULL. * * Requires there to be a current isolate. */ DART_EXPORT void Dart_ExitIsolate(void); /* TODO(turnidge): We don't want users of the api to be able to exit a * "pure" dart isolate. Implement and document. */ /** * Creates a full snapshot of the current isolate heap. * * A full snapshot is a compact representation of the dart vm isolate heap * and dart isolate heap states. These snapshots are used to initialize * the vm isolate on startup and fast initialization of an isolate. * A Snapshot of the heap is created before any dart code has executed. * * Requires there to be a current isolate. Not available in the precompiled * runtime (check Dart_IsPrecompiledRuntime). * * \param vm_snapshot_data_buffer Returns a pointer to a buffer containing the * vm snapshot. This buffer is scope allocated and is only valid * until the next call to Dart_ExitScope. * \param vm_snapshot_data_size Returns the size of vm_snapshot_data_buffer. * \param isolate_snapshot_data_buffer Returns a pointer to a buffer containing * the isolate snapshot. This buffer is scope allocated and is only valid * until the next call to Dart_ExitScope. * \param isolate_snapshot_data_size Returns the size of * isolate_snapshot_data_buffer. * \param is_core Create a snapshot containing core libraries. * Such snapshot should be agnostic to null safety mode. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateSnapshot(uint8_t** vm_snapshot_data_buffer, intptr_t* vm_snapshot_data_size, uint8_t** isolate_snapshot_data_buffer, intptr_t* isolate_snapshot_data_size, bool is_core); /** * Returns whether the buffer contains a kernel file. * * \param buffer Pointer to a buffer that might contain a kernel binary. * \param buffer_size Size of the buffer. * * \return Whether the buffer contains a kernel binary (full or partial). */ DART_EXPORT bool Dart_IsKernel(const uint8_t* buffer, intptr_t buffer_size); /** * Make isolate runnable. * * When isolates are spawned, this function is used to indicate that * the creation and initialization (including script loading) of the * isolate is complete and the isolate can start. * This function expects there to be no current isolate. * * \param isolate The isolate to be made runnable. * * \return NULL if successful. Returns an error message otherwise. The caller * is responsible for freeing the error message. */ DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_IsolateMakeRunnable( Dart_Isolate isolate); /* * ================== * Messages and Ports * ================== */ /** * A port is used to send or receive inter-isolate messages */ typedef int64_t Dart_Port; /** * ILLEGAL_PORT is a port number guaranteed never to be associated with a valid * port. */ #define ILLEGAL_PORT ((Dart_Port)0) /** * A message notification callback. * * This callback allows the embedder to provide a custom wakeup mechanism for * the delivery of inter-isolate messages. This function is called once per * message on an arbitrary thread. It is the responsibility of the embedder to * eventually call Dart_HandleMessage once per callback received with the * destination isolate set as the current isolate to process the message. */ typedef void (*Dart_MessageNotifyCallback)(Dart_Isolate destination_isolate); /** * Allows embedders to provide a custom wakeup mechanism for the delivery of * inter-isolate messages. This setting only applies to the current isolate. * * This mechanism is optional: if not provided, the isolate will be scheduled on * a VM-managed thread pool. An embedder should provide this callback if it * wants to run an isolate on a specific thread or to interleave handling of * inter-isolate messages with other event sources. * * Most embedders will only call this function once, before isolate * execution begins. If this function is called after isolate * execution begins, the embedder is responsible for threading issues. */ DART_EXPORT void Dart_SetMessageNotifyCallback( Dart_MessageNotifyCallback message_notify_callback); /* TODO(turnidge): Consider moving this to isolate creation so that it * is impossible to mess up. */ /** * Query the current message notify callback for the isolate. * * \return The current message notify callback for the isolate. */ DART_EXPORT Dart_MessageNotifyCallback Dart_GetMessageNotifyCallback(void); /** * The VM's default message handler supports pausing an isolate before it * processes the first message and right after the it processes the isolate's * final message. This can be controlled for all isolates by two VM flags: * * `--pause-isolates-on-start` * `--pause-isolates-on-exit` * * Additionally, Dart_SetShouldPauseOnStart and Dart_SetShouldPauseOnExit can be * used to control this behaviour on a per-isolate basis. * * When an embedder is using a Dart_MessageNotifyCallback the embedder * needs to cooperate with the VM so that the service protocol can report * accurate information about isolates and so that tools such as debuggers * work reliably. * * The following functions can be used to implement pausing on start and exit. */ /** * If the VM flag `--pause-isolates-on-start` was passed this will be true. * * \return A boolean value indicating if pause on start was requested. */ DART_EXPORT bool Dart_ShouldPauseOnStart(void); /** * Override the VM flag `--pause-isolates-on-start` for the current isolate. * * \param should_pause Should the isolate be paused on start? * * NOTE: This must be called before Dart_IsolateMakeRunnable. */ DART_EXPORT void Dart_SetShouldPauseOnStart(bool should_pause); /** * Is the current isolate paused on start? * * \return A boolean value indicating if the isolate is paused on start. */ DART_EXPORT bool Dart_IsPausedOnStart(void); /** * Called when the embedder has paused the current isolate on start and when * the embedder has resumed the isolate. * * \param paused Is the isolate paused on start? */ DART_EXPORT void Dart_SetPausedOnStart(bool paused); /** * If the VM flag `--pause-isolates-on-exit` was passed this will be true. * * \return A boolean value indicating if pause on exit was requested. */ DART_EXPORT bool Dart_ShouldPauseOnExit(void); /** * Override the VM flag `--pause-isolates-on-exit` for the current isolate. * * \param should_pause Should the isolate be paused on exit? * */ DART_EXPORT void Dart_SetShouldPauseOnExit(bool should_pause); /** * Is the current isolate paused on exit? * * \return A boolean value indicating if the isolate is paused on exit. */ DART_EXPORT bool Dart_IsPausedOnExit(void); /** * Called when the embedder has paused the current isolate on exit and when * the embedder has resumed the isolate. * * \param paused Is the isolate paused on exit? */ DART_EXPORT void Dart_SetPausedOnExit(bool paused); /** * Called when the embedder has caught a top level unhandled exception error * in the current isolate. * * NOTE: It is illegal to call this twice on the same isolate without first * clearing the sticky error to null. * * \param error The unhandled exception error. */ DART_EXPORT void Dart_SetStickyError(Dart_Handle error); /** * Does the current isolate have a sticky error? */ DART_EXPORT bool Dart_HasStickyError(void); /** * Gets the sticky error for the current isolate. * * \return A handle to the sticky error object or null. */ DART_EXPORT Dart_Handle Dart_GetStickyError(void); /** * Handles the next pending message for the current isolate. * * May generate an unhandled exception error. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_HandleMessage(void); /** * Drains the microtask queue, then blocks the calling thread until the current * isolate receives a message, then handles all messages. * * \param timeout_millis When non-zero, the call returns after the indicated number of milliseconds even if no message was received. * \return A valid handle if no error occurs, otherwise an error handle. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_WaitForEvent(int64_t timeout_millis); /** * Handles any pending messages for the vm service for the current * isolate. * * This function may be used by an embedder at a breakpoint to avoid * pausing the vm service. * * This function can indirectly cause the message notify callback to * be called. * * \return true if the vm service requests the program resume * execution, false otherwise */ DART_EXPORT bool Dart_HandleServiceMessages(void); /** * Does the current isolate have pending service messages? * * \return true if the isolate has pending service messages, false otherwise. */ DART_EXPORT bool Dart_HasServiceMessages(void); /** * Processes any incoming messages for the current isolate. * * This function may only be used when the embedder has not provided * an alternate message delivery mechanism with * Dart_SetMessageCallbacks. It is provided for convenience. * * This function waits for incoming messages for the current * isolate. As new messages arrive, they are handled using * Dart_HandleMessage. The routine exits when all ports to the * current isolate are closed. * * \return A valid handle if the run loop exited successfully. If an * exception or other error occurs while processing messages, an * error handle is returned. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_RunLoop(void); /** * Lets the VM run message processing for the isolate. * * This function expects there to a current isolate and the current isolate * must not have an active api scope. The VM will take care of making the * isolate runnable (if not already), handles its message loop and will take * care of shutting the isolate down once it's done. * * \param errors_are_fatal Whether uncaught errors should be fatal. * \param on_error_port A port to notify on uncaught errors (or ILLEGAL_PORT). * \param on_exit_port A port to notify on exit (or ILLEGAL_PORT). * \param error A non-NULL pointer which will hold an error message if the call * fails. The error has to be free()ed by the caller. * * \return If successful the VM takes ownership of the isolate and takes care * of its message loop. If not successful the caller retains ownership of the * isolate. */ DART_EXPORT DART_WARN_UNUSED_RESULT bool Dart_RunLoopAsync( bool errors_are_fatal, Dart_Port on_error_port, Dart_Port on_exit_port, char** error); /* TODO(turnidge): Should this be removed from the public api? */ /** * Gets the main port id for the current isolate. */ DART_EXPORT Dart_Port Dart_GetMainPortId(void); /** * Does the current isolate have live ReceivePorts? * * A ReceivePort is live when it has not been closed. */ DART_EXPORT bool Dart_HasLivePorts(void); /** * Posts a message for some isolate. The message is a serialized * object. * * Requires there to be a current isolate. * * For posting messages outside of an isolate see \ref Dart_PostCObject. * * \param port_id The destination port. * \param object An object from the current isolate. * * \return True if the message was posted. */ DART_EXPORT bool Dart_Post(Dart_Port port_id, Dart_Handle object); /** * Returns a new SendPort with the provided port id. * * \param port_id The destination port. * * \return A new SendPort if no errors occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewSendPort(Dart_Port port_id); /** * Gets the SendPort id for the provided SendPort. * \param port A SendPort object whose id is desired. * \param port_id Returns the id of the SendPort. * \return Success if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_SendPortGetId(Dart_Handle port, Dart_Port* port_id); /* * ====== * Scopes * ====== */ /** * Enters a new scope. * * All new local handles will be created in this scope. Additionally, * some functions may return "scope allocated" memory which is only * valid within this scope. * * Requires there to be a current isolate. */ DART_EXPORT void Dart_EnterScope(void); /** * Exits a scope. * * The previous scope (if any) becomes the current scope. * * Requires there to be a current isolate. */ DART_EXPORT void Dart_ExitScope(void); /** * The Dart VM uses "zone allocation" for temporary structures. Zones * support very fast allocation of small chunks of memory. The chunks * cannot be deallocated individually, but instead zones support * deallocating all chunks in one fast operation. * * This function makes it possible for the embedder to allocate * temporary data in the VMs zone allocator. * * Zone allocation is possible: * 1. when inside a scope where local handles can be allocated * 2. when processing a message from a native port in a native port * handler * * All the memory allocated this way will be reclaimed either on the * next call to Dart_ExitScope or when the native port handler exits. * * \param size Size of the memory to allocate. * * \return A pointer to the allocated memory. NULL if allocation * failed. Failure might due to is no current VM zone. */ DART_EXPORT uint8_t* Dart_ScopeAllocate(intptr_t size); /* * ======= * Objects * ======= */ /** * Returns the null object. * * \return A handle to the null object. */ DART_EXPORT Dart_Handle Dart_Null(void); /** * Is this object null? */ DART_EXPORT bool Dart_IsNull(Dart_Handle object); /** * Returns the empty string object. * * \return A handle to the empty string object. */ DART_EXPORT Dart_Handle Dart_EmptyString(void); /** * Returns types that are not classes, and which therefore cannot be looked up * as library members by Dart_GetType. * * \return A handle to the dynamic, void or Never type. */ DART_EXPORT Dart_Handle Dart_TypeDynamic(void); DART_EXPORT Dart_Handle Dart_TypeVoid(void); DART_EXPORT Dart_Handle Dart_TypeNever(void); /** * Checks if the two objects are equal. * * The result of the comparison is returned through the 'equal' * parameter. The return value itself is used to indicate success or * failure, not equality. * * May generate an unhandled exception error. * * \param obj1 An object to be compared. * \param obj2 An object to be compared. * \param equal Returns the result of the equality comparison. * * \return A valid handle if no error occurs during the comparison. */ DART_EXPORT Dart_Handle Dart_ObjectEquals(Dart_Handle obj1, Dart_Handle obj2, bool* equal); /** * Is this object an instance of some type? * * The result of the test is returned through the 'instanceof' parameter. * The return value itself is used to indicate success or failure. * * \param object An object. * \param type A type. * \param instanceof Return true if 'object' is an instance of type 'type'. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_ObjectIsType(Dart_Handle object, Dart_Handle type, bool* instanceof); /** * Query object type. * * \param object Some Object. * * \return true if Object is of the specified type. */ DART_EXPORT bool Dart_IsInstance(Dart_Handle object); DART_EXPORT bool Dart_IsNumber(Dart_Handle object); DART_EXPORT bool Dart_IsInteger(Dart_Handle object); DART_EXPORT bool Dart_IsDouble(Dart_Handle object); DART_EXPORT bool Dart_IsBoolean(Dart_Handle object); DART_EXPORT bool Dart_IsString(Dart_Handle object); DART_EXPORT bool Dart_IsStringLatin1(Dart_Handle object); /* (ISO-8859-1) */ DART_EXPORT bool Dart_IsExternalString(Dart_Handle object); DART_EXPORT bool Dart_IsList(Dart_Handle object); DART_EXPORT bool Dart_IsMap(Dart_Handle object); DART_EXPORT bool Dart_IsLibrary(Dart_Handle object); DART_EXPORT bool Dart_IsType(Dart_Handle handle); DART_EXPORT bool Dart_IsFunction(Dart_Handle handle); DART_EXPORT bool Dart_IsVariable(Dart_Handle handle); DART_EXPORT bool Dart_IsTypeVariable(Dart_Handle handle); DART_EXPORT bool Dart_IsClosure(Dart_Handle object); DART_EXPORT bool Dart_IsTypedData(Dart_Handle object); DART_EXPORT bool Dart_IsByteBuffer(Dart_Handle object); DART_EXPORT bool Dart_IsFuture(Dart_Handle object); /* * ========= * Instances * ========= */ /* * For the purposes of the embedding api, not all objects returned are * Dart language objects. Within the api, we use the term 'Instance' * to indicate handles which refer to true Dart language objects. * * TODO(turnidge): Reorganize the "Object" section above, pulling down * any functions that more properly belong here. */ /** * Gets the type of a Dart language object. * * \param instance Some Dart object. * * \return If no error occurs, the type is returned. Otherwise an * error handle is returned. */ DART_EXPORT Dart_Handle Dart_InstanceGetType(Dart_Handle instance); /** * Returns the name for the provided class type. * * \return A valid string handle if no error occurs during the * operation. */ DART_EXPORT Dart_Handle Dart_ClassName(Dart_Handle cls_type); /** * Returns the name for the provided function or method. * * \return A valid string handle if no error occurs during the * operation. */ DART_EXPORT Dart_Handle Dart_FunctionName(Dart_Handle function); /** * Returns a handle to the owner of a function. * * The owner of an instance method or a static method is its defining * class. The owner of a top-level function is its defining * library. The owner of the function of a non-implicit closure is the * function of the method or closure that defines the non-implicit * closure. * * \return A valid handle to the owner of the function, or an error * handle if the argument is not a valid handle to a function. */ DART_EXPORT Dart_Handle Dart_FunctionOwner(Dart_Handle function); /** * Determines whether a function handle refers to a static function * of method. * * For the purposes of the embedding API, a top-level function is * implicitly declared static. * * \param function A handle to a function or method declaration. * \param is_static Returns whether the function or method is declared static. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_FunctionIsStatic(Dart_Handle function, bool* is_static); /** * Is this object a closure resulting from a tear-off (closurized method)? * * Returns true for closures produced when an ordinary method is accessed * through a getter call. Returns false otherwise, in particular for closures * produced from local function declarations. * * \param object Some Object. * * \return true if Object is a tear-off. */ DART_EXPORT bool Dart_IsTearOff(Dart_Handle object); /** * Retrieves the function of a closure. * * \return A handle to the function of the closure, or an error handle if the * argument is not a closure. */ DART_EXPORT Dart_Handle Dart_ClosureFunction(Dart_Handle closure); /** * Returns a handle to the library which contains class. * * \return A valid handle to the library with owns class, null if the class * has no library or an error handle if the argument is not a valid handle * to a class type. */ DART_EXPORT Dart_Handle Dart_ClassLibrary(Dart_Handle cls_type); /* * ============================= * Numbers, Integers and Doubles * ============================= */ /** * Does this Integer fit into a 64-bit signed integer? * * \param integer An integer. * \param fits Returns true if the integer fits into a 64-bit signed integer. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_IntegerFitsIntoInt64(Dart_Handle integer, bool* fits); /** * Does this Integer fit into a 64-bit unsigned integer? * * \param integer An integer. * \param fits Returns true if the integer fits into a 64-bit unsigned integer. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_IntegerFitsIntoUint64(Dart_Handle integer, bool* fits); /** * Returns an Integer with the provided value. * * \param value The value of the integer. * * \return The Integer object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewInteger(int64_t value); /** * Returns an Integer with the provided value. * * \param value The unsigned value of the integer. * * \return The Integer object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewIntegerFromUint64(uint64_t value); /** * Returns an Integer with the provided value. * * \param value The value of the integer represented as a C string * containing a hexadecimal number. * * \return The Integer object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewIntegerFromHexCString(const char* value); /** * Gets the value of an Integer. * * The integer must fit into a 64-bit signed integer, otherwise an error occurs. * * \param integer An Integer. * \param value Returns the value of the Integer. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_IntegerToInt64(Dart_Handle integer, int64_t* value); /** * Gets the value of an Integer. * * The integer must fit into a 64-bit unsigned integer, otherwise an * error occurs. * * \param integer An Integer. * \param value Returns the value of the Integer. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_IntegerToUint64(Dart_Handle integer, uint64_t* value); /** * Gets the value of an integer as a hexadecimal C string. * * \param integer An Integer. * \param value Returns the value of the Integer as a hexadecimal C * string. This C string is scope allocated and is only valid until * the next call to Dart_ExitScope. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_IntegerToHexCString(Dart_Handle integer, const char** value); /** * Returns a Double with the provided value. * * \param value A double. * * \return The Double object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewDouble(double value); /** * Gets the value of a Double * * \param double_obj A Double * \param value Returns the value of the Double. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_DoubleValue(Dart_Handle double_obj, double* value); /** * Returns a closure of static function 'function_name' in the class 'class_name' * in the exported namespace of specified 'library'. * * \param library Library object * \param cls_type Type object representing a Class * \param function_name Name of the static function in the class * * \return A valid Dart instance if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_GetStaticMethodClosure(Dart_Handle library, Dart_Handle cls_type, Dart_Handle function_name); /* * ======== * Booleans * ======== */ /** * Returns the True object. * * Requires there to be a current isolate. * * \return A handle to the True object. */ DART_EXPORT Dart_Handle Dart_True(void); /** * Returns the False object. * * Requires there to be a current isolate. * * \return A handle to the False object. */ DART_EXPORT Dart_Handle Dart_False(void); /** * Returns a Boolean with the provided value. * * \param value true or false. * * \return The Boolean object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewBoolean(bool value); /** * Gets the value of a Boolean * * \param boolean_obj A Boolean * \param value Returns the value of the Boolean. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_BooleanValue(Dart_Handle boolean_obj, bool* value); /* * ======= * Strings * ======= */ /** * Gets the length of a String. * * \param str A String. * \param length Returns the length of the String. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_StringLength(Dart_Handle str, intptr_t* length); /** * Returns a String built from the provided C string * (There is an implicit assumption that the C string passed in contains * UTF-8 encoded characters and '\0' is considered as a termination * character). * * \param str A C String * * \return The String object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewStringFromCString(const char* str); /* TODO(turnidge): Document what happens when we run out of memory * during this call. */ /** * Returns a String built from an array of UTF-8 encoded characters. * * \param utf8_array An array of UTF-8 encoded characters. * \param length The length of the codepoints array. * * \return The String object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewStringFromUTF8(const uint8_t* utf8_array, intptr_t length); /** * Returns a String built from an array of UTF-16 encoded characters. * * \param utf16_array An array of UTF-16 encoded characters. * \param length The length of the codepoints array. * * \return The String object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewStringFromUTF16(const uint16_t* utf16_array, intptr_t length); /** * Returns a String built from an array of UTF-32 encoded characters. * * \param utf32_array An array of UTF-32 encoded characters. * \param length The length of the codepoints array. * * \return The String object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewStringFromUTF32(const int32_t* utf32_array, intptr_t length); /** * Returns a String which references an external array of * Latin-1 (ISO-8859-1) encoded characters. * * \param latin1_array Array of Latin-1 encoded characters. This must not move. * \param length The length of the characters array. * \param peer An external pointer to associate with this string. * \param external_allocation_size The number of externally allocated * bytes for peer. Used to inform the garbage collector. * \param callback A callback to be called when this string is finalized. * * \return The String object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewExternalLatin1String(const uint8_t* latin1_array, intptr_t length, void* peer, intptr_t external_allocation_size, Dart_HandleFinalizer callback); /** * Returns a String which references an external array of UTF-16 encoded * characters. * * \param utf16_array An array of UTF-16 encoded characters. This must not move. * \param length The length of the characters array. * \param peer An external pointer to associate with this string. * \param external_allocation_size The number of externally allocated * bytes for peer. Used to inform the garbage collector. * \param callback A callback to be called when this string is finalized. * * \return The String object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewExternalUTF16String(const uint16_t* utf16_array, intptr_t length, void* peer, intptr_t external_allocation_size, Dart_HandleFinalizer callback); /** * Gets the C string representation of a String. * (It is a sequence of UTF-8 encoded values with a '\0' termination.) * * \param str A string. * \param cstr Returns the String represented as a C string. * This C string is scope allocated and is only valid until * the next call to Dart_ExitScope. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_StringToCString(Dart_Handle str, const char** cstr); /** * Gets a UTF-8 encoded representation of a String. * * Any unpaired surrogate code points in the string will be converted as * replacement characters (U+FFFD, 0xEF 0xBF 0xBD in UTF-8). If you need * to preserve unpaired surrogates, use the Dart_StringToUTF16 function. * * \param str A string. * \param utf8_array Returns the String represented as UTF-8 code * units. This UTF-8 array is scope allocated and is only valid * until the next call to Dart_ExitScope. * \param length Used to return the length of the array which was * actually used. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_StringToUTF8(Dart_Handle str, uint8_t** utf8_array, intptr_t* length); /** * Gets the data corresponding to the string object. This function returns * the data only for Latin-1 (ISO-8859-1) string objects. For all other * string objects it returns an error. * * \param str A string. * \param latin1_array An array allocated by the caller, used to return * the string data. * \param length Used to pass in the length of the provided array. * Used to return the length of the array which was actually used. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_StringToLatin1(Dart_Handle str, uint8_t* latin1_array, intptr_t* length); /** * Gets the UTF-16 encoded representation of a string. * * \param str A string. * \param utf16_array An array allocated by the caller, used to return * the array of UTF-16 encoded characters. * \param length Used to pass in the length of the provided array. * Used to return the length of the array which was actually used. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_StringToUTF16(Dart_Handle str, uint16_t* utf16_array, intptr_t* length); /** * Gets the storage size in bytes of a String. * * \param str A String. * \param size Returns the storage size in bytes of the String. * This is the size in bytes needed to store the String. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_StringStorageSize(Dart_Handle str, intptr_t* size); /** * Retrieves some properties associated with a String. * Properties retrieved are: * - character size of the string (one or two byte) * - length of the string * - peer pointer of string if it is an external string. * \param str A String. * \param char_size Returns the character size of the String. * \param str_len Returns the length of the String. * \param peer Returns the peer pointer associated with the String or 0 if * there is no peer pointer for it. * \return Success if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_StringGetProperties(Dart_Handle str, intptr_t* char_size, intptr_t* str_len, void** peer); /* * ===== * Lists * ===== */ /** * Returns a List of the desired length. * * \param length The length of the list. * * \return The List object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewList(intptr_t length); typedef enum { Dart_CoreType_Dynamic, Dart_CoreType_Int, Dart_CoreType_String, } Dart_CoreType_Id; // TODO(bkonyi): convert this to use nullable types once NNBD is enabled. /** * Returns a List of the desired length with the desired legacy element type. * * \param element_type_id The type of elements of the list. * \param length The length of the list. * * \return The List object if no error occurs. Otherwise returns an error * handle. */ DART_EXPORT Dart_Handle Dart_NewListOf(Dart_CoreType_Id element_type_id, intptr_t length); /** * Returns a List of the desired length with the desired element type. * * \param element_type Handle to a nullable type object. E.g., from * Dart_GetType or Dart_GetNullableType. * * \param length The length of the list. * * \return The List object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewListOfType(Dart_Handle element_type, intptr_t length); /** * Returns a List of the desired length with the desired element type, filled * with the provided object. * * \param element_type Handle to a type object. E.g., from Dart_GetType. * * \param fill_object Handle to an object of type 'element_type' that will be * used to populate the list. This parameter can only be Dart_Null() if the * length of the list is 0 or 'element_type' is a nullable type. * * \param length The length of the list. * * \return The List object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewListOfTypeFilled(Dart_Handle element_type, Dart_Handle fill_object, intptr_t length); /** * Gets the length of a List. * * May generate an unhandled exception error. * * \param list A List. * \param length Returns the length of the List. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_ListLength(Dart_Handle list, intptr_t* length); /** * Gets the Object at some index of a List. * * If the index is out of bounds, an error occurs. * * May generate an unhandled exception error. * * \param list A List. * \param index A valid index into the List. * * \return The Object in the List at the specified index if no error * occurs. Otherwise returns an error handle. */ DART_EXPORT Dart_Handle Dart_ListGetAt(Dart_Handle list, intptr_t index); /** * Gets a range of Objects from a List. * * If any of the requested index values are out of bounds, an error occurs. * * May generate an unhandled exception error. * * \param list A List. * \param offset The offset of the first item to get. * \param length The number of items to get. * \param result A pointer to fill with the objects. * * \return Success if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_ListGetRange(Dart_Handle list, intptr_t offset, intptr_t length, Dart_Handle* result); /** * Sets the Object at some index of a List. * * If the index is out of bounds, an error occurs. * * May generate an unhandled exception error. * * \param list A List. * \param index A valid index into the List. * \param value The Object to put in the List. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT Dart_Handle Dart_ListSetAt(Dart_Handle list, intptr_t index, Dart_Handle value); /** * May generate an unhandled exception error. */ DART_EXPORT Dart_Handle Dart_ListGetAsBytes(Dart_Handle list, intptr_t offset, uint8_t* native_array, intptr_t length); /** * May generate an unhandled exception error. */ DART_EXPORT Dart_Handle Dart_ListSetAsBytes(Dart_Handle list, intptr_t offset, const uint8_t* native_array, intptr_t length); /* * ==== * Maps * ==== */ /** * Gets the Object at some key of a Map. * * May generate an unhandled exception error. * * \param map A Map. * \param key An Object. * * \return The value in the map at the specified key, null if the map does not * contain the key, or an error handle. */ DART_EXPORT Dart_Handle Dart_MapGetAt(Dart_Handle map, Dart_Handle key); /** * Returns whether the Map contains a given key. * * May generate an unhandled exception error. * * \param map A Map. * * \return A handle on a boolean indicating whether map contains the key. * Otherwise returns an error handle. */ DART_EXPORT Dart_Handle Dart_MapContainsKey(Dart_Handle map, Dart_Handle key); /** * Gets the list of keys of a Map. * * May generate an unhandled exception error. * * \param map A Map. * * \return The list of key Objects if no error occurs. Otherwise returns an * error handle. */ DART_EXPORT Dart_Handle Dart_MapKeys(Dart_Handle map); /* * ========== * Typed Data * ========== */ typedef enum { Dart_TypedData_kByteData = 0, Dart_TypedData_kInt8, Dart_TypedData_kUint8, Dart_TypedData_kUint8Clamped, Dart_TypedData_kInt16, Dart_TypedData_kUint16, Dart_TypedData_kInt32, Dart_TypedData_kUint32, Dart_TypedData_kInt64, Dart_TypedData_kUint64, Dart_TypedData_kFloat32, Dart_TypedData_kFloat64, Dart_TypedData_kInt32x4, Dart_TypedData_kFloat32x4, Dart_TypedData_kFloat64x2, Dart_TypedData_kInvalid } Dart_TypedData_Type; /** * Return type if this object is a TypedData object. * * \return kInvalid if the object is not a TypedData object or the appropriate * Dart_TypedData_Type. */ DART_EXPORT Dart_TypedData_Type Dart_GetTypeOfTypedData(Dart_Handle object); /** * Return type if this object is an external TypedData object. * * \return kInvalid if the object is not an external TypedData object or * the appropriate Dart_TypedData_Type. */ DART_EXPORT Dart_TypedData_Type Dart_GetTypeOfExternalTypedData(Dart_Handle object); /** * Returns a TypedData object of the desired length and type. * * \param type The type of the TypedData object. * \param length The length of the TypedData object (length in type units). * * \return The TypedData object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewTypedData(Dart_TypedData_Type type, intptr_t length); /** * Returns a TypedData object which references an external data array. * * \param type The type of the data array. * \param data A data array. This array must not move. * \param length The length of the data array (length in type units). * * \return The TypedData object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewExternalTypedData(Dart_TypedData_Type type, void* data, intptr_t length); /** * Returns a TypedData object which references an external data array. * * \param type The type of the data array. * \param data A data array. This array must not move. * \param length The length of the data array (length in type units). * \param peer A pointer to a native object or NULL. This value is * provided to callback when it is invoked. * \param external_allocation_size The number of externally allocated * bytes for peer. Used to inform the garbage collector. * \param callback A function pointer that will be invoked sometime * after the object is garbage collected, unless the handle has been deleted. * A valid callback needs to be specified it cannot be NULL. * * \return The TypedData object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewExternalTypedDataWithFinalizer(Dart_TypedData_Type type, void* data, intptr_t length, void* peer, intptr_t external_allocation_size, Dart_HandleFinalizer callback); DART_EXPORT Dart_Handle Dart_NewUnmodifiableExternalTypedDataWithFinalizer( Dart_TypedData_Type type, const void* data, intptr_t length, void* peer, intptr_t external_allocation_size, Dart_HandleFinalizer callback); /** * Returns a ByteBuffer object for the typed data. * * \param typed_data The TypedData object. * * \return The ByteBuffer object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_NewByteBuffer(Dart_Handle typed_data); /** * Acquires access to the internal data address of a TypedData object. * * \param object The typed data object whose internal data address is to * be accessed. * \param type The type of the object is returned here. * \param data The internal data address is returned here. * \param len Size of the typed array is returned here. * * Notes: * When the internal address of the object is acquired any calls to a * Dart API function that could potentially allocate an object or run * any Dart code will return an error. * * Any Dart API functions for accessing the data should not be called * before the corresponding release. In particular, the object should * not be acquired again before its release. This leads to undefined * behavior. * * \return Success if the internal data address is acquired successfully. * Otherwise, returns an error handle. */ DART_EXPORT Dart_Handle Dart_TypedDataAcquireData(Dart_Handle object, Dart_TypedData_Type* type, void** data, intptr_t* len); /** * Releases access to the internal data address that was acquired earlier using * Dart_TypedDataAcquireData. * * \param object The typed data object whose internal data address is to be * released. * * \return Success if the internal data address is released successfully. * Otherwise, returns an error handle. */ DART_EXPORT Dart_Handle Dart_TypedDataReleaseData(Dart_Handle object); /** * Returns the TypedData object associated with the ByteBuffer object. * * \param byte_buffer The ByteBuffer object. * * \return The TypedData object if no error occurs. Otherwise returns * an error handle. */ DART_EXPORT Dart_Handle Dart_GetDataFromByteBuffer(Dart_Handle byte_buffer); /* * ============================================================ * Invoking Constructors, Methods, Closures and Field accessors * ============================================================ */ /** * Invokes a constructor, creating a new object. * * This function allows hidden constructors (constructors with leading * underscores) to be called. * * \param type Type of object to be constructed. * \param constructor_name The name of the constructor to invoke. Use * Dart_Null() or Dart_EmptyString() to invoke the unnamed constructor. * This name should not include the name of the class. * \param number_of_arguments Size of the arguments array. * \param arguments An array of arguments to the constructor. * * \return If the constructor is called and completes successfully, * then the new object. If an error occurs during execution, then an * error handle is returned. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_New(Dart_Handle type, Dart_Handle constructor_name, int number_of_arguments, Dart_Handle* arguments); /** * Allocate a new object without invoking a constructor. * * \param type The type of an object to be allocated. * * \return The new object. If an error occurs during execution, then an * error handle is returned. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_Allocate(Dart_Handle type); /** * Allocate a new object without invoking a constructor, and sets specified * native fields. * * \param type The type of an object to be allocated. * \param num_native_fields The number of native fields to set. * \param native_fields An array containing the value of native fields. * * \return The new object. If an error occurs during execution, then an * error handle is returned. */ DART_EXPORT Dart_Handle Dart_AllocateWithNativeFields(Dart_Handle type, intptr_t num_native_fields, const intptr_t* native_fields); /** * Invokes a method or function. * * The 'target' parameter may be an object, type, or library. If * 'target' is an object, then this function will invoke an instance * method. If 'target' is a type, then this function will invoke a * static method. If 'target' is a library, then this function will * invoke a top-level function from that library. * NOTE: This API call cannot be used to invoke methods of a type object. * * This function ignores visibility (leading underscores in names). * * May generate an unhandled exception error. * * \param target An object, type, or library. * \param name The name of the function or method to invoke. * \param number_of_arguments Size of the arguments array. * \param arguments An array of arguments to the function. * * \return If the function or method is called and completes * successfully, then the return value is returned. If an error * occurs during execution, then an error handle is returned. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_Invoke(Dart_Handle target, Dart_Handle name, int number_of_arguments, Dart_Handle* arguments); /* TODO(turnidge): Document how to invoke operators. */ /** * Invokes a Closure with the given arguments. * * May generate an unhandled exception error. * * \return If no error occurs during execution, then the result of * invoking the closure is returned. If an error occurs during * execution, then an error handle is returned. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_InvokeClosure(Dart_Handle closure, int number_of_arguments, Dart_Handle* arguments); /** * Invokes a Generative Constructor on an object that was previously * allocated using Dart_Allocate/Dart_AllocateWithNativeFields. * * The 'object' parameter must be an object. * * This function ignores visibility (leading underscores in names). * * May generate an unhandled exception error. * * \param object An object. * \param name The name of the constructor to invoke. * Use Dart_Null() or Dart_EmptyString() to invoke the unnamed constructor. * \param number_of_arguments Size of the arguments array. * \param arguments An array of arguments to the function. * * \return If the constructor is called and completes * successfully, then the object is returned. If an error * occurs during execution, then an error handle is returned. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_InvokeConstructor(Dart_Handle object, Dart_Handle name, int number_of_arguments, Dart_Handle* arguments); /** * Gets the value of a field. * * The 'container' parameter may be an object, type, or library. If * 'container' is an object, then this function will access an * instance field. If 'container' is a type, then this function will * access a static field. If 'container' is a library, then this * function will access a top-level variable. * NOTE: This API call cannot be used to access fields of a type object. * * This function ignores field visibility (leading underscores in names). * * May generate an unhandled exception error. * * \param container An object, type, or library. * \param name A field name. * * \return If no error occurs, then the value of the field is * returned. Otherwise an error handle is returned. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_GetField(Dart_Handle container, Dart_Handle name); /** * Sets the value of a field. * * The 'container' parameter may actually be an object, type, or * library. If 'container' is an object, then this function will * access an instance field. If 'container' is a type, then this * function will access a static field. If 'container' is a library, * then this function will access a top-level variable. * NOTE: This API call cannot be used to access fields of a type object. * * This function ignores field visibility (leading underscores in names). * * May generate an unhandled exception error. * * \param container An object, type, or library. * \param name A field name. * \param value The new field value. * * \return A valid handle if no error occurs. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_SetField(Dart_Handle container, Dart_Handle name, Dart_Handle value); /* * ========== * Exceptions * ========== */ /* * TODO(turnidge): Remove these functions from the api and replace all * uses with Dart_NewUnhandledExceptionError. */ /** * Throws an exception. * * This function causes a Dart language exception to be thrown. This * will proceed in the standard way, walking up Dart frames until an * appropriate 'catch' block is found, executing 'finally' blocks, * etc. * * If an error handle is passed into this function, the error is * propagated immediately. See Dart_PropagateError for a discussion * of error propagation. * * If successful, this function does not return. Note that this means * that the destructors of any stack-allocated C++ objects will not be * called. If there are no Dart frames on the stack, an error occurs. * * \return An error handle if the exception was not thrown. * Otherwise the function does not return. */ DART_EXPORT Dart_Handle Dart_ThrowException(Dart_Handle exception); /** * Rethrows an exception. * * Rethrows an exception, unwinding all dart frames on the stack. If * successful, this function does not return. Note that this means * that the destructors of any stack-allocated C++ objects will not be * called. If there are no Dart frames on the stack, an error occurs. * * \return An error handle if the exception was not thrown. * Otherwise the function does not return. */ DART_EXPORT Dart_Handle Dart_ReThrowException(Dart_Handle exception, Dart_Handle stacktrace); /* * =========================== * Native fields and functions * =========================== */ /** * Gets the number of native instance fields in an object. */ DART_EXPORT Dart_Handle Dart_GetNativeInstanceFieldCount(Dart_Handle obj, int* count); /** * Gets the value of a native field. * * TODO(turnidge): Document. */ DART_EXPORT Dart_Handle Dart_GetNativeInstanceField(Dart_Handle obj, int index, intptr_t* value); /** * Sets the value of a native field. * * TODO(turnidge): Document. */ DART_EXPORT Dart_Handle Dart_SetNativeInstanceField(Dart_Handle obj, int index, intptr_t value); /** * The arguments to a native function. * * This object is passed to a native function to represent its * arguments and return value. It allows access to the arguments to a * native function by index. It also allows the return value of a * native function to be set. */ typedef struct _Dart_NativeArguments* Dart_NativeArguments; /** * Extracts current isolate group data from the native arguments structure. */ DART_EXPORT void* Dart_GetNativeIsolateGroupData(Dart_NativeArguments args); typedef enum { Dart_NativeArgument_kBool = 0, Dart_NativeArgument_kInt32, Dart_NativeArgument_kUint32, Dart_NativeArgument_kInt64, Dart_NativeArgument_kUint64, Dart_NativeArgument_kDouble, Dart_NativeArgument_kString, Dart_NativeArgument_kInstance, Dart_NativeArgument_kNativeFields, } Dart_NativeArgument_Type; typedef struct _Dart_NativeArgument_Descriptor { uint8_t type; uint8_t index; } Dart_NativeArgument_Descriptor; typedef union _Dart_NativeArgument_Value { bool as_bool; int32_t as_int32; uint32_t as_uint32; int64_t as_int64; uint64_t as_uint64; double as_double; struct { Dart_Handle dart_str; void* peer; } as_string; struct { intptr_t num_fields; intptr_t* values; } as_native_fields; Dart_Handle as_instance; } Dart_NativeArgument_Value; enum { kNativeArgNumberPos = 0, kNativeArgNumberSize = 8, kNativeArgTypePos = kNativeArgNumberPos + kNativeArgNumberSize, kNativeArgTypeSize = 8, }; #define BITMASK(size) ((1 << size) - 1) #define DART_NATIVE_ARG_DESCRIPTOR(type, position) \ (((type & BITMASK(kNativeArgTypeSize)) << kNativeArgTypePos) | \ (position & BITMASK(kNativeArgNumberSize))) /** * Gets the native arguments based on the types passed in and populates * the passed arguments buffer with appropriate native values. * * \param args the Native arguments block passed into the native call. * \param num_arguments length of argument descriptor array and argument * values array passed in. * \param arg_descriptors an array that describes the arguments that * need to be retrieved. For each argument to be retrieved the descriptor * contains the argument number (0, 1 etc.) and the argument type * described using Dart_NativeArgument_Type, e.g: * DART_NATIVE_ARG_DESCRIPTOR(Dart_NativeArgument_kBool, 1) indicates * that the first argument is to be retrieved and it should be a boolean. * \param arg_values array into which the native arguments need to be * extracted into, the array is allocated by the caller (it could be * stack allocated to avoid the malloc/free performance overhead). * * \return Success if all the arguments could be extracted correctly, * returns an error handle if there were any errors while extracting the * arguments (mismatched number of arguments, incorrect types, etc.). */ DART_EXPORT Dart_Handle Dart_GetNativeArguments(Dart_NativeArguments args, int num_arguments, const Dart_NativeArgument_Descriptor* arg_descriptors, Dart_NativeArgument_Value* arg_values); /** * Gets the native argument at some index. */ DART_EXPORT Dart_Handle Dart_GetNativeArgument(Dart_NativeArguments args, int index); /* TODO(turnidge): Specify the behavior of an out-of-bounds access. */ /** * Gets the number of native arguments. */ DART_EXPORT int Dart_GetNativeArgumentCount(Dart_NativeArguments args); /** * Gets all the native fields of the native argument at some index. * \param args Native arguments structure. * \param arg_index Index of the desired argument in the structure above. * \param num_fields size of the intptr_t array 'field_values' passed in. * \param field_values intptr_t array in which native field values are returned. * \return Success if the native fields where copied in successfully. Otherwise * returns an error handle. On success the native field values are copied * into the 'field_values' array, if the argument at 'arg_index' is a * null object then 0 is copied as the native field values into the * 'field_values' array. */ DART_EXPORT Dart_Handle Dart_GetNativeFieldsOfArgument(Dart_NativeArguments args, int arg_index, int num_fields, intptr_t* field_values); /** * Gets the native field of the receiver. */ DART_EXPORT Dart_Handle Dart_GetNativeReceiver(Dart_NativeArguments args, intptr_t* value); /** * Gets a string native argument at some index. * \param args Native arguments structure. * \param arg_index Index of the desired argument in the structure above. * \param peer Returns the peer pointer if the string argument has one. * \return Success if the string argument has a peer, if it does not * have a peer then the String object is returned. Otherwise returns * an error handle (argument is not a String object). */ DART_EXPORT Dart_Handle Dart_GetNativeStringArgument(Dart_NativeArguments args, int arg_index, void** peer); /** * Gets an integer native argument at some index. * \param args Native arguments structure. * \param index Index of the desired argument in the structure above. * \param value Returns the integer value if the argument is an Integer. * \return Success if no error occurs. Otherwise returns an error handle. */ DART_EXPORT Dart_Handle Dart_GetNativeIntegerArgument(Dart_NativeArguments args, int index, int64_t* value); /** * Gets a boolean native argument at some index. * \param args Native arguments structure. * \param index Index of the desired argument in the structure above. * \param value Returns the boolean value if the argument is a Boolean. * \return Success if no error occurs. Otherwise returns an error handle. */ DART_EXPORT Dart_Handle Dart_GetNativeBooleanArgument(Dart_NativeArguments args, int index, bool* value); /** * Gets a double native argument at some index. * \param args Native arguments structure. * \param index Index of the desired argument in the structure above. * \param value Returns the double value if the argument is a double. * \return Success if no error occurs. Otherwise returns an error handle. */ DART_EXPORT Dart_Handle Dart_GetNativeDoubleArgument(Dart_NativeArguments args, int index, double* value); /** * Sets the return value for a native function. * * If retval is an Error handle, then error will be propagated once * the native functions exits. See Dart_PropagateError for a * discussion of how different types of errors are propagated. */ DART_EXPORT void Dart_SetReturnValue(Dart_NativeArguments args, Dart_Handle retval); DART_EXPORT void Dart_SetWeakHandleReturnValue(Dart_NativeArguments args, Dart_WeakPersistentHandle rval); DART_EXPORT void Dart_SetBooleanReturnValue(Dart_NativeArguments args, bool retval); DART_EXPORT void Dart_SetIntegerReturnValue(Dart_NativeArguments args, int64_t retval); DART_EXPORT void Dart_SetDoubleReturnValue(Dart_NativeArguments args, double retval); /** * A native function. */ typedef void (*Dart_NativeFunction)(Dart_NativeArguments arguments); /** * Native entry resolution callback. * * For libraries and scripts which have native functions, the embedder * can provide a native entry resolver. This callback is used to map a * name/arity to a Dart_NativeFunction. If no function is found, the * callback should return NULL. * * The parameters to the native resolver function are: * \param name a Dart string which is the name of the native function. * \param num_of_arguments is the number of arguments expected by the * native function. * \param auto_setup_scope is a boolean flag that can be set by the resolver * to indicate if this function needs a Dart API scope (see Dart_EnterScope/ * Dart_ExitScope) to be setup automatically by the VM before calling into * the native function. By default most native functions would require this * to be true but some light weight native functions which do not call back * into the VM through the Dart API may not require a Dart scope to be * setup automatically. * * \return A valid Dart_NativeFunction which resolves to a native entry point * for the native function. * * See Dart_SetNativeResolver. */ typedef Dart_NativeFunction (*Dart_NativeEntryResolver)(Dart_Handle name, int num_of_arguments, bool* auto_setup_scope); /* TODO(turnidge): Consider renaming to NativeFunctionResolver or * NativeResolver. */ /** * Native entry symbol lookup callback. * * For libraries and scripts which have native functions, the embedder * can provide a callback for mapping a native entry to a symbol. This callback * maps a native function entry PC to the native function name. If no native * entry symbol can be found, the callback should return NULL. * * The parameters to the native reverse resolver function are: * \param nf A Dart_NativeFunction. * * \return A const UTF-8 string containing the symbol name or NULL. * * See Dart_SetNativeResolver. */ typedef const uint8_t* (*Dart_NativeEntrySymbol)(Dart_NativeFunction nf); /** * FFI Native C function pointer resolver callback. * * See Dart_SetFfiNativeResolver. */ typedef void* (*Dart_FfiNativeResolver)(const char* name, uintptr_t args_n); /* * =========== * Environment * =========== */ /** * An environment lookup callback function. * * \param name The name of the value to lookup in the environment. * * \return A valid handle to a string if the name exists in the * current environment or Dart_Null() if not. */ typedef Dart_Handle (*Dart_EnvironmentCallback)(Dart_Handle name); /** * Sets the environment callback for the current isolate. This * callback is used to lookup environment values by name in the * current environment. This enables the embedder to supply values for * the const constructors bool.fromEnvironment, int.fromEnvironment * and String.fromEnvironment. */ DART_EXPORT Dart_Handle Dart_SetEnvironmentCallback(Dart_EnvironmentCallback callback); /** * Sets the callback used to resolve native functions for a library. * * \param library A library. * \param resolver A native entry resolver. * * \return A valid handle if the native resolver was set successfully. */ DART_EXPORT Dart_Handle Dart_SetNativeResolver(Dart_Handle library, Dart_NativeEntryResolver resolver, Dart_NativeEntrySymbol symbol); /* TODO(turnidge): Rename to Dart_LibrarySetNativeResolver? */ /** * Returns the callback used to resolve native functions for a library. * * \param library A library. * \param resolver a pointer to a Dart_NativeEntryResolver * * \return A valid handle if the library was found. */ DART_EXPORT Dart_Handle Dart_GetNativeResolver(Dart_Handle library, Dart_NativeEntryResolver* resolver); /** * Returns the callback used to resolve native function symbols for a library. * * \param library A library. * \param resolver a pointer to a Dart_NativeEntrySymbol. * * \return A valid handle if the library was found. */ DART_EXPORT Dart_Handle Dart_GetNativeSymbol(Dart_Handle library, Dart_NativeEntrySymbol* resolver); /** * Sets the callback used to resolve FFI native functions for a library. * The resolved functions are expected to be a C function pointer of the * correct signature (as specified in the `@FfiNative()` function * annotation in Dart code). * * NOTE: This is an experimental feature and might change in the future. * * \param library A library. * \param resolver A native function resolver. * * \return A valid handle if the native resolver was set successfully. */ DART_EXPORT Dart_Handle Dart_SetFfiNativeResolver(Dart_Handle library, Dart_FfiNativeResolver resolver); /* * ===================== * Scripts and Libraries * ===================== */ typedef enum { Dart_kCanonicalizeUrl = 0, Dart_kImportTag, Dart_kKernelTag, } Dart_LibraryTag; /** * The library tag handler is a multi-purpose callback provided by the * embedder to the Dart VM. The embedder implements the tag handler to * provide the ability to load Dart scripts and imports. * * -- TAGS -- * * Dart_kCanonicalizeUrl * * This tag indicates that the embedder should canonicalize 'url' with * respect to 'library'. For most embedders, the * Dart_DefaultCanonicalizeUrl function is a sufficient implementation * of this tag. The return value should be a string holding the * canonicalized url. * * Dart_kImportTag * * This tag is used to load a library from IsolateMirror.loadUri. The embedder * should call Dart_LoadLibraryFromKernel to provide the library to the VM. The * return value should be an error or library (the result from * Dart_LoadLibraryFromKernel). * * Dart_kKernelTag * * This tag is used to load the intermediate file (kernel) generated by * the Dart front end. This tag is typically used when a 'hot-reload' * of an application is needed and the VM is 'use dart front end' mode. * The dart front end typically compiles all the scripts, imports and part * files into one intermediate file hence we don't use the source/import or * script tags. The return value should be an error or a TypedData containing * the kernel bytes. * */ typedef Dart_Handle (*Dart_LibraryTagHandler)( Dart_LibraryTag tag, Dart_Handle library_or_package_map_url, Dart_Handle url); /** * Sets library tag handler for the current isolate. This handler is * used to handle the various tags encountered while loading libraries * or scripts in the isolate. * * \param handler Handler code to be used for handling the various tags * encountered while loading libraries or scripts in the isolate. * * \return If no error occurs, the handler is set for the isolate. * Otherwise an error handle is returned. * * TODO(turnidge): Document. */ DART_EXPORT Dart_Handle Dart_SetLibraryTagHandler(Dart_LibraryTagHandler handler); /** * Handles deferred loading requests. When this handler is invoked, it should * eventually load the deferred loading unit with the given id and call * Dart_DeferredLoadComplete or Dart_DeferredLoadCompleteError. It is * recommended that the loading occur asynchronously, but it is permitted to * call Dart_DeferredLoadComplete or Dart_DeferredLoadCompleteError before the * handler returns. * * If an error is returned, it will be propagated through * `prefix.loadLibrary()`. This is useful for synchronous * implementations, which must propagate any unwind errors from * Dart_DeferredLoadComplete or Dart_DeferredLoadComplete. Otherwise the handler * should return a non-error such as `Dart_Null()`. */ typedef Dart_Handle (*Dart_DeferredLoadHandler)(intptr_t loading_unit_id); /** * Sets the deferred load handler for the current isolate. This handler is * used to handle loading deferred imports in an AppJIT or AppAOT program. */ DART_EXPORT Dart_Handle Dart_SetDeferredLoadHandler(Dart_DeferredLoadHandler handler); /** * Notifies the VM that a deferred load completed successfully. This function * will eventually cause the corresponding `prefix.loadLibrary()` futures to * complete. * * Requires the current isolate to be the same current isolate during the * invocation of the Dart_DeferredLoadHandler. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_DeferredLoadComplete(intptr_t loading_unit_id, const uint8_t* snapshot_data, const uint8_t* snapshot_instructions); /** * Notifies the VM that a deferred load failed. This function * will eventually cause the corresponding `prefix.loadLibrary()` futures to * complete with an error. * * If `transient` is true, future invocations of `prefix.loadLibrary()` will * trigger new load requests. If false, futures invocation will complete with * the same error. * * Requires the current isolate to be the same current isolate during the * invocation of the Dart_DeferredLoadHandler. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_DeferredLoadCompleteError(intptr_t loading_unit_id, const char* error_message, bool transient); /** * Canonicalizes a url with respect to some library. * * The url is resolved with respect to the library's url and some url * normalizations are performed. * * This canonicalization function should be sufficient for most * embedders to implement the Dart_kCanonicalizeUrl tag. * * \param base_url The base url relative to which the url is * being resolved. * \param url The url being resolved and canonicalized. This * parameter is a string handle. * * \return If no error occurs, a String object is returned. Otherwise * an error handle is returned. */ DART_EXPORT Dart_Handle Dart_DefaultCanonicalizeUrl(Dart_Handle base_url, Dart_Handle url); /** * Loads the root library for the current isolate. * * Requires there to be no current root library. * * \param kernel_buffer A buffer which contains a kernel binary (see * pkg/kernel/binary.md). Must remain valid until isolate group shutdown. * \param kernel_size Length of the passed in buffer. * * \return A handle to the root library, or an error. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_LoadScriptFromKernel(const uint8_t* kernel_buffer, intptr_t kernel_size); /** * Gets the library for the root script for the current isolate. * * If the root script has not yet been set for the current isolate, * this function returns Dart_Null(). This function never returns an * error handle. * * \return Returns the root Library for the current isolate or Dart_Null(). */ DART_EXPORT Dart_Handle Dart_RootLibrary(void); /** * Sets the root library for the current isolate. * * \return Returns an error handle if `library` is not a library handle. */ DART_EXPORT Dart_Handle Dart_SetRootLibrary(Dart_Handle library); /** * Lookup or instantiate a legacy type by name and type arguments from a * Library. * * \param library The library containing the class or interface. * \param class_name The class name for the type. * \param number_of_type_arguments Number of type arguments. * For non parametric types the number of type arguments would be 0. * \param type_arguments Pointer to an array of type arguments. * For non parametric types a NULL would be passed in for this argument. * * \return If no error occurs, the type is returned. * Otherwise an error handle is returned. */ DART_EXPORT Dart_Handle Dart_GetType(Dart_Handle library, Dart_Handle class_name, intptr_t number_of_type_arguments, Dart_Handle* type_arguments); /** * Lookup or instantiate a nullable type by name and type arguments from * Library. * * \param library The library containing the class or interface. * \param class_name The class name for the type. * \param number_of_type_arguments Number of type arguments. * For non parametric types the number of type arguments would be 0. * \param type_arguments Pointer to an array of type arguments. * For non parametric types a NULL would be passed in for this argument. * * \return If no error occurs, the type is returned. * Otherwise an error handle is returned. */ DART_EXPORT Dart_Handle Dart_GetNullableType(Dart_Handle library, Dart_Handle class_name, intptr_t number_of_type_arguments, Dart_Handle* type_arguments); /** * Lookup or instantiate a non-nullable type by name and type arguments from * Library. * * \param library The library containing the class or interface. * \param class_name The class name for the type. * \param number_of_type_arguments Number of type arguments. * For non parametric types the number of type arguments would be 0. * \param type_arguments Pointer to an array of type arguments. * For non parametric types a NULL would be passed in for this argument. * * \return If no error occurs, the type is returned. * Otherwise an error handle is returned. */ DART_EXPORT Dart_Handle Dart_GetNonNullableType(Dart_Handle library, Dart_Handle class_name, intptr_t number_of_type_arguments, Dart_Handle* type_arguments); /** * Creates a nullable version of the provided type. * * \param type The type to be converted to a nullable type. * * \return If no error occurs, a nullable type is returned. * Otherwise an error handle is returned. */ DART_EXPORT Dart_Handle Dart_TypeToNullableType(Dart_Handle type); /** * Creates a non-nullable version of the provided type. * * \param type The type to be converted to a non-nullable type. * * \return If no error occurs, a non-nullable type is returned. * Otherwise an error handle is returned. */ DART_EXPORT Dart_Handle Dart_TypeToNonNullableType(Dart_Handle type); /** * A type's nullability. * * \param type A Dart type. * \param result An out parameter containing the result of the check. True if * the type is of the specified nullability, false otherwise. * * \return Returns an error handle if type is not of type Type. */ DART_EXPORT Dart_Handle Dart_IsNullableType(Dart_Handle type, bool* result); DART_EXPORT Dart_Handle Dart_IsNonNullableType(Dart_Handle type, bool* result); DART_EXPORT Dart_Handle Dart_IsLegacyType(Dart_Handle type, bool* result); /** * Lookup a class or interface by name from a Library. * * \param library The library containing the class or interface. * \param class_name The name of the class or interface. * * \return If no error occurs, the class or interface is * returned. Otherwise an error handle is returned. */ DART_EXPORT Dart_Handle Dart_GetClass(Dart_Handle library, Dart_Handle class_name); /* TODO(asiva): The above method needs to be removed once all uses * of it are removed from the embedder code. */ /** * Returns an import path to a Library, such as "file:///test.dart" or * "dart:core". */ DART_EXPORT Dart_Handle Dart_LibraryUrl(Dart_Handle library); /** * Returns a URL from which a Library was loaded. */ DART_EXPORT Dart_Handle Dart_LibraryResolvedUrl(Dart_Handle library); /** * \return An array of libraries. */ DART_EXPORT Dart_Handle Dart_GetLoadedLibraries(void); DART_EXPORT Dart_Handle Dart_LookupLibrary(Dart_Handle url); /* TODO(turnidge): Consider returning Dart_Null() when the library is * not found to distinguish that from a true error case. */ /** * Report an loading error for the library. * * \param library The library that failed to load. * \param error The Dart error instance containing the load error. * * \return If the VM handles the error, the return value is * a null handle. If it doesn't handle the error, the error * object is returned. */ DART_EXPORT Dart_Handle Dart_LibraryHandleError(Dart_Handle library, Dart_Handle error); /** * Called by the embedder to load a partial program. Does not set the root * library. * * \param kernel_buffer A buffer which contains a kernel binary (see * pkg/kernel/binary.md). Must remain valid until isolate shutdown. * \param kernel_buffer_size Length of the passed in buffer. * * \return A handle to the main library of the compilation unit, or an error. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_LoadLibraryFromKernel(const uint8_t* kernel_buffer, intptr_t kernel_buffer_size); DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_LoadLibrary(Dart_Handle kernel_buffer); /** * Indicates that all outstanding load requests have been satisfied. * This finalizes all the new classes loaded and optionally completes * deferred library futures. * * Requires there to be a current isolate. * * \param complete_futures Specify true if all deferred library * futures should be completed, false otherwise. * * \return Success if all classes have been finalized and deferred library * futures are completed. Otherwise, returns an error. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_FinalizeLoading(bool complete_futures); /* * ===== * Peers * ===== */ /** * The peer field is a lazily allocated field intended for storage of * an uncommonly used values. Most instances types can have a peer * field allocated. The exceptions are subtypes of Null, num, and * bool. */ /** * Returns the value of peer field of 'object' in 'peer'. * * \param object An object. * \param peer An out parameter that returns the value of the peer * field. * * \return Returns an error if 'object' is a subtype of Null, num, or * bool. */ DART_EXPORT Dart_Handle Dart_GetPeer(Dart_Handle object, void** peer); /** * Sets the value of the peer field of 'object' to the value of * 'peer'. * * \param object An object. * \param peer A value to store in the peer field. * * \return Returns an error if 'object' is a subtype of Null, num, or * bool. */ DART_EXPORT Dart_Handle Dart_SetPeer(Dart_Handle object, void* peer); /* * ====== * Kernel * ====== */ /** * Experimental support for Dart to Kernel parser isolate. * * TODO(hausner): Document finalized interface. * */ // TODO(33433): Remove kernel service from the embedding API. typedef enum { Dart_KernelCompilationStatus_Unknown = -1, Dart_KernelCompilationStatus_Ok = 0, Dart_KernelCompilationStatus_Error = 1, Dart_KernelCompilationStatus_Crash = 2, Dart_KernelCompilationStatus_MsgFailed = 3, } Dart_KernelCompilationStatus; typedef struct { Dart_KernelCompilationStatus status; bool null_safety; char* error; uint8_t* kernel; intptr_t kernel_size; } Dart_KernelCompilationResult; typedef enum { Dart_KernelCompilationVerbosityLevel_Error = 0, Dart_KernelCompilationVerbosityLevel_Warning, Dart_KernelCompilationVerbosityLevel_Info, Dart_KernelCompilationVerbosityLevel_All, } Dart_KernelCompilationVerbosityLevel; DART_EXPORT bool Dart_IsKernelIsolate(Dart_Isolate isolate); DART_EXPORT bool Dart_KernelIsolateIsRunning(void); DART_EXPORT Dart_Port Dart_KernelPort(void); /** * Compiles the given `script_uri` to a kernel file. * * \param platform_kernel A buffer containing the kernel of the platform (e.g. * `vm_platform_strong.dill`). The VM does not take ownership of this memory. * * \param platform_kernel_size The length of the platform_kernel buffer. * * \param snapshot_compile Set to `true` when the compilation is for a snapshot. * This is used by the frontend to determine if compilation related information * should be printed to console (e.g., null safety mode). * * \param verbosity Specifies the logging behavior of the kernel compilation * service. * * \return Returns the result of the compilation. * * On a successful compilation the returned [Dart_KernelCompilationResult] has * a status of [Dart_KernelCompilationStatus_Ok] and the `kernel`/`kernel_size` * fields are set. The caller takes ownership of the malloc()ed buffer. * * On a failed compilation the `error` might be set describing the reason for * the failed compilation. The caller takes ownership of the malloc()ed * error. * * Requires there to be a current isolate. */ DART_EXPORT Dart_KernelCompilationResult Dart_CompileToKernel(const char* script_uri, const uint8_t* platform_kernel, const intptr_t platform_kernel_size, bool incremental_compile, bool snapshot_compile, const char* package_config, Dart_KernelCompilationVerbosityLevel verbosity); typedef struct { const char* uri; const char* source; } Dart_SourceFile; DART_EXPORT Dart_KernelCompilationResult Dart_KernelListDependencies(void); /** * Sets the kernel buffer which will be used to load Dart SDK sources * dynamically at runtime. * * \param platform_kernel A buffer containing kernel which has sources for the * Dart SDK populated. Note: The VM does not take ownership of this memory. * * \param platform_kernel_size The length of the platform_kernel buffer. */ DART_EXPORT void Dart_SetDartLibrarySourcesKernel( const uint8_t* platform_kernel, const intptr_t platform_kernel_size); /** * Detect the null safety opt-in status. * * When running from source, it is based on the opt-in status of `script_uri`. * When running from a kernel buffer, it is based on the mode used when * generating `kernel_buffer`. * When running from an appJIT or AOT snapshot, it is based on the mode used * when generating `snapshot_data`. * * \param script_uri Uri of the script that contains the source code * * \param package_config Uri of the package configuration file (either in format * of .packages or .dart_tool/package_config.json) for the null safety * detection to resolve package imports against. If this parameter is not * passed the package resolution of the parent isolate should be used. * * \param original_working_directory current working directory when the VM * process was launched, this is used to correctly resolve the path specified * for package_config. * * \param snapshot_data Buffer containing the snapshot data of the * isolate or NULL if no snapshot is provided. If provided, the buffers must * remain valid until the isolate shuts down. * * \param snapshot_instructions Buffer containing the snapshot instructions of * the isolate or NULL if no snapshot is provided. If provided, the buffers * must remain valid until the isolate shuts down. * * \param kernel_buffer A buffer which contains a kernel/DIL program. Must * remain valid until isolate shutdown. * * \param kernel_buffer_size The size of `kernel_buffer`. * * \return Returns true if the null safety is opted in by the input being * run `script_uri`, `snapshot_data` or `kernel_buffer`. * */ DART_EXPORT bool Dart_DetectNullSafety(const char* script_uri, const char* package_config, const char* original_working_directory, const uint8_t* snapshot_data, const uint8_t* snapshot_instructions, const uint8_t* kernel_buffer, intptr_t kernel_buffer_size); #define DART_KERNEL_ISOLATE_NAME "kernel-service" /* * ======= * Service * ======= */ #define DART_VM_SERVICE_ISOLATE_NAME "vm-service" /** * Returns true if isolate is the service isolate. * * \param isolate An isolate * * \return Returns true if 'isolate' is the service isolate. */ DART_EXPORT bool Dart_IsServiceIsolate(Dart_Isolate isolate); /** * Writes the CPU profile to the timeline as a series of 'instant' events. * * Note that this is an expensive operation. * * \param main_port The main port of the Isolate whose profile samples to write. * \param error An optional error, must be free()ed by caller. * * \return Returns true if the profile is successfully written and false * otherwise. */ DART_EXPORT bool Dart_WriteProfileToTimeline(Dart_Port main_port, char** error); /* * ============== * Precompilation * ============== */ /** * Compiles all functions reachable from entry points and marks * the isolate to disallow future compilation. * * Entry points should be specified using `@pragma("vm:entry-point")` * annotation. * * \return An error handle if a compilation error or runtime error running const * constructors was encountered. */ DART_EXPORT Dart_Handle Dart_Precompile(void); typedef void (*Dart_CreateLoadingUnitCallback)( void* callback_data, intptr_t loading_unit_id, void** write_callback_data, void** write_debug_callback_data); typedef void (*Dart_StreamingWriteCallback)(void* callback_data, const uint8_t* buffer, intptr_t size); typedef void (*Dart_StreamingCloseCallback)(void* callback_data); DART_EXPORT Dart_Handle Dart_LoadingUnitLibraryUris(intptr_t loading_unit_id); // On Darwin systems, 'dlsym' adds an '_' to the beginning of the symbol name. // Use the '...CSymbol' definitions for resolving through 'dlsym'. The actual // symbol names in the objects are given by the '...AsmSymbol' definitions. #if defined(__APPLE__) #define kSnapshotBuildIdCSymbol "kDartSnapshotBuildId" #define kVmSnapshotDataCSymbol "kDartVmSnapshotData" #define kVmSnapshotInstructionsCSymbol "kDartVmSnapshotInstructions" #define kVmSnapshotBssCSymbol "kDartVmSnapshotBss" #define kIsolateSnapshotDataCSymbol "kDartIsolateSnapshotData" #define kIsolateSnapshotInstructionsCSymbol "kDartIsolateSnapshotInstructions" #define kIsolateSnapshotBssCSymbol "kDartIsolateSnapshotBss" #else #define kSnapshotBuildIdCSymbol "_kDartSnapshotBuildId" #define kVmSnapshotDataCSymbol "_kDartVmSnapshotData" #define kVmSnapshotInstructionsCSymbol "_kDartVmSnapshotInstructions" #define kVmSnapshotBssCSymbol "_kDartVmSnapshotBss" #define kIsolateSnapshotDataCSymbol "_kDartIsolateSnapshotData" #define kIsolateSnapshotInstructionsCSymbol "_kDartIsolateSnapshotInstructions" #define kIsolateSnapshotBssCSymbol "_kDartIsolateSnapshotBss" #endif #define kSnapshotBuildIdAsmSymbol "_kDartSnapshotBuildId" #define kVmSnapshotDataAsmSymbol "_kDartVmSnapshotData" #define kVmSnapshotInstructionsAsmSymbol "_kDartVmSnapshotInstructions" #define kVmSnapshotBssAsmSymbol "_kDartVmSnapshotBss" #define kIsolateSnapshotDataAsmSymbol "_kDartIsolateSnapshotData" #define kIsolateSnapshotInstructionsAsmSymbol \ "_kDartIsolateSnapshotInstructions" #define kIsolateSnapshotBssAsmSymbol "_kDartIsolateSnapshotBss" /** * Creates a precompiled snapshot. * - A root library must have been loaded. * - Dart_Precompile must have been called. * * Outputs an assembly file defining the symbols listed in the definitions * above. * * The assembly should be compiled as a static or shared library and linked or * loaded by the embedder. Running this snapshot requires a VM compiled with * DART_PRECOMPILED_SNAPSHOT. The kDartVmSnapshotData and * kDartVmSnapshotInstructions should be passed to Dart_Initialize. The * kDartIsolateSnapshotData and kDartIsolateSnapshotInstructions should be * passed to Dart_CreateIsolateGroup. * * The callback will be invoked one or more times to provide the assembly code. * * If stripped is true, then the assembly code will not include DWARF * debugging sections. * * If debug_callback_data is provided, debug_callback_data will be used with * the callback to provide separate debugging information. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateAppAOTSnapshotAsAssembly(Dart_StreamingWriteCallback callback, void* callback_data, bool stripped, void* debug_callback_data); DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateAppAOTSnapshotAsAssemblies( Dart_CreateLoadingUnitCallback next_callback, void* next_callback_data, bool stripped, Dart_StreamingWriteCallback write_callback, Dart_StreamingCloseCallback close_callback); /** * Creates a precompiled snapshot. * - A root library must have been loaded. * - Dart_Precompile must have been called. * * Outputs an ELF shared library defining the symbols * - _kDartVmSnapshotData * - _kDartVmSnapshotInstructions * - _kDartIsolateSnapshotData * - _kDartIsolateSnapshotInstructions * * The shared library should be dynamically loaded by the embedder. * Running this snapshot requires a VM compiled with DART_PRECOMPILED_SNAPSHOT. * The kDartVmSnapshotData and kDartVmSnapshotInstructions should be passed to * Dart_Initialize. The kDartIsolateSnapshotData and * kDartIsolateSnapshotInstructions should be passed to Dart_CreateIsolate. * * The callback will be invoked one or more times to provide the binary output. * * If stripped is true, then the binary output will not include DWARF * debugging sections. * * If debug_callback_data is provided, debug_callback_data will be used with * the callback to provide separate debugging information. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateAppAOTSnapshotAsElf(Dart_StreamingWriteCallback callback, void* callback_data, bool stripped, void* debug_callback_data); DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateAppAOTSnapshotAsElfs(Dart_CreateLoadingUnitCallback next_callback, void* next_callback_data, bool stripped, Dart_StreamingWriteCallback write_callback, Dart_StreamingCloseCallback close_callback); /** * Like Dart_CreateAppAOTSnapshotAsAssembly, but only includes * kDartVmSnapshotData and kDartVmSnapshotInstructions. It also does * not strip DWARF information from the generated assembly or allow for * separate debug information. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateVMAOTSnapshotAsAssembly(Dart_StreamingWriteCallback callback, void* callback_data); /** * Sorts the class-ids in depth first traversal order of the inheritance * tree. This is a costly operation, but it can make method dispatch * more efficient and is done before writing snapshots. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_SortClasses(void); /** * Creates a snapshot that caches compiled code and type feedback for faster * startup and quicker warmup in a subsequent process. * * Outputs a snapshot in two pieces. The pieces should be passed to * Dart_CreateIsolateGroup in a VM using the same VM snapshot pieces used in the * current VM. The instructions piece must be loaded with read and execute * permissions; the data piece may be loaded as read-only. * * - Requires the VM to have not been started with --precompilation. * - Not supported when targeting IA32. * - The VM writing the snapshot and the VM reading the snapshot must be the * same version, must be built in the same DEBUG/RELEASE/PRODUCT mode, must * be targeting the same architecture, and must both be in checked mode or * both in unchecked mode. * * The buffers are scope allocated and are only valid until the next call to * Dart_ExitScope. * * \return A valid handle if no error occurs during the operation. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateAppJITSnapshotAsBlobs(uint8_t** isolate_snapshot_data_buffer, intptr_t* isolate_snapshot_data_size, uint8_t** isolate_snapshot_instructions_buffer, intptr_t* isolate_snapshot_instructions_size); /** * Like Dart_CreateAppJITSnapshotAsBlobs, but also creates a new VM snapshot. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CreateCoreJITSnapshotAsBlobs( uint8_t** vm_snapshot_data_buffer, intptr_t* vm_snapshot_data_size, uint8_t** vm_snapshot_instructions_buffer, intptr_t* vm_snapshot_instructions_size, uint8_t** isolate_snapshot_data_buffer, intptr_t* isolate_snapshot_data_size, uint8_t** isolate_snapshot_instructions_buffer, intptr_t* isolate_snapshot_instructions_size); /** * Get obfuscation map for precompiled code. * * Obfuscation map is encoded as a JSON array of pairs (original name, * obfuscated name). * * \return Returns an error handler if the VM was built in a mode that does not * support obfuscation. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_GetObfuscationMap(uint8_t** buffer, intptr_t* buffer_length); /** * Returns whether the VM only supports running from precompiled snapshots and * not from any other kind of snapshot or from source (that is, the VM was * compiled with DART_PRECOMPILED_RUNTIME). */ DART_EXPORT bool Dart_IsPrecompiledRuntime(void); /** * Print a native stack trace. Used for crash handling. * * If context is NULL, prints the current stack trace. Otherwise, context * should be a CONTEXT* (Windows) or ucontext_t* (POSIX) from a signal handler * running on the current thread. */ DART_EXPORT void Dart_DumpNativeStackTrace(void* context); /** * Indicate that the process is about to abort, and the Dart VM should not * attempt to cleanup resources. */ DART_EXPORT void Dart_PrepareToAbort(void); /** * Callback provided by the embedder that is used by the VM to * produce footnotes appended to DWARF stack traces. * * Whenever VM formats a stack trace as a string it would call this callback * passing raw program counters for each frame in the stack trace. * * Embedder can then return a string which if not-null will be appended to the * formatted stack trace. * * Returned string is expected to be `malloc()` allocated. VM takes ownership * of the returned string and will `free()` it. * * \param addresses raw program counter addresses for each frame * \param count number of elements in the addresses array */ typedef char* (*Dart_DwarfStackTraceFootnoteCallback)(void* addresses[], intptr_t count); /** * Configure DWARF stack trace footnote callback. */ DART_EXPORT void Dart_SetDwarfStackTraceFootnoteCallback( Dart_DwarfStackTraceFootnoteCallback callback); #endif /* INCLUDE_DART_API_H_ */ /* NOLINT */ ================================================ FILE: core/dart-bridge/include/dart_api_dl.c ================================================ /* * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file * for details. All rights reserved. Use of this source code is governed by a * BSD-style license that can be found in the LICENSE file. */ #include "dart_api_dl.h" /* NOLINT */ #include "dart_version.h" /* NOLINT */ #include "internal/dart_api_dl_impl.h" /* NOLINT */ #include #define DART_API_DL_DEFINITIONS(name, R, A) name##_Type name##_DL = NULL; DART_API_ALL_DL_SYMBOLS(DART_API_DL_DEFINITIONS) #undef DART_API_DL_DEFINITIONS typedef void* DartApiEntry_function; DartApiEntry_function FindFunctionPointer(const DartApiEntry* entries, const char* name) { while (entries->name != NULL) { if (strcmp(entries->name, name) == 0) return entries->function; entries++; } return NULL; } intptr_t Dart_InitializeApiDL(void* data) { DartApi* dart_api_data = (DartApi*)data; if (dart_api_data->major != DART_API_DL_MAJOR_VERSION) { // If the DartVM we're running on does not have the same version as this // file was compiled against, refuse to initialize. The symbols are not // compatible. return -1; } // Minor versions are allowed to be different. // If the DartVM has a higher minor version, it will provide more symbols // than we initialize here. // If the DartVM has a lower minor version, it will not provide all symbols. // In that case, we leave the missing symbols un-initialized. Those symbols // should not be used by the Dart and native code. The client is responsible // for checking the minor version number himself based on which symbols it // is using. // (If we would error out on this case, recompiling native code against a // newer SDK would break all uses on older SDKs, which is too strict.) const DartApiEntry* dart_api_function_pointers = dart_api_data->functions; #define DART_API_DL_INIT(name, R, A) \ name##_DL = \ (name##_Type)(FindFunctionPointer(dart_api_function_pointers, #name)); DART_API_ALL_DL_SYMBOLS(DART_API_DL_INIT) #undef DART_API_DL_INIT return 0; } ================================================ FILE: core/dart-bridge/include/dart_api_dl.h ================================================ /* * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file * for details. All rights reserved. Use of this source code is governed by a * BSD-style license that can be found in the LICENSE file. */ #ifndef RUNTIME_INCLUDE_DART_API_DL_H_ #define RUNTIME_INCLUDE_DART_API_DL_H_ #include "dart_api.h" /* NOLINT */ #include "dart_native_api.h" /* NOLINT */ /** \mainpage Dynamically Linked Dart API * * This exposes a subset of symbols from dart_api.h and dart_native_api.h * available in every Dart embedder through dynamic linking. * * All symbols are postfixed with _DL to indicate that they are dynamically * linked and to prevent conflicts with the original symbol. * * Link `dart_api_dl.c` file into your library and invoke * `Dart_InitializeApiDL` with `NativeApi.initializeApiDLData`. */ DART_EXPORT intptr_t Dart_InitializeApiDL(void* data); // ============================================================================ // IMPORTANT! Never update these signatures without properly updating // DART_API_DL_MAJOR_VERSION and DART_API_DL_MINOR_VERSION. // // Verbatim copy of `dart_native_api.h` and `dart_api.h` symbol names and types // to trigger compile-time errors if the symbols in those files are updated // without updating these. // // Function return and argument types, and typedefs are carbon copied. Structs // are typechecked nominally in C/C++, so they are not copied, instead a // comment is added to their definition. typedef int64_t Dart_Port_DL; typedef void (*Dart_NativeMessageHandler_DL)(Dart_Port_DL dest_port_id, Dart_CObject* message); // dart_native_api.h symbols can be called on any thread. #define DART_NATIVE_API_DL_SYMBOLS(F) \ /***** dart_native_api.h *****/ \ /* Dart_Port */ \ F(Dart_PostCObject, bool, (Dart_Port_DL port_id, Dart_CObject * message)) \ F(Dart_PostInteger, bool, (Dart_Port_DL port_id, int64_t message)) \ F(Dart_NewNativePort, Dart_Port_DL, \ (const char* name, Dart_NativeMessageHandler_DL handler, \ bool handle_concurrently)) \ F(Dart_CloseNativePort, bool, (Dart_Port_DL native_port_id)) // dart_api.h symbols can only be called on Dart threads. #define DART_API_DL_SYMBOLS(F) \ /***** dart_api.h *****/ \ /* Errors */ \ F(Dart_IsError, bool, (Dart_Handle handle)) \ F(Dart_IsApiError, bool, (Dart_Handle handle)) \ F(Dart_IsUnhandledExceptionError, bool, (Dart_Handle handle)) \ F(Dart_IsCompilationError, bool, (Dart_Handle handle)) \ F(Dart_IsFatalError, bool, (Dart_Handle handle)) \ F(Dart_GetError, const char*, (Dart_Handle handle)) \ F(Dart_ErrorHasException, bool, (Dart_Handle handle)) \ F(Dart_ErrorGetException, Dart_Handle, (Dart_Handle handle)) \ F(Dart_ErrorGetStackTrace, Dart_Handle, (Dart_Handle handle)) \ F(Dart_NewApiError, Dart_Handle, (const char* error)) \ F(Dart_NewCompilationError, Dart_Handle, (const char* error)) \ F(Dart_NewUnhandledExceptionError, Dart_Handle, (Dart_Handle exception)) \ F(Dart_PropagateError, void, (Dart_Handle handle)) \ /* Dart_Handle, Dart_PersistentHandle, Dart_WeakPersistentHandle */ \ F(Dart_HandleFromPersistent, Dart_Handle, (Dart_PersistentHandle object)) \ F(Dart_HandleFromWeakPersistent, Dart_Handle, \ (Dart_WeakPersistentHandle object)) \ F(Dart_NewPersistentHandle, Dart_PersistentHandle, (Dart_Handle object)) \ F(Dart_SetPersistentHandle, void, \ (Dart_PersistentHandle obj1, Dart_Handle obj2)) \ F(Dart_DeletePersistentHandle, void, (Dart_PersistentHandle object)) \ F(Dart_NewWeakPersistentHandle, Dart_WeakPersistentHandle, \ (Dart_Handle object, void* peer, intptr_t external_allocation_size, \ Dart_HandleFinalizer callback)) \ F(Dart_DeleteWeakPersistentHandle, void, (Dart_WeakPersistentHandle object)) \ F(Dart_UpdateExternalSize, void, \ (Dart_WeakPersistentHandle object, intptr_t external_allocation_size)) \ F(Dart_NewFinalizableHandle, Dart_FinalizableHandle, \ (Dart_Handle object, void* peer, intptr_t external_allocation_size, \ Dart_HandleFinalizer callback)) \ F(Dart_DeleteFinalizableHandle, void, \ (Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object)) \ F(Dart_UpdateFinalizableExternalSize, void, \ (Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object, \ intptr_t external_allocation_size)) \ /* Isolates */ \ F(Dart_CurrentIsolate, Dart_Isolate, (void)) \ F(Dart_ExitIsolate, void, (void)) \ F(Dart_EnterIsolate, void, (Dart_Isolate)) \ /* Dart_Port */ \ F(Dart_Post, bool, (Dart_Port_DL port_id, Dart_Handle object)) \ F(Dart_NewSendPort, Dart_Handle, (Dart_Port_DL port_id)) \ F(Dart_SendPortGetId, Dart_Handle, \ (Dart_Handle port, Dart_Port_DL * port_id)) \ /* Scopes */ \ F(Dart_EnterScope, void, (void)) \ F(Dart_ExitScope, void, (void)) \ /* Objects */ \ F(Dart_IsNull, bool, (Dart_Handle)) #define DART_API_ALL_DL_SYMBOLS(F) \ DART_NATIVE_API_DL_SYMBOLS(F) \ DART_API_DL_SYMBOLS(F) // IMPORTANT! Never update these signatures without properly updating // DART_API_DL_MAJOR_VERSION and DART_API_DL_MINOR_VERSION. // // End of verbatim copy. // ============================================================================ // Copy of definition of DART_EXPORT without 'used' attribute. // // The 'used' attribute cannot be used with DART_API_ALL_DL_SYMBOLS because // they are not function declarations, but variable declarations with a // function pointer type. // // The function pointer variables are initialized with the addresses of the // functions in the VM. If we were to use function declarations instead, we // would need to forward the call to the VM adding indirection. #if defined(__CYGWIN__) #error Tool chain and platform not supported. #elif defined(_WIN32) #if defined(DART_SHARED_LIB) #define DART_EXPORT_DL DART_EXTERN_C __declspec(dllexport) #else #define DART_EXPORT_DL DART_EXTERN_C #endif #else #if __GNUC__ >= 4 #if defined(DART_SHARED_LIB) #define DART_EXPORT_DL DART_EXTERN_C __attribute__((visibility("default"))) #else #define DART_EXPORT_DL DART_EXTERN_C #endif #else #error Tool chain not supported. #endif #endif #define DART_API_DL_DECLARATIONS(name, R, A) \ typedef R(*name##_Type) A; \ DART_EXPORT_DL name##_Type name##_DL; DART_API_ALL_DL_SYMBOLS(DART_API_DL_DECLARATIONS) #undef DART_API_DL_DECLARATIONS #undef DART_EXPORT_DL #endif /* RUNTIME_INCLUDE_DART_API_DL_H_ */ /* NOLINT */ ================================================ FILE: core/dart-bridge/include/dart_native_api.h ================================================ /* * Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file * for details. All rights reserved. Use of this source code is governed by a * BSD-style license that can be found in the LICENSE file. */ #ifndef RUNTIME_INCLUDE_DART_NATIVE_API_H_ #define RUNTIME_INCLUDE_DART_NATIVE_API_H_ #include "dart_api.h" /* NOLINT */ /* * ========================================== * Message sending/receiving from native code * ========================================== */ /** * A Dart_CObject is used for representing Dart objects as native C * data outside the Dart heap. These objects are totally detached from * the Dart heap. Only a subset of the Dart objects have a * representation as a Dart_CObject. * * The string encoding in the 'value.as_string' is UTF-8. * * All the different types from dart:typed_data are exposed as type * kTypedData. The specific type from dart:typed_data is in the type * field of the as_typed_data structure. The length in the * as_typed_data structure is always in bytes. * * The data for kTypedData is copied on message send and ownership remains with * the caller. The ownership of data for kExternalTyped is passed to the VM on * message send and returned when the VM invokes the * Dart_HandleFinalizer callback; a non-NULL callback must be provided. * * Note that Dart_CObject_kNativePointer is intended for internal use by * dart:io implementation and has no connection to dart:ffi Pointer class. * It represents a pointer to a native resource of a known type. * The receiving side will only see this pointer as an integer and will not * see the specified finalizer. * The specified finalizer will only be invoked if the message is not delivered. */ typedef enum { Dart_CObject_kNull = 0, Dart_CObject_kBool, Dart_CObject_kInt32, Dart_CObject_kInt64, Dart_CObject_kDouble, Dart_CObject_kString, Dart_CObject_kArray, Dart_CObject_kTypedData, Dart_CObject_kExternalTypedData, Dart_CObject_kSendPort, Dart_CObject_kCapability, Dart_CObject_kNativePointer, Dart_CObject_kUnsupported, Dart_CObject_kUnmodifiableExternalTypedData, Dart_CObject_kNumberOfTypes } Dart_CObject_Type; // This enum is versioned by DART_API_DL_MAJOR_VERSION, only add at the end // and bump the DART_API_DL_MINOR_VERSION. typedef struct _Dart_CObject { Dart_CObject_Type type; union { bool as_bool; int32_t as_int32; int64_t as_int64; double as_double; const char* as_string; struct { Dart_Port id; Dart_Port origin_id; } as_send_port; struct { int64_t id; } as_capability; struct { intptr_t length; struct _Dart_CObject** values; } as_array; struct { Dart_TypedData_Type type; intptr_t length; /* in elements, not bytes */ const uint8_t* values; } as_typed_data; struct { Dart_TypedData_Type type; intptr_t length; /* in elements, not bytes */ uint8_t* data; void* peer; Dart_HandleFinalizer callback; } as_external_typed_data; struct { intptr_t ptr; intptr_t size; Dart_HandleFinalizer callback; } as_native_pointer; } value; } Dart_CObject; // This struct is versioned by DART_API_DL_MAJOR_VERSION, bump the version when // changing this struct. /** * Posts a message on some port. The message will contain the Dart_CObject * object graph rooted in 'message'. * * While the message is being sent the state of the graph of Dart_CObject * structures rooted in 'message' should not be accessed, as the message * generation will make temporary modifications to the data. When the message * has been sent the graph will be fully restored. * * If true is returned, the message was enqueued, and finalizers for external * typed data will eventually run, even if the receiving isolate shuts down * before processing the message. If false is returned, the message was not * enqueued and ownership of external typed data in the message remains with the * caller. * * This function may be called on any thread when the VM is running (that is, * after Dart_Initialize has returned and before Dart_Cleanup has been called). * * \param port_id The destination port. * \param message The message to send. * * \return True if the message was posted. */ DART_EXPORT bool Dart_PostCObject(Dart_Port port_id, Dart_CObject* message); /** * Posts a message on some port. The message will contain the integer 'message'. * * \param port_id The destination port. * \param message The message to send. * * \return True if the message was posted. */ DART_EXPORT bool Dart_PostInteger(Dart_Port port_id, int64_t message); /** * A native message handler. * * This handler is associated with a native port by calling * Dart_NewNativePort. * * The message received is decoded into the message structure. The * lifetime of the message data is controlled by the caller. All the * data references from the message are allocated by the caller and * will be reclaimed when returning to it. */ typedef void (*Dart_NativeMessageHandler)(Dart_Port dest_port_id, Dart_CObject* message); /** * Creates a new native port. When messages are received on this * native port, then they will be dispatched to the provided native * message handler. * * \param name The name of this port in debugging messages. * \param handler The C handler to run when messages arrive on the port. * \param handle_concurrently Is it okay to process requests on this * native port concurrently? * * \return If successful, returns the port id for the native port. In * case of error, returns ILLEGAL_PORT. */ DART_EXPORT Dart_Port Dart_NewNativePort(const char* name, Dart_NativeMessageHandler handler, bool handle_concurrently); /* TODO(turnidge): Currently handle_concurrently is ignored. */ /** * Closes the native port with the given id. * * The port must have been allocated by a call to Dart_NewNativePort. * * \param native_port_id The id of the native port to close. * * \return Returns true if the port was closed successfully. */ DART_EXPORT bool Dart_CloseNativePort(Dart_Port native_port_id); /* * ================== * Verification Tools * ================== */ /** * Forces all loaded classes and functions to be compiled eagerly in * the current isolate.. * * TODO(turnidge): Document. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CompileAll(void); /** * Finalizes all classes. */ DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_FinalizeAllClasses(void); /* This function is intentionally undocumented. * * It should not be used outside internal tests. */ DART_EXPORT void* Dart_ExecuteInternalCommand(const char* command, void* arg); #endif /* INCLUDE_DART_NATIVE_API_H_ */ /* NOLINT */ ================================================ FILE: core/dart-bridge/include/dart_tools_api.h ================================================ // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. #ifndef RUNTIME_INCLUDE_DART_TOOLS_API_H_ #define RUNTIME_INCLUDE_DART_TOOLS_API_H_ #include "dart_api.h" /* NOLINT */ /** \mainpage Dart Tools Embedding API Reference * * This reference describes the Dart embedding API for tools. Tools include * a debugger, service protocol, and timeline. * * NOTE: The APIs described in this file are unstable and subject to change. * * This reference is generated from the header include/dart_tools_api.h. */ /* * ======== * Debugger * ======== */ /** * ILLEGAL_ISOLATE_ID is a number guaranteed never to be associated with a * valid isolate. */ #define ILLEGAL_ISOLATE_ID ILLEGAL_PORT /** * ILLEGAL_ISOLATE_GROUP_ID is a number guaranteed never to be associated with a * valid isolate group. */ #define ILLEGAL_ISOLATE_GROUP_ID 0 /* * ======= * Service * ======= */ /** * A service request callback function. * * These callbacks, registered by the embedder, are called when the VM receives * a service request it can't handle and the service request command name * matches one of the embedder registered handlers. * * The return value of the callback indicates whether the response * should be used as a regular result or an error result. * Specifically, if the callback returns true, a regular JSON-RPC * response is built in the following way: * * { * "jsonrpc": "2.0", * "result": , * "id": , * } * * If the callback returns false, a JSON-RPC error is built like this: * * { * "jsonrpc": "2.0", * "error": , * "id": , * } * * \param method The rpc method name. * \param param_keys Service requests can have key-value pair parameters. The * keys and values are flattened and stored in arrays. * \param param_values The values associated with the keys. * \param num_params The length of the param_keys and param_values arrays. * \param user_data The user_data pointer registered with this handler. * \param result A C string containing a valid JSON object. The returned * pointer will be freed by the VM by calling free. * * \return True if the result is a regular JSON-RPC response, false if the * result is a JSON-RPC error. */ typedef bool (*Dart_ServiceRequestCallback)(const char* method, const char** param_keys, const char** param_values, intptr_t num_params, void* user_data, const char** json_object); /** * Register a Dart_ServiceRequestCallback to be called to handle * requests for the named rpc on a specific isolate. The callback will * be invoked with the current isolate set to the request target. * * \param method The name of the method that this callback is responsible for. * \param callback The callback to invoke. * \param user_data The user data passed to the callback. * * NOTE: If multiple callbacks with the same name are registered, only * the last callback registered will be remembered. */ DART_EXPORT void Dart_RegisterIsolateServiceRequestCallback( const char* method, Dart_ServiceRequestCallback callback, void* user_data); /** * Register a Dart_ServiceRequestCallback to be called to handle * requests for the named rpc. The callback will be invoked without a * current isolate. * * \param method The name of the command that this callback is responsible for. * \param callback The callback to invoke. * \param user_data The user data passed to the callback. * * NOTE: If multiple callbacks with the same name are registered, only * the last callback registered will be remembered. */ DART_EXPORT void Dart_RegisterRootServiceRequestCallback( const char* method, Dart_ServiceRequestCallback callback, void* user_data); /** * Embedder information which can be requested by the VM for internal or * reporting purposes. * * The pointers in this structure are not going to be cached or freed by the VM. */ #define DART_EMBEDDER_INFORMATION_CURRENT_VERSION (0x00000001) typedef struct { int32_t version; const char* name; // [optional] The name of the embedder int64_t current_rss; // [optional] the current RSS of the embedder int64_t max_rss; // [optional] the maximum RSS of the embedder } Dart_EmbedderInformation; /** * Callback provided by the embedder that is used by the VM to request * information. * * \return Returns a pointer to a Dart_EmbedderInformation structure. * The embedder keeps the ownership of the structure and any field in it. * The embedder must ensure that the structure will remain valid until the * next invocation of the callback. */ typedef void (*Dart_EmbedderInformationCallback)( Dart_EmbedderInformation* info); /** * Register a Dart_ServiceRequestCallback to be called to handle * requests for the named rpc. The callback will be invoked without a * current isolate. * * \param method The name of the command that this callback is responsible for. * \param callback The callback to invoke. * \param user_data The user data passed to the callback. * * NOTE: If multiple callbacks are registered, only the last callback registered * will be remembered. */ DART_EXPORT void Dart_SetEmbedderInformationCallback( Dart_EmbedderInformationCallback callback); /** * Invoke a vm-service method and wait for its result. * * \param request_json The utf8-encoded json-rpc request. * \param request_json_length The length of the json-rpc request. * * \param response_json The returned utf8-encoded json response, must be * free()ed by caller. * \param response_json_length The length of the returned json response. * \param error An optional error, must be free()ed by caller. * * \return Whether the call was successfully performed. * * NOTE: This method does not need a current isolate and must not have the * vm-isolate being the current isolate. It must be called after * Dart_Initialize() and before Dart_Cleanup(). */ DART_EXPORT bool Dart_InvokeVMServiceMethod(uint8_t* request_json, intptr_t request_json_length, uint8_t** response_json, intptr_t* response_json_length, char** error); /* * ======== * Event Streams * ======== */ /** * A callback invoked when the VM service gets a request to listen to * some stream. * * \return Returns true iff the embedder supports the named stream id. */ typedef bool (*Dart_ServiceStreamListenCallback)(const char* stream_id); /** * A callback invoked when the VM service gets a request to cancel * some stream. */ typedef void (*Dart_ServiceStreamCancelCallback)(const char* stream_id); /** * Adds VM service stream callbacks. * * \param listen_callback A function pointer to a listen callback function. * A listen callback function should not be already set when this function * is called. A NULL value removes the existing listen callback function * if any. * * \param cancel_callback A function pointer to a cancel callback function. * A cancel callback function should not be already set when this function * is called. A NULL value removes the existing cancel callback function * if any. * * \return Success if the callbacks were added. Otherwise, returns an * error handle. */ DART_EXPORT char* Dart_SetServiceStreamCallbacks( Dart_ServiceStreamListenCallback listen_callback, Dart_ServiceStreamCancelCallback cancel_callback); /** * Sends a data event to clients of the VM Service. * * A data event is used to pass an array of bytes to subscribed VM * Service clients. For example, in the standalone embedder, this is * function used to provide WriteEvents on the Stdout and Stderr * streams. * * If the embedder passes in a stream id for which no client is * subscribed, then the event is ignored. * * \param stream_id The id of the stream on which to post the event. * * \param event_kind A string identifying what kind of event this is. * For example, 'WriteEvent'. * * \param bytes A pointer to an array of bytes. * * \param bytes_length The length of the byte array. * * \return NULL if the arguments are well formed. Otherwise, returns an * error string. The caller is responsible for freeing the error message. */ DART_EXPORT char* Dart_ServiceSendDataEvent(const char* stream_id, const char* event_kind, const uint8_t* bytes, intptr_t bytes_length); /* * ======== * Reload support * ======== * * These functions are used to implement reloading in the Dart VM. * This is an experimental feature, so embedders should be prepared * for these functions to change. */ /** * A callback which determines whether the file at some url has been * modified since some time. If the file cannot be found, true should * be returned. */ typedef bool (*Dart_FileModifiedCallback)(const char* url, int64_t since); DART_EXPORT char* Dart_SetFileModifiedCallback( Dart_FileModifiedCallback file_modified_callback); /** * Returns true if isolate is currently reloading. */ DART_EXPORT bool Dart_IsReloading(); /* * ======== * Timeline * ======== */ /** * Enable tracking of specified timeline category. This is operational * only when systrace timeline functionality is turned on. * * \param categories A comma separated list of categories that need to * be enabled, the categories are * "all" : All categories * "API" - Execution of Dart C API functions * "Compiler" - Execution of Dart JIT compiler * "CompilerVerbose" - More detailed Execution of Dart JIT compiler * "Dart" - Execution of Dart code * "Debugger" - Execution of Dart debugger * "Embedder" - Execution of Dart embedder code * "GC" - Execution of Dart Garbage Collector * "Isolate" - Dart Isolate lifecycle execution * "VM" - Execution in Dart VM runtime code * "" - None * * When "all" is specified all the categories are enabled. * When a comma separated list of categories is specified, the categories * that are specified will be enabled and the rest will be disabled. * When "" is specified all the categories are disabled. * The category names are case sensitive. * eg: Dart_EnableTimelineCategory("all"); * Dart_EnableTimelineCategory("GC,API,Isolate"); * Dart_EnableTimelineCategory("GC,Debugger,Dart"); * * \return True if the categories were successfully enabled, False otherwise. */ DART_EXPORT bool Dart_SetEnabledTimelineCategory(const char* categories); /** * Returns a timestamp in microseconds. This timestamp is suitable for * passing into the timeline system, and uses the same monotonic clock * as dart:developer's Timeline.now. * * \return A timestamp that can be passed to the timeline system. */ DART_EXPORT int64_t Dart_TimelineGetMicros(); /** * Returns a raw timestamp in from the monotonic clock. * * \return A raw timestamp from the monotonic clock. */ DART_EXPORT int64_t Dart_TimelineGetTicks(); /** * Returns the frequency of the monotonic clock. * * \return The frequency of the monotonic clock. */ DART_EXPORT int64_t Dart_TimelineGetTicksFrequency(); typedef enum { Dart_Timeline_Event_Begin, // Phase = 'B'. Dart_Timeline_Event_End, // Phase = 'E'. Dart_Timeline_Event_Instant, // Phase = 'i'. Dart_Timeline_Event_Duration, // Phase = 'X'. Dart_Timeline_Event_Async_Begin, // Phase = 'b'. Dart_Timeline_Event_Async_End, // Phase = 'e'. Dart_Timeline_Event_Async_Instant, // Phase = 'n'. Dart_Timeline_Event_Counter, // Phase = 'C'. Dart_Timeline_Event_Flow_Begin, // Phase = 's'. Dart_Timeline_Event_Flow_Step, // Phase = 't'. Dart_Timeline_Event_Flow_End, // Phase = 'f'. } Dart_Timeline_Event_Type; /** * Add a timeline event to the embedder stream. * * DEPRECATED: this function will be removed in Dart SDK v3.2. * * \param label The name of the event. Its lifetime must extend at least until * Dart_Cleanup. * \param timestamp0 The first timestamp of the event. * \param timestamp1_or_id When reporting an event of type * |Dart_Timeline_Event_Duration|, the second (end) timestamp of the event * should be passed through |timestamp1_or_id|. When reporting an event of * type |Dart_Timeline_Event_Async_Begin|, |Dart_Timeline_Event_Async_End|, * or |Dart_Timeline_Event_Async_Instant|, the async ID associated with the * event should be passed through |timestamp1_or_id|. When reporting an * event of type |Dart_Timeline_Event_Flow_Begin|, * |Dart_Timeline_Event_Flow_Step|, or |Dart_Timeline_Event_Flow_End|, the * flow ID associated with the event should be passed through * |timestamp1_or_id|. When reporting an event of type * |Dart_Timeline_Event_Begin| or |Dart_Timeline_Event_End|, the event ID * associated with the event should be passed through |timestamp1_or_id|. * Note that this event ID will only be used by the MacOS recorder. The * argument to |timestamp1_or_id| will not be used when reporting events of * other types. * \param argument_count The number of argument names and values. * \param argument_names An array of names of the arguments. The lifetime of the * names must extend at least until Dart_Cleanup. The array may be reclaimed * when this call returns. * \param argument_values An array of values of the arguments. The values and * the array may be reclaimed when this call returns. */ DART_EXPORT void Dart_TimelineEvent(const char* label, int64_t timestamp0, int64_t timestamp1_or_id, Dart_Timeline_Event_Type type, intptr_t argument_count, const char** argument_names, const char** argument_values); /** * Add a timeline event to the embedder stream. * * Note regarding flow events: events must be associated with flow IDs in two * different ways to allow flow events to be serialized correctly in both * Chrome's JSON trace event format and Perfetto's proto trace format. Events * of type |Dart_Timeline_Event_Flow_Begin|, |Dart_Timeline_Event_Flow_Step|, * and |Dart_Timeline_Event_Flow_End| must be reported to support serialization * in Chrome's trace format. The |flow_ids| argument must be supplied when * reporting events of type |Dart_Timeline_Event_Begin|, * |Dart_Timeline_Event_Duration|, |Dart_Timeline_Event_Instant|, * |Dart_Timeline_Event_Async_Begin|, and |Dart_Timeline_Event_Async_Instant| to * support serialization in Perfetto's proto format. * * \param label The name of the event. Its lifetime must extend at least until * Dart_Cleanup. * \param timestamp0 The first timestamp of the event. * \param timestamp1_or_id When reporting an event of type * |Dart_Timeline_Event_Duration|, the second (end) timestamp of the event * should be passed through |timestamp1_or_id|. When reporting an event of * type |Dart_Timeline_Event_Async_Begin|, |Dart_Timeline_Event_Async_End|, * or |Dart_Timeline_Event_Async_Instant|, the async ID associated with the * event should be passed through |timestamp1_or_id|. When reporting an * event of type |Dart_Timeline_Event_Flow_Begin|, * |Dart_Timeline_Event_Flow_Step|, or |Dart_Timeline_Event_Flow_End|, the * flow ID associated with the event should be passed through * |timestamp1_or_id|. When reporting an event of type * |Dart_Timeline_Event_Begin| or |Dart_Timeline_Event_End|, the event ID * associated with the event should be passed through |timestamp1_or_id|. * Note that this event ID will only be used by the MacOS recorder. The * argument to |timestamp1_or_id| will not be used when reporting events of * other types. * \param flow_id_count The number of flow IDs associated with this event. * \param flow_ids An array of flow IDs associated with this event. The array * may be reclaimed when this call returns. * \param argument_count The number of argument names and values. * \param argument_names An array of names of the arguments. The lifetime of the * names must extend at least until Dart_Cleanup. The array may be reclaimed * when this call returns. * \param argument_values An array of values of the arguments. The values and * the array may be reclaimed when this call returns. */ DART_EXPORT void Dart_RecordTimelineEvent(const char* label, int64_t timestamp0, int64_t timestamp1_or_id, intptr_t flow_id_count, const int64_t* flow_ids, Dart_Timeline_Event_Type type, intptr_t argument_count, const char** argument_names, const char** argument_values); /** * Associates a name with the current thread. This name will be used to name * threads in the timeline. Can only be called after a call to Dart_Initialize. * * \param name The name of the thread. */ DART_EXPORT void Dart_SetThreadName(const char* name); typedef struct { const char* name; const char* value; } Dart_TimelineRecorderEvent_Argument; #define DART_TIMELINE_RECORDER_CURRENT_VERSION (0x00000002) typedef struct { /* Set to DART_TIMELINE_RECORDER_CURRENT_VERSION */ int32_t version; /* The event's type / phase. */ Dart_Timeline_Event_Type type; /* The event's timestamp according to the same clock as * Dart_TimelineGetMicros. For a duration event, this is the beginning time. */ int64_t timestamp0; /** * For a duration event, this is the end time. For an async event, this is the * async ID. For a flow event, this is the flow ID. For a begin or end event, * this is the event ID (which is only referenced by the MacOS recorder). */ int64_t timestamp1_or_id; /* The current isolate of the event, as if by Dart_GetMainPortId, or * ILLEGAL_PORT if the event had no current isolate. */ Dart_Port isolate; /* The current isolate group of the event, as if by * Dart_CurrentIsolateGroupId, or ILLEGAL_PORT if the event had no current * isolate group. */ Dart_IsolateGroupId isolate_group; /* The callback data associated with the isolate if any. */ void* isolate_data; /* The callback data associated with the isolate group if any. */ void* isolate_group_data; /* The name / label of the event. */ const char* label; /* The stream / category of the event. */ const char* stream; intptr_t argument_count; Dart_TimelineRecorderEvent_Argument* arguments; } Dart_TimelineRecorderEvent; /** * Callback provided by the embedder to handle the completion of timeline * events. * * \param event A timeline event that has just been completed. The VM keeps * ownership of the event and any field in it (i.e., the embedder should copy * any values it needs after the callback returns). */ typedef void (*Dart_TimelineRecorderCallback)( Dart_TimelineRecorderEvent* event); /** * Register a `Dart_TimelineRecorderCallback` to be called as timeline events * are completed. * * The callback will be invoked without a current isolate. * * The callback will be invoked on the thread completing the event. Because * `Dart_TimelineEvent` may be called by any thread, the callback may be called * on any thread. * * The callback may be invoked at any time after `Dart_Initialize` is called and * before `Dart_Cleanup` returns. * * If multiple callbacks are registered, only the last callback registered * will be remembered. Providing a NULL callback will clear the registration * (i.e., a NULL callback produced a no-op instead of a crash). * * Setting a callback is insufficient to receive events through the callback. The * VM flag `timeline_recorder` must also be set to `callback`. */ DART_EXPORT void Dart_SetTimelineRecorderCallback( Dart_TimelineRecorderCallback callback); /* * ======= * Metrics * ======= */ /** * Return metrics gathered for the VM and individual isolates. */ DART_EXPORT int64_t Dart_IsolateGroupHeapOldUsedMetric(Dart_IsolateGroup group); // Byte DART_EXPORT int64_t Dart_IsolateGroupHeapOldCapacityMetric(Dart_IsolateGroup group); // Byte DART_EXPORT int64_t Dart_IsolateGroupHeapOldExternalMetric(Dart_IsolateGroup group); // Byte DART_EXPORT int64_t Dart_IsolateGroupHeapNewUsedMetric(Dart_IsolateGroup group); // Byte DART_EXPORT int64_t Dart_IsolateGroupHeapNewCapacityMetric(Dart_IsolateGroup group); // Byte DART_EXPORT int64_t Dart_IsolateGroupHeapNewExternalMetric(Dart_IsolateGroup group); // Byte /* * ======== * UserTags * ======== */ /* * Gets the current isolate's currently set UserTag instance. * * \return The currently set UserTag instance. */ DART_EXPORT Dart_Handle Dart_GetCurrentUserTag(); /* * Gets the current isolate's default UserTag instance. * * \return The default UserTag with label 'Default' */ DART_EXPORT Dart_Handle Dart_GetDefaultUserTag(); /* * Creates a new UserTag instance. * * \param label The name of the new UserTag. * * \return The newly created UserTag instance or an error handle. */ DART_EXPORT Dart_Handle Dart_NewUserTag(const char* label); /* * Updates the current isolate's UserTag to a new value. * * \param user_tag The UserTag to be set as the current UserTag. * * \return The previously set UserTag instance or an error handle. */ DART_EXPORT Dart_Handle Dart_SetCurrentUserTag(Dart_Handle user_tag); /* * Returns the label of a given UserTag instance. * * \param user_tag The UserTag from which the label will be retrieved. * * \return The UserTag's label. NULL if the user_tag is invalid. The caller is * responsible for freeing the returned label. */ DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_GetUserTagLabel( Dart_Handle user_tag); /* * ======= * Heap Snapshot * ======= */ /** * Callback provided by the caller of `Dart_WriteHeapSnapshot` which is * used to write out chunks of the requested heap snapshot. * * \param context An opaque context which was passed to `Dart_WriteHeapSnapshot` * together with this callback. * * \param buffer Pointer to the buffer containing a chunk of the snapshot. * The callback owns the buffer and needs to `free` it. * * \param size Number of bytes in the `buffer` to be written. * * \param is_last Set to `true` for the last chunk. The callback will not * be invoked again after it was invoked once with `is_last` set to `true`. */ typedef void (*Dart_HeapSnapshotWriteChunkCallback)(void* context, uint8_t* buffer, intptr_t size, bool is_last); /** * Generate heap snapshot of the current isolate group and stream it into the * given `callback`. VM would produce snapshot in chunks and send these chunks * one by one back to the embedder by invoking the provided `callback`. * * This API enables embedder to stream snapshot into a file or socket without * allocating a buffer to hold the whole snapshot in memory. * * The isolate group will be paused for the duration of this operation. * * \param write Callback used to write chunks of the heap snapshot. * * \param context Opaque context which would be passed on each invocation of * `write` callback. * * \returns `nullptr` if the operation is successful otherwise error message. * Caller owns error message string and needs to `free` it. */ DART_EXPORT char* Dart_WriteHeapSnapshot( Dart_HeapSnapshotWriteChunkCallback write, void* context); #endif // RUNTIME_INCLUDE_DART_TOOLS_API_H_ ================================================ FILE: core/dart-bridge/include/dart_version.h ================================================ /* * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file * for details. All rights reserved. Use of this source code is governed by a * BSD-style license that can be found in the LICENSE file. */ #ifndef RUNTIME_INCLUDE_DART_VERSION_H_ #define RUNTIME_INCLUDE_DART_VERSION_H_ // On breaking changes the major version is increased. // On backwards compatible changes the minor version is increased. // The versioning covers the symbols exposed in dart_api_dl.h #define DART_API_DL_MAJOR_VERSION 2 #define DART_API_DL_MINOR_VERSION 3 #endif /* RUNTIME_INCLUDE_DART_VERSION_H_ */ /* NOLINT */ ================================================ FILE: core/dart-bridge/include/internal/dart_api_dl_impl.h ================================================ /* * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file * for details. All rights reserved. Use of this source code is governed by a * BSD-style license that can be found in the LICENSE file. */ #ifndef RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ #define RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ typedef struct { const char* name; void (*function)(void); } DartApiEntry; typedef struct { const int major; const int minor; const DartApiEntry* const functions; } DartApi; #endif /* RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ */ /* NOLINT */ ================================================ FILE: core/dart-bridge/lib.go ================================================ //go:build cgo package dart_bridge /* #include #include "stdint.h" #include "include/dart_api_dl.h" #include "include/dart_api_dl.c" #include "include/dart_native_api.h" bool GoDart_PostCObject(Dart_Port_DL port, Dart_CObject* obj) { return Dart_PostCObject_DL(port, obj); } */ import "C" import ( "fmt" "unsafe" ) func InitDartApi(api unsafe.Pointer) { if C.Dart_InitializeApiDL(api) != 0 { panic("failed to create dart bridge") } else { fmt.Println("Dart Api DL is initialized") } } func SendToPort(port int64, msg string) bool { var obj C.Dart_CObject obj._type = C.Dart_CObject_kString msgString := C.CString(msg) defer C.free(unsafe.Pointer(msgString)) ptr := unsafe.Pointer(&obj.value[0]) *(**C.char)(ptr) = msgString isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj) if !isSuccess { return false } return true } ================================================ FILE: core/dart-bridge/lib_common.go ================================================ //go:build !cgo package dart_bridge func SendToPort(port int64, msg string) bool { return false } ================================================ FILE: core/go.mod ================================================ module core go 1.20 replace github.com/metacubex/mihomo => ./Clash.Meta require ( github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000 golang.org/x/sync v0.11.0 ) require ( github.com/RyuaNerin/go-krypto v1.3.0 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dunglas/httpsfv v1.0.2 // indirect github.com/enfein/mieru/v3 v3.31.0 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gaukas/godicttls v0.0.4 // 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/gobwas/ws v1.4.0 // indirect github.com/gofrs/uuid/v5 v5.4.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect github.com/josharian/native v1.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/reedsolomon v1.12.3 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d // indirect github.com/metacubex/ascon v0.1.0 // indirect github.com/metacubex/bart v0.26.0 // indirect github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b // indirect github.com/metacubex/blake3 v0.1.0 // indirect github.com/metacubex/chacha v0.1.5 // indirect github.com/metacubex/chi v0.1.0 // indirect github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a // indirect github.com/metacubex/cpu v0.1.1 // indirect github.com/metacubex/edwards25519 v1.2.0 // indirect github.com/metacubex/fswatch v0.1.1 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect github.com/metacubex/hkdf v0.1.0 // indirect github.com/metacubex/hpke v0.1.0 // indirect github.com/metacubex/http v0.1.6 // indirect github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 // indirect github.com/metacubex/mhurl v0.1.0 // indirect github.com/metacubex/mlkem v0.1.0 // indirect github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb // indirect github.com/metacubex/qpack v0.6.0 // indirect github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306 // indirect github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/restls-client-go v0.1.7 // indirect github.com/metacubex/sing v0.5.7 // indirect github.com/metacubex/sing-mux v0.3.9 // indirect github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a // indirect github.com/metacubex/sing-shadowsocks v0.2.12 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.7 // indirect github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect github.com/metacubex/sing-tun v0.4.18 // indirect github.com/metacubex/sing-vmess v0.2.5 // indirect github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947 // indirect github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 // indirect github.com/metacubex/ssh v0.1.0 // indirect github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 // indirect github.com/metacubex/tls v0.1.5 // indirect github.com/metacubex/utls v1.8.4 // indirect github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f // indirect github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect github.com/miekg/dns v1.1.63 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/openacid/low v0.1.21 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/samber/lo v1.53.0 // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: core/go.sum ================================================ github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg= github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/enfein/mieru/v3 v3.31.0 h1:Fl2ocRCRXJzMygzdRjBHgqI996ZuIDHUmyQyovSf9sA= github.com/enfein/mieru/v3 v3.31.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= 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/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 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/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc= github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 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/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d h1:vAJ0ZT4aO803F1uw2roIA9yH7Sxzox34tVVyye1bz6c= github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY= github.com/metacubex/ascon v0.1.0 h1:6ZWxmXYszT1XXtwkf6nxfFhc/OTtQ9R3Vyj1jN32lGM= github.com/metacubex/ascon v0.1.0/go.mod h1:eV5oim4cVPPdEL8/EYaTZ0iIKARH9pnhAK/fcT5Kacc= github.com/metacubex/bart v0.26.0 h1:d/bBTvVatfVWGfQbiDpYKI1bXUJgjaabB2KpK1Tnk6w= github.com/metacubex/bart v0.26.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b h1:j7dadXD8I2KTmMt8jg1JcaP1ANL3JEObJPdANKcSYPY= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b/go.mod h1:+WmP0VJZDkDszvpa83HzfUp6QzARl/IKkMorH4+nODw= github.com/metacubex/blake3 v0.1.0 h1:KGnjh/56REO7U+cgZA8dnBhxdP7jByrG7hTP+bu6cqY= github.com/metacubex/blake3 v0.1.0/go.mod h1:CCkLdzFrqf7xmxCdhQFvJsRRV2mwOLDoSPg6vUTB9Uk= github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M= github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chi v0.1.0 h1:rjNDyDj50nRpicG43CNkIw4ssiCbmDL8d7wJXKlUCsg= github.com/metacubex/chi v0.1.0/go.mod h1:zM5u5oMQt8b2DjvDHvzadKrP6B2ztmasL1YHRMbVV+g= github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a h1:Ph5UfTWDsGruZ+v95Df1ycTflQFmpZBFg2LUvj2kx/M= github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ= github.com/metacubex/cpu v0.1.1 h1:rRV5HGmeuGzjiKI3hYbL0dCd0qGwM7VUtk4ICXD06mI= github.com/metacubex/cpu v0.1.1/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU= github.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4= github.com/metacubex/edwards25519 v1.2.0/go.mod h1:NCQF3J/Ki7382FJuokwsywEIIEI/gro/3smyXgQJsx0= github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU= github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ= github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA= github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4= github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ= github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U= github.com/metacubex/http v0.1.6 h1:xvXuvXMCMxCWMF5nEJF4yiKvXL+p2atWMzs37e80m1I= github.com/metacubex/http v0.1.6/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc= github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE= github.com/metacubex/mhurl v0.1.0/go.mod h1:2qpQImCbXoUs6GwJrjuEXKelPyoimsIXr07eNKZdS00= github.com/metacubex/mlkem v0.1.0 h1:wFClitonSFcmipzzQvax75beLQU+D7JuC+VK1RzSL8I= github.com/metacubex/mlkem v0.1.0/go.mod h1:amhaXZVeYNShuy9BILcR7P0gbeo/QLZsnqCdL8U2PDQ= github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb h1:wk6mHYPURSUvWcUv72gNP79oiylFsscBSDPJ6ieV6Iw= github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb/go.mod h1:73ZrCfhdkW4F2E2GAlta3km/S2RHhFNogCMtWZV2anQ= github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw= github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA= github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306 h1:HlGLmLsWJMLSu0CMI9z/BmEnithB4oXM5Rom6/0Qxtg= github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM= github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing-mux v0.3.9 h1:/aoBD2+sK2qsXDlNDe3hkR0GZuFDtwIZhOeGUx9W0Yk= github.com/metacubex/sing-mux v0.3.9/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc= github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a h1:977o0ZYYbiQAGuOxql7Q6UN3rEy59OyAE0tELq4gZfI= github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A= github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= github.com/metacubex/sing-tun v0.4.18 h1:WRzAosG0YkT3aZq5RJWtF+RdCgeJ8EpooS5ZM1lkXo0= github.com/metacubex/sing-tun v0.4.18/go.mod h1:g4I/JNplDBhXLF+aQWgFbhNeJPSXQOWS9HvLeNvkgeA= github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE= github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q= github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947 h1:IB03BvRQtvjWScyOK5jSQVJYY8osmZXHL+4VCEFMWcM= github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80= github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk= github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg= github.com/metacubex/ssh v0.1.0 h1:iGfr99qk/eMHzUnQ/0bTxXT8+8SWqLSHBWDHoAhngzw= github.com/metacubex/ssh v0.1.0/go.mod h1:NUtl0d+/f2cG9ECEpMM8iCVOpmggQlC13oLeDUONDlU= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc= github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM= github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4= github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E= github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49/go.mod h1:MBeEa9IVBphH7vc3LNtW6ZujVXFizotPo3OEiHQ+TNU= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I= github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU= github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo= github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/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/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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.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= ================================================ FILE: core/hub.go ================================================ package main import ( "context" "core/state" "encoding/json" "fmt" "net" "runtime" "sort" "strconv" "time" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/observable" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" cp "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel/statistic" ) var ( isInit = false externalProviders = map[string]cp.Provider{} logSubscriber observable.Subscription[log.Event] ) func handleInitClash(paramsString string) bool { var params = InitParams{} err := json.Unmarshal([]byte(paramsString), ¶ms) if err != nil { return false } version = params.Version if !isInit { constant.SetHomeDir(params.HomeDir) isInit = true } return isInit } func handleStartListener() bool { runLock.Lock() defer runLock.Unlock() isRunning = true updateListeners() resolver.ResetConnection() return true } func handleStopListener() bool { runLock.Lock() defer runLock.Unlock() isRunning = false listener.StopListener() return true } func handleGetIsInit() bool { return isInit } func handleForceGc() { go func() { log.Infoln("[APP] request force GC") runtime.GC() }() } func handleShutdown() bool { stopListeners() executor.Shutdown() runtime.GC() isInit = false return true } func handleValidateConfig(bytes []byte) string { _, err := config.UnmarshalRawConfig(bytes) if err != nil { return err.Error() } return "" } func handleGetProxies() map[string]constant.Proxy { runLock.Lock() defer runLock.Unlock() return tunnel.ProxiesWithProviders() } func handleChangeProxy(data string, fn func(string string)) { runLock.Lock() go func() { defer runLock.Unlock() var params = &ChangeProxyParams{} err := json.Unmarshal([]byte(data), params) if err != nil { fn(err.Error()) return } groupName := *params.GroupName proxyName := *params.ProxyName proxies := tunnel.ProxiesWithProviders() group, ok := proxies[groupName] if !ok { fn("Not found group") return } adapterProxy := group.(*adapter.Proxy) selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble) if !ok { fn("Group is not selectable") return } if proxyName == "" { selector.ForceSet(proxyName) } else { err = selector.Set(proxyName) } if err != nil { fn(err.Error()) return } fn("") return }() } func handleGetTraffic() string { up, down := statistic.DefaultManager.NowTraffic(state.CurrentState.OnlyStatisticsProxy) traffic := map[string]int64{ "up": up, "down": down, } data, err := json.Marshal(traffic) if err != nil { fmt.Println("Error:", err) return "" } return string(data) } func handleGetTotalTraffic() string { up, down := statistic.DefaultManager.TotalTraffic(state.CurrentState.OnlyStatisticsProxy) traffic := map[string]int64{ "up": up, "down": down, } data, err := json.Marshal(traffic) if err != nil { fmt.Println("Error:", err) return "" } return string(data) } func handleResetTraffic() { statistic.DefaultManager.ResetStatistic() } func handleAsyncTestDelay(paramsString string, fn func(string)) { mBatch.Go(paramsString, func() (bool, error) { var params = &TestDelayParams{} err := json.Unmarshal([]byte(paramsString), params) if err != nil { fn("") return false, nil } expectedStatus, err := utils.NewUnsignedRanges[uint16]("") if err != nil { fn("") return false, nil } ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout)) defer cancel() proxies := tunnel.ProxiesWithProviders() proxy := proxies[params.ProxyName] delayData := &Delay{ Name: params.ProxyName, } if proxy == nil { delayData.Value = -1 data, _ := json.Marshal(delayData) fn(string(data)) return false, nil } testUrl := constant.DefaultTestURL if params.TestUrl != "" { testUrl = params.TestUrl } delayData.Url = testUrl delay, err := proxy.URLTest(ctx, testUrl, expectedStatus) if err != nil || delay == 0 { delayData.Value = -1 data, _ := json.Marshal(delayData) fn(string(data)) return false, nil } delayData.Value = int32(delay) data, _ := json.Marshal(delayData) fn(string(data)) return false, nil }) } func handleGetConnections() string { runLock.Lock() defer runLock.Unlock() snapshot := statistic.DefaultManager.Snapshot() data, err := json.Marshal(snapshot) if err != nil { fmt.Println("Error:", err) return "" } return string(data) } func handleCloseConnections() bool { runLock.Lock() defer runLock.Unlock() closeConnections() return true } func closeConnections() { statistic.DefaultManager.Range(func(c statistic.Tracker) bool { err := c.Close() if err != nil { return false } return true }) } func handleResetConnections() bool { runLock.Lock() defer runLock.Unlock() resolver.ResetConnection() return true } func handleCloseConnection(connectionId string) bool { runLock.Lock() defer runLock.Unlock() c := statistic.DefaultManager.Get(connectionId) if c == nil { return false } _ = c.Close() return true } func handleGetExternalProviders() string { runLock.Lock() defer runLock.Unlock() externalProviders = getExternalProvidersRaw() eps := make([]ExternalProvider, 0) for _, p := range externalProviders { externalProvider, err := toExternalProvider(p) if err != nil { continue } eps = append(eps, *externalProvider) } sort.Sort(ExternalProviders(eps)) data, err := json.Marshal(eps) if err != nil { return "" } return string(data) } func handleGetExternalProvider(externalProviderName string) string { runLock.Lock() defer runLock.Unlock() externalProvider, exist := externalProviders[externalProviderName] if !exist { return "" } e, err := toExternalProvider(externalProvider) if err != nil { return "" } data, err := json.Marshal(e) if err != nil { return "" } return string(data) } func handleUpdateGeoData(geoType string, geoName string, fn func(value string)) { go func() { path := constant.Path.Resolve(geoName) switch geoType { case "MMDB": err := updater.UpdateMMDBWithPath(path) if err != nil { fn(err.Error()) return } case "ASN": err := updater.UpdateASNWithPath(path) if err != nil { fn(err.Error()) return } case "GeoIp": err := updater.UpdateGeoIpWithPath(path) if err != nil { fn(err.Error()) return } case "GeoSite": err := updater.UpdateGeoSiteWithPath(path) if err != nil { fn(err.Error()) return } } fn("") }() } func handleUpdateExternalProvider(providerName string, fn func(value string)) { go func() { externalProvider, exist := externalProviders[providerName] if !exist { fn("external provider is not exist") return } err := externalProvider.Update() if err != nil { fn(err.Error()) return } fn("") }() } func handleSideLoadExternalProvider(providerName string, data []byte, fn func(value string)) { go func() { runLock.Lock() defer runLock.Unlock() externalProvider, exist := externalProviders[providerName] if !exist { fn("external provider is not exist") return } err := sideUpdateExternalProvider(externalProvider, data) if err != nil { fn(err.Error()) return } fn("") }() } func handleStartLog() { if logSubscriber != nil { log.UnSubscribe(logSubscriber) logSubscriber = nil } logSubscriber = log.Subscribe() go func() { for logData := range logSubscriber { if logData.LogLevel < log.Level() { continue } message := &Message{ Type: LogMessage, Data: logData, } sendMessage(*message) } }() } func handleStopLog() { if logSubscriber != nil { log.UnSubscribe(logSubscriber) logSubscriber = nil } } func handleGetCountryCode(ip string, fn func(value string)) { go func() { runLock.Lock() defer runLock.Unlock() codes := mmdb.IPInstance().LookupCode(net.ParseIP(ip)) if len(codes) == 0 { fn("") return } fn(codes[0]) }() } func handleGetMemory(fn func(value string)) { go func() { fn(strconv.FormatUint(statistic.DefaultManager.Memory(), 10)) }() } func handleSetState(params string) { _ = json.Unmarshal([]byte(params), state.CurrentState) } func handleGetConfig(path string) (*config.RawConfig, error) { bytes, err := readFile(path) if err != nil { return nil, err } prof, err := config.UnmarshalRawConfig(bytes) if err != nil { return nil, err } return prof, nil } func handleFlushFakeIP() bool { err := resolver.FlushFakeIP() if err != nil { log.Errorln("[APP] Flush FakeIP error: %v", err) return false } log.Infoln("[APP] FakeIP pool flushed") return true } func handleFlushDnsCache() { resolver.ClearCache() log.Infoln("[APP] DNS cache flushed") } func handleCrash() { panic("handle invoke crash") } func handleUpdateConfig(bytes []byte) string { var params = &UpdateParams{} err := json.Unmarshal(bytes, params) if err != nil { return err.Error() } updateConfig(params) return "" } func handleSetupConfig(bytes []byte) string { var params = defaultSetupParams() err := UnmarshalJson(bytes, params) if err != nil { log.Errorln("unmarshalRawConfig error %v", err) _ = setupConfig(defaultSetupParams()) return err.Error() } err = setupConfig(params) if err != nil { return err.Error() } return "" } func handleSuspend(suspended bool) bool { if suspended { log.Infoln("[APP] Suspend mode enabled") tunnel.OnSuspend() } else { log.Infoln("[APP] Resume from suspend") tunnel.OnRunning() } return true } func init() { adapter.UrlTestHook = func(url string, name string, delay uint16) { delayData := &Delay{ Url: url, Name: name, } if delay == 0 { delayData.Value = -1 } else { delayData.Value = int32(delay) } sendMessage(Message{ Type: DelayMessage, Data: delayData, }) } statistic.DefaultRequestNotify = func(c statistic.Tracker) { sendMessage(Message{ Type: RequestMessage, Data: c.Info(), }) } executor.DefaultProviderLoadedHook = func(providerName string) { sendMessage(Message{ Type: LoadedMessage, Data: providerName, }) } } ================================================ FILE: core/lib.go ================================================ //go:build cgo package main /* #include */ import "C" import ( bridge "core/dart-bridge" "encoding/json" "unsafe" ) var messagePort int64 = -1 //export initNativeApiBridge func initNativeApiBridge(api unsafe.Pointer) { bridge.InitDartApi(api) } //export attachMessagePort func attachMessagePort(mPort C.longlong) { messagePort = int64(mPort) } //export getTraffic func getTraffic() *C.char { return C.CString(handleGetTraffic()) } //export getTotalTraffic func getTotalTraffic() *C.char { return C.CString(handleGetTotalTraffic()) } //export freeCString func freeCString(s *C.char) { C.free(unsafe.Pointer(s)) } func (result ActionResult) send() { data, err := result.Json() if err != nil { return } bridge.SendToPort(result.Port, string(data)) } //export invokeAction func invokeAction(paramsChar *C.char, port C.longlong) { params := C.GoString(paramsChar) i := int64(port) var action = &Action{} err := json.Unmarshal([]byte(params), action) if err != nil { bridge.SendToPort(i, err.Error()) return } result := ActionResult{ Id: action.Id, Method: action.Method, Port: i, } go handleAction(action, result) } func sendMessage(message Message) { if messagePort == -1 { return } result := ActionResult{ Method: messageMethod, Port: messagePort, Data: message, } result.send() } //export getConfig func getConfig(s *C.char) *C.char { path := C.GoString(s) config, err := handleGetConfig(path) if err != nil { return C.CString("") } marshal, err := json.Marshal(config) if err != nil { return C.CString("") } return C.CString(string(marshal)) } //export startListener func startListener() { handleStartListener() } //export stopListener func stopListener() { handleStopListener() } //export suspend func suspend(suspended C.int) { handleSuspend(suspended != 0) } ================================================ FILE: core/lib_android.go ================================================ //go:build android && cgo package main import "C" import ( "context" bridge "core/dart-bridge" "core/platform" "core/state" t "core/tun" "encoding/json" "errors" "fmt" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/listener/sing_tun" "github.com/metacubex/mihomo/log" "golang.org/x/sync/semaphore" "net" "strconv" "strings" "sync" "syscall" "time" "unsafe" ) type TunHandler struct { listener *sing_tun.Listener callback unsafe.Pointer limit *semaphore.Weighted } func (t *TunHandler) close() { _ = t.limit.Acquire(context.TODO(), 4) defer t.limit.Release(4) removeTunHook() if t.listener != nil { _ = t.listener.Close() } if t.callback != nil { releaseObject(t.callback) } t.callback = nil t.listener = nil } func (t *TunHandler) handleProtect(fd int) { _ = t.limit.Acquire(context.Background(), 1) defer t.limit.Release(1) if t.listener == nil { return } Protect(t.callback, fd) } func (t *TunHandler) handleResolveProcess(source, target net.Addr) string { _ = t.limit.Acquire(context.Background(), 1) defer t.limit.Release(1) if t.listener == nil { return "" } var protocol int uid := -1 switch source.Network() { case "udp", "udp4", "udp6": protocol = syscall.IPPROTO_UDP case "tcp", "tcp4", "tcp6": protocol = syscall.IPPROTO_TCP } if version < 29 { uid = platform.QuerySocketUidFromProcFs(source, target) } return ResolveProcess(t.callback, protocol, source.String(), target.String(), uid) } var ( tunLock sync.Mutex runTime *time.Time errBlocked = errors.New("blocked") tunHandler *TunHandler ) func handleStopTun() { tunLock.Lock() defer tunLock.Unlock() runTime = nil if tunHandler != nil { tunHandler.close() } } func handleStartTun(fd int, callback unsafe.Pointer) { handleStopTun() tunLock.Lock() defer tunLock.Unlock() now := time.Now() runTime = &now if fd != 0 { tunHandler = &TunHandler{ callback: callback, limit: semaphore.NewWeighted(4), } initTunHook() tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack, currentConfig.General.Tun.DisableICMPForwarding, uint32(currentConfig.General.Tun.MTU), currentConfig.General.IPv6) if tunListener != nil { log.Infoln("TUN address: %v", tunListener.Address()) tunHandler.listener = tunListener } else { removeTunHook() } } } func handleGetRunTime() string { if runTime == nil { return "" } return strconv.FormatInt(runTime.UnixMilli(), 10) } func initTunHook() { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { if platform.ShouldBlockConnection() { return errBlocked } return conn.Control(func(fd uintptr) { tunHandler.handleProtect(int(fd)) }) } process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) { src, dst := metadata.RawSrcAddr, metadata.RawDstAddr if src == nil || dst == nil { return "", process.ErrInvalidNetwork } return tunHandler.handleResolveProcess(src, dst), nil } } func removeTunHook() { dialer.DefaultSocketHook = nil process.DefaultPackageNameResolver = nil } func handleGetAndroidVpnOptions() string { tunLock.Lock() defer tunLock.Unlock() ipv6Address := "" if currentConfig.General.IPv6 { ipv6Address = state.DefaultIpv6Address } options := state.AndroidVpnOptions{ Enable: state.CurrentState.VpnProps.Enable, Port: currentConfig.General.MixedPort, Ipv4Address: state.DefaultIpv4Address, Ipv6Address: ipv6Address, AccessControl: state.CurrentState.VpnProps.AccessControl, SystemProxy: state.CurrentState.VpnProps.SystemProxy, AllowBypass: state.CurrentState.VpnProps.AllowBypass, RouteAddress: currentConfig.General.Tun.RouteAddress, RouteMode: state.CurrentState.VpnProps.RouteMode, BypassDomain: state.CurrentState.BypassDomain, DnsServerAddress: state.GetDnsServerAddress(), DozeSuspend: state.CurrentState.VpnProps.DozeSuspend, DisableIcmpForwarding: currentConfig.General.Tun.DisableICMPForwarding, Mtu: uint32(currentConfig.General.Tun.MTU), } data, err := json.Marshal(options) if err != nil { fmt.Println("Error:", err) return "" } return string(data) } func handleUpdateDns(value string) { go func() { log.Infoln("[DNS] updateDns %s", value) dns.UpdateSystemDNS(strings.Split(value, ",")) dns.FlushCacheWithDefaultResolver() }() } func handleGetCurrentProfileName() string { if state.CurrentState == nil { return "" } return state.CurrentState.CurrentProfileName } func nextHandle(action *Action, result ActionResult) bool { switch action.Method { case getAndroidVpnOptionsMethod: result.success(handleGetAndroidVpnOptions()) return true case updateDnsMethod: data := action.Data.(string) handleUpdateDns(data) result.success(true) return true case getRunTimeMethod: result.success(handleGetRunTime()) return true case getCurrentProfileNameMethod: result.success(handleGetCurrentProfileName()) return true } return false } //export quickStart func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) { i := int64(port) paramsString := C.GoString(initParamsChar) bytes := []byte(C.GoString(paramsChar)) stateParams := C.GoString(stateParamsChar) go func() { res := handleInitClash(paramsString) if res == false { bridge.SendToPort(i, "init error") } handleSetState(stateParams) bridge.SendToPort(i, handleSetupConfig(bytes)) }() } //export startTUN func startTUN(fd C.int, callback unsafe.Pointer) bool { go func() { handleStartTun(int(fd), callback) }() return true } //export getRunTime func getRunTime() *C.char { return C.CString(handleGetRunTime()) } //export stopTun func stopTun() { go func() { handleStopTun() }() } //export getCurrentProfileName func getCurrentProfileName() *C.char { return C.CString(handleGetCurrentProfileName()) } //export getAndroidVpnOptions func getAndroidVpnOptions() *C.char { return C.CString(handleGetAndroidVpnOptions()) } //export setState func setState(s *C.char) { paramsString := C.GoString(s) handleSetState(paramsString) } //export updateDns func updateDns(s *C.char) { dnsList := C.GoString(s) handleUpdateDns(dnsList) } ================================================ FILE: core/lib_no_android.go ================================================ //go:build !android && cgo package main func nextHandle(action *Action, result ActionResult) bool { return false } ================================================ FILE: core/main.go ================================================ //go:build !cgo package main import ( "fmt" "os" ) func main() { args := os.Args if len(args) <= 1 { fmt.Println("Arguments error") os.Exit(1) } startServer(args[1]) } ================================================ FILE: core/main_cgo.go ================================================ //go:build cgo package main import "C" func main() { } ================================================ FILE: core/platform/limit.go ================================================ //go:build android && cgo package platform import "syscall" var nullFd int var maxFdCount int func init() { fd, err := syscall.Open("/dev/null", syscall.O_WRONLY, 0644) if err != nil { panic(err.Error()) } nullFd = fd var limit syscall.Rlimit if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil { maxFdCount = 1024 } else { maxFdCount = int(limit.Cur) } maxFdCount = maxFdCount / 4 * 3 } func ShouldBlockConnection() bool { fd, err := syscall.Dup(nullFd) if err != nil { return true } _ = syscall.Close(fd) if fd > maxFdCount { return true } return false } ================================================ FILE: core/platform/procfs.go ================================================ //go:build linux // +build linux package platform import ( "bufio" "encoding/binary" "encoding/hex" "fmt" "net" "os" "strconv" "strings" "unsafe" ) var netIndexOfLocal = -1 var netIndexOfUid = -1 var nativeEndian binary.ByteOrder func QuerySocketUidFromProcFs(source, _ net.Addr) int { if netIndexOfLocal < 0 || netIndexOfUid < 0 { return -1 } network := source.Network() if strings.HasSuffix(network, "4") || strings.HasSuffix(network, "6") { network = network[:len(network)-1] } path := "/proc/net/" + network var sIP net.IP var sPort int switch s := source.(type) { case *net.TCPAddr: sIP = s.IP sPort = s.Port case *net.UDPAddr: sIP = s.IP sPort = s.Port default: return -1 } sIP = sIP.To16() if sIP == nil { return -1 } uid := doQuery(path+"6", sIP, sPort) if uid == -1 { sIP = sIP.To4() if sIP == nil { return -1 } uid = doQuery(path, sIP, sPort) } return uid } func doQuery(path string, sIP net.IP, sPort int) int { file, err := os.Open(path) if err != nil { return -1 } defer func(file *os.File) { _ = file.Close() }(file) reader := bufio.NewReader(file) var bytes [2]byte binary.BigEndian.PutUint16(bytes[:], uint16(sPort)) local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:])) 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 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 func(file *os.File) { _ = file.Close() }(file) 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 } } } func init() { var x uint32 = 0x01020304 if *(*byte)(unsafe.Pointer(&x)) == 0x01 { nativeEndian = binary.BigEndian } else { nativeEndian = binary.LittleEndian } } ================================================ FILE: core/server.go ================================================ //go:build !cgo package main import ( "bufio" "encoding/json" "fmt" "net" "strconv" ) var conn net.Conn func (result ActionResult) send() { data, err := result.Json() if err != nil { return } send(data) } func sendMessage(message Message) { result := ActionResult{ Method: messageMethod, Data: message, } result.send() } func send(data []byte) { if conn == nil { return } _, _ = conn.Write(append(data, []byte("\n")...)) } func startServer(arg string) { _, err := strconv.Atoi(arg) if err != nil { conn, err = net.Dial("unix", arg) } else { conn, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", arg)) } if err != nil { panic(err.Error()) } defer func(conn net.Conn) { _ = conn.Close() }(conn) reader := bufio.NewReader(conn) for { data, err := reader.ReadString('\n') if err != nil { return } var action = &Action{} err = json.Unmarshal([]byte(data), action) if err != nil { return } result := ActionResult{ Id: action.Id, Method: action.Method, } go handleAction(action, result) } } func nextHandle(action *Action, result ActionResult) bool { return false } ================================================ FILE: core/state/state.go ================================================ package state import "net/netip" var DefaultIpv4Address = "198.51.100.1/30" var DefaultDnsAddress = "198.51.100.2" var DefaultIpv6Address = "fdfe:dcba:9876::1/126" type AndroidVpnOptions struct { Enable bool `json:"enable"` Port int `json:"port"` AccessControl *AccessControl `json:"accessControl"` AllowBypass bool `json:"allowBypass"` SystemProxy bool `json:"systemProxy"` BypassDomain []string `json:"bypassDomain"` RouteAddress []netip.Prefix `json:"routeAddress"` RouteMode string `json:"routeMode"` Ipv4Address string `json:"ipv4Address"` Ipv6Address string `json:"ipv6Address"` DnsServerAddress string `json:"dnsServerAddress"` DozeSuspend bool `json:"dozeSuspend"` DisableIcmpForwarding bool `json:"disableIcmpForwarding"` Mtu uint32 `json:"mtu"` } type AccessControl struct { Enable bool `json:"enable"` Mode string `json:"mode"` AcceptList []string `json:"acceptList"` RejectList []string `json:"rejectList"` } type AndroidVpnRawOptions struct { Enable bool `json:"enable"` AccessControl *AccessControl `json:"accessControl"` AllowBypass bool `json:"allowBypass"` SystemProxy bool `json:"systemProxy"` RouteMode string `json:"routeMode"` DozeSuspend bool `json:"dozeSuspend"` } type State struct { VpnProps AndroidVpnRawOptions `json:"vpn-props"` CurrentProfileName string `json:"current-profile-name"` OnlyStatisticsProxy bool `json:"only-statistics-proxy"` BypassDomain []string `json:"bypass-domain"` } var CurrentState = &State{ OnlyStatisticsProxy: false, CurrentProfileName: "", } func GetDnsServerAddress() string { return DefaultDnsAddress } ================================================ FILE: core/tun/tun.go ================================================ //go:build android && cgo package tun import "C" import ( "core/state" "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing_tun" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel" "net" "net/netip" ) type Props struct { Fd int `json:"fd"` Gateway string `json:"gateway"` Gateway6 string `json:"gateway6"` Portal string `json:"portal"` Portal6 string `json:"portal6"` Dns string `json:"dns"` Dns6 string `json:"dns6"` } func Start(fd int, device string, stack constant.TUNStack, disableIcmpForwarding bool, mtu uint32, ipv6Enabled bool) (*sing_tun.Listener, error) { var prefix4 []netip.Prefix tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address) if err != nil { log.Errorln("startTUN error:", err) return nil, err } prefix4 = append(prefix4, tempPrefix4) var prefix6 []netip.Prefix if ipv6Enabled { tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address) if err != nil { log.Errorln("startTUN error:", err) return nil, err } prefix6 = append(prefix6, tempPrefix6) } var dnsHijack []string dnsHijack = append(dnsHijack, net.JoinHostPort(state.GetDnsServerAddress(), "53")) validMtu := mtu if validMtu < 1280 || validMtu > 65535 { validMtu = 1480 } options := LC.Tun{ Enable: true, Device: device, Stack: stack, DNSHijack: dnsHijack, AutoRoute: false, AutoDetectInterface: false, Inet4Address: prefix4, Inet6Address: prefix6, MTU: validMtu, FileDescriptor: fd, DisableICMPForwarding: disableIcmpForwarding, } listener, err := sing_tun.New(options, tunnel.Tunnel) if err != nil { log.Errorln("startTUN error:", err) return nil, err } return listener, nil } ================================================ FILE: devtools_options.yaml ================================================ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: ================================================ FILE: distribute_options.yaml ================================================ app_name: 'Bettbox' output: 'dist/' ================================================ FILE: lib/application.dart ================================================ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:bett_box/clash/clash.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/l10n/l10n.dart'; import 'package:bett_box/manager/hotkey_manager.dart'; import 'package:bett_box/manager/manager.dart'; import 'package:bett_box/plugins/app.dart'; import 'package:bett_box/providers/providers.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'controller.dart'; import 'pages/pages.dart'; class Application extends ConsumerStatefulWidget { const Application({super.key}); @override ConsumerState createState() => ApplicationState(); } class ApplicationState extends ConsumerState with WidgetsBindingObserver { Timer? _autoUpdateGroupTaskTimer; Timer? _autoUpdateProfilesTaskTimer; final _pageTransitionsTheme = const PageTransitionsTheme( builders: { TargetPlatform.android: CupertinoPageTransitionsBuilder(), TargetPlatform.windows: CupertinoPageTransitionsBuilder(), TargetPlatform.linux: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), }, ); ColorScheme _getAppColorScheme({ required Brightness brightness, int? primaryColor, }) { return ref.read(genColorSchemeProvider(brightness)); } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); globalState.backgroundMode.addListener(_syncAutoUpdateTasks); _syncAutoUpdateTasks(); globalState.appController = AppController(context, ref); WidgetsBinding.instance.addPostFrameCallback((_) { unawaited(_initApp()); }); } bool get _isForeground { final lifecycleState = WidgetsBinding.instance.lifecycleState; return lifecycleState == null || lifecycleState == AppLifecycleState.resumed; } Future _initApp() async { final currentContext = globalState.navigatorKey.currentContext; if (currentContext != null && currentContext != context) { globalState.appController = AppController(currentContext, ref); } await globalState.appController.init(); globalState.appController.initLink(); if (system.isAndroid) { app.initShortcuts(); } } @override void didChangeAppLifecycleState(AppLifecycleState state) { _syncAutoUpdateTasks(); if (state == AppLifecycleState.resumed) { if (system.isAndroid && globalState.config.appSetting.enableHighRefreshRate) { _restoreHighRefreshRate(); } } } void _syncAutoUpdateTasks() { final shouldRun = _isForeground && !globalState.backgroundMode.value; if (!shouldRun) { _autoUpdateGroupTaskTimer?.cancel(); _autoUpdateGroupTaskTimer = null; return; } if (_autoUpdateGroupTaskTimer == null) { _autoUpdateGroupTask(); } if (_autoUpdateProfilesTaskTimer == null) { _autoUpdateProfilesTask(); } } Future _restoreHighRefreshRate() async { try { await FlutterDisplayMode.setHighRefreshRate(); } catch (e) { commonPrint.log('Failed to restore high refresh rate: $e'); } } void _autoUpdateGroupTask() { _autoUpdateGroupTaskTimer = Timer.periodic( const Duration(seconds: 60), (_) => globalState.appController.updateGroupsDebounce(), ); } void _autoUpdateProfilesTask() { _autoUpdateProfilesTaskTimer = Timer.periodic( const Duration(hours: 24), (_) => unawaited(globalState.appController.autoUpdateProfiles()), ); } Widget _buildPlatformState(Widget child) { if (system.isDesktop) { return WindowManager( child: TrayManager( child: HotKeyManager( child: ProxyManager(child: SmartAutoStopManager(child: child)), ), ), ); } return AndroidManager( child: TileManager(child: SmartAutoStopManager(child: child)), ); } Widget _buildState(Widget child) { return AppStateManager( child: ClashManager( child: ConnectivityManager( onConnectivityChanged: (results) async { if (!results.contains(ConnectivityResult.vpn)) { clashCore.closeConnections(); } globalState.appController.updateLocalIp(); globalState.appController.addCheckIpNumDebounce(); }, child: child, ), ), ); } Widget _buildPlatformApp(Widget child) { if (system.isDesktop) { return WindowHeaderContainer(child: child); } return VpnManager(child: child); } Widget _buildApp(Widget child) { return MessageManager(child: ThemeManager(child: child)); } @override Widget build(context) { return _buildPlatformState( _buildState( Consumer( builder: (_, ref, child) { final locale = ref.watch( appSettingProvider.select((state) => state.locale), ); final themeProps = ref.watch(themeSettingProvider); final fontFamily = themeProps.useHarmonyFont ? 'HarmonyOS_Sans' : null; return MaterialApp( debugShowCheckedModeBanner: false, navigatorKey: globalState.navigatorKey, localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], builder: (_, child) { return ValueListenableBuilder( valueListenable: globalState.animationEnabled, builder: (_, enabled, _) { return TickerMode( enabled: enabled, child: AppEnvManager( child: _buildApp( AppSidebarContainer(child: _buildPlatformApp(child!)), ), ), ); }, ); }, scrollBehavior: BaseScrollBehavior(), title: appName, locale: utils.getLocaleForString(locale) ?? utils.getSystemLocale(), supportedLocales: AppLocalizations.delegate.supportedLocales, themeMode: themeProps.themeMode, theme: ThemeData( useMaterial3: true, pageTransitionsTheme: _pageTransitionsTheme, colorScheme: _getAppColorScheme( brightness: Brightness.light, primaryColor: themeProps.primaryColor, ), fontFamily: fontFamily, ), darkTheme: ThemeData( useMaterial3: true, pageTransitionsTheme: _pageTransitionsTheme, colorScheme: _getAppColorScheme( brightness: Brightness.dark, primaryColor: themeProps.primaryColor, ).toPureBlack(themeProps.pureBlack), fontFamily: fontFamily, ), home: child!, ); }, child: const HomePage(), ), ), ); } @override void dispose() { globalState.backgroundMode.removeListener(_syncAutoUpdateTasks); WidgetsBinding.instance.removeObserver(this); linkManager.destroy(); _autoUpdateGroupTaskTimer?.cancel(); _autoUpdateProfilesTaskTimer?.cancel(); if (!system.isAndroid && !globalState.isExiting) { unawaited(globalState.appController.handleExit()); } super.dispose(); } } ================================================ FILE: lib/clash/clash.dart ================================================ export 'core.dart'; export 'lib.dart'; export 'message.dart'; export 'service.dart'; ================================================ FILE: lib/clash/core.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'package:bett_box/clash/clash.dart'; import 'package:bett_box/clash/interface.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart'; class ClashCore { static ClashCore? _instance; late ClashHandlerInterface clashInterface; ClashCore._internal() { if (system.isAndroid) { clashInterface = clashLib!; } else { clashInterface = clashService!; } } factory ClashCore() { _instance ??= ClashCore._internal(); return _instance!; } Future preload() { return clashInterface.preload(); } static Future initGeo() async { final homePath = await appPath.homeDirPath; final homeDir = Directory(homePath); if (!await homeDir.exists()) { await homeDir.create(recursive: true); } const geoFileNameList = [mmdbFileName, geoSiteFileName, asnFileName]; try { for (final geoFileName in geoFileNameList) { final geoFile = File(join(homePath, geoFileName)); if (await geoFile.exists()) continue; final data = await rootBundle.load('assets/data/$geoFileName'); await geoFile.writeAsBytes(data.buffer.asUint8List(), flush: true); } } catch (e) { exit(0); } } Future init() async { await initGeo(); if (globalState.config.appSetting.openLogs) { clashCore.startLog(); } else { clashCore.stopLog(); } final homeDirPath = await appPath.homeDirPath; return await clashInterface.init( InitParams(homeDir: homeDirPath, version: globalState.appState.version), ); } Future setState(CoreState state) async { return await clashInterface.setState(state); } Future shutdown() async { await clashInterface.shutdown(); } FutureOr get isInit => clashInterface.isInit; FutureOr validateConfig(String data) { return clashInterface.validateConfig(data); } Future updateConfig(UpdateParams updateParams) async { return await clashInterface.updateConfig(updateParams); } Future setupConfig(SetupParams setupParams) async { return await clashInterface.setupConfig(setupParams); } Future> getProxiesGroups() async { final proxies = await clashInterface.getProxies(); if (proxies.isEmpty) return []; return Isolate.run>(() { final groupNames = [ UsedProxy.GLOBAL.name, ...(proxies[UsedProxy.GLOBAL.name]['all'] as List).where((e) { final proxy = proxies[e] as Map?; return GroupTypeExtension.valueList.contains(proxy?['type']); }), ]; final groupsRaw = groupNames.map((groupName) { final group = Map.from( (proxies[groupName] as Map).cast(), ); group['all'] = ((group['all'] ?? []) as List) .map((name) => proxies[name]) .whereType>() .toList(); return group; }).toList(); return groupsRaw.map((e) => Group.fromJson(e)).toList(); }); } FutureOr changeProxy(ChangeProxyParams changeProxyParams) async { return await clashInterface.changeProxy(changeProxyParams); } Future> getConnections() async { final res = await clashInterface.getConnections(); if (res.isEmpty) { return []; } try { final connectionsData = json.decode(res) as Map; final connectionsRaw = connectionsData['connections'] as List? ?? []; return connectionsRaw.map((e) => TrackerInfo.fromJson(e)).toList(); } catch (e) { commonPrint.log('Failed to parse connections: $e'); return []; } } void closeConnection(String id) { clashInterface.closeConnection(id); } void closeConnections() { clashInterface.closeConnections(); } void resetConnections() { clashInterface.resetConnections(); } Future> getExternalProviders() async { final externalProvidersRawString = await clashInterface .getExternalProviders(); if (externalProvidersRawString.isEmpty) { return []; } try { return Isolate.run>(() { final externalProviders = (json.decode(externalProvidersRawString) as List) .map((item) => ExternalProvider.fromJson(item)) .toList(); return externalProviders; }); } catch (e) { commonPrint.log('Failed to parse external providers: $e'); return []; } } Future getExternalProvider( String externalProviderName, ) async { final externalProvidersRawString = await clashInterface.getExternalProvider( externalProviderName, ); if (externalProvidersRawString.isEmpty) { return null; } try { return ExternalProvider.fromJson(json.decode(externalProvidersRawString)); } catch (e) { commonPrint.log('Failed to parse external provider: $e'); return null; } } Future updateGeoData(UpdateGeoDataParams params) { return clashInterface.updateGeoData(params); } Future sideLoadExternalProvider({ required String providerName, required String data, }) { return clashInterface.sideLoadExternalProvider( providerName: providerName, data: data, ); } Future updateExternalProvider({required String providerName}) async { return clashInterface.updateExternalProvider(providerName); } Future startListener() async { await clashInterface.startListener(); } Future stopListener() async { await clashInterface.stopListener(); } Future getDelay(String url, String proxyName) async { final data = await clashInterface.asyncTestDelay(url, proxyName); if (data.isEmpty) { throw Exception('Empty delay response'); } try { return Delay.fromJson(json.decode(data)); } catch (e) { commonPrint.log('Failed to parse delay: $e'); rethrow; } } Future> getConfig(String id) async { final profilePath = await appPath.getProfilePath(id); final res = await clashInterface.getConfig(profilePath); if (res.isSuccess) { return res.data as Map; } else { throw res.message; } } Future getTraffic() async { final trafficString = await clashInterface.getTraffic(); if (trafficString.isEmpty) { return Traffic(); } try { return Traffic.fromMap(json.decode(trafficString)); } catch (e) { commonPrint.log('Failed to parse traffic: $e'); return Traffic(); } } Future getCountryCode(String ip) async { final countryCode = await clashInterface.getCountryCode(ip); if (countryCode.isEmpty) { return null; } return IpInfo(ip: ip, countryCode: countryCode); } Future getTotalTraffic() async { final totalTrafficString = await clashInterface.getTotalTraffic(); if (totalTrafficString.isEmpty) { return Traffic(); } try { return Traffic.fromMap(json.decode(totalTrafficString)); } catch (e) { commonPrint.log('Failed to parse total traffic: $e'); return Traffic(); } } Future getMemory() async { final value = await clashInterface.getMemory(); if (value.isEmpty) { return 0; } return int.parse(value); } void resetTraffic() { clashInterface.resetTraffic(); } void startLog() { clashInterface.startLog(); } void stopLog() { clashInterface.stopLog(); } Future requestGc() async { await clashInterface.forceGc(); } Future flushFakeIP() async { await clashInterface.flushFakeIP(); } Future flushDnsCache() async { await clashInterface.flushDnsCache(); } Future destroy() async { await clashInterface.destroy(); } } final clashCore = ClashCore(); ================================================ FILE: lib/clash/generated/clash_ffi.dart ================================================ // AUTO GENERATED FILE, DO NOT EDIT. // // Generated by `package:ffigen`. // ignore_for_file: type=lint import 'dart:ffi' as ffi; class ClashFFI { /// Holds the symbol lookup function. final ffi.Pointer Function(String symbolName) _lookup; /// The symbols are looked up in [dynamicLibrary]. ClashFFI(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; /// The symbols are looked up with [lookup]. ClashFFI.fromLookup( ffi.Pointer Function(String symbolName) lookup, ) : _lookup = lookup; ffi.Pointer> signal( int arg0, ffi.Pointer> arg1, ) { return _signal(arg0, arg1); } late final _signalPtr = _lookup< ffi.NativeFunction< ffi.Pointer> Function( ffi.Int, ffi.Pointer>, ) > >('signal'); late final _signal = _signalPtr .asFunction< ffi.Pointer> Function( int, ffi.Pointer>, ) >(); int getpriority(int arg0, int arg1) { return _getpriority(arg0, arg1); } late final _getpriorityPtr = _lookup>( 'getpriority', ); late final _getpriority = _getpriorityPtr .asFunction(); int getiopolicy_np(int arg0, int arg1) { return _getiopolicy_np(arg0, arg1); } late final _getiopolicy_npPtr = _lookup>( 'getiopolicy_np', ); late final _getiopolicy_np = _getiopolicy_npPtr .asFunction(); int getrlimit(int arg0, ffi.Pointer arg1) { return _getrlimit(arg0, arg1); } late final _getrlimitPtr = _lookup< ffi.NativeFunction)> >('getrlimit'); late final _getrlimit = _getrlimitPtr .asFunction)>(); int getrusage(int arg0, ffi.Pointer arg1) { return _getrusage(arg0, arg1); } late final _getrusagePtr = _lookup< ffi.NativeFunction)> >('getrusage'); late final _getrusage = _getrusagePtr .asFunction)>(); int setpriority(int arg0, int arg1, int arg2) { return _setpriority(arg0, arg1, arg2); } late final _setpriorityPtr = _lookup>( 'setpriority', ); late final _setpriority = _setpriorityPtr .asFunction(); int setiopolicy_np(int arg0, int arg1, int arg2) { return _setiopolicy_np(arg0, arg1, arg2); } late final _setiopolicy_npPtr = _lookup>( 'setiopolicy_np', ); late final _setiopolicy_np = _setiopolicy_npPtr .asFunction(); int setrlimit(int arg0, ffi.Pointer arg1) { return _setrlimit(arg0, arg1); } late final _setrlimitPtr = _lookup< ffi.NativeFunction)> >('setrlimit'); late final _setrlimit = _setrlimitPtr .asFunction)>(); int wait$1(ffi.Pointer arg0) { return _wait$1(arg0); } late final _wait$1Ptr = _lookup)>>('wait'); late final _wait$1 = _wait$1Ptr .asFunction)>(); int waitpid(int arg0, ffi.Pointer arg1, int arg2) { return _waitpid(arg0, arg1, arg2); } late final _waitpidPtr = _lookup< ffi.NativeFunction, ffi.Int)> >('waitpid'); late final _waitpid = _waitpidPtr .asFunction, int)>(); int waitid( idtype_t arg0, Dart__uint32_t arg1, ffi.Pointer arg2, int arg3, ) { return _waitid(arg0.value, arg1, arg2, arg3); } late final _waitidPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.UnsignedInt, id_t, ffi.Pointer, ffi.Int, ) > >('waitid'); late final _waitid = _waitidPtr .asFunction, int)>(); int wait3(ffi.Pointer arg0, int arg1, ffi.Pointer arg2) { return _wait3(arg0, arg1, arg2); } late final _wait3Ptr = _lookup< ffi.NativeFunction< pid_t Function(ffi.Pointer, ffi.Int, ffi.Pointer) > >('wait3'); late final _wait3 = _wait3Ptr .asFunction< int Function(ffi.Pointer, int, ffi.Pointer) >(); int wait4( int arg0, ffi.Pointer arg1, int arg2, ffi.Pointer arg3, ) { return _wait4(arg0, arg1, arg2, arg3); } late final _wait4Ptr = _lookup< ffi.NativeFunction< pid_t Function( pid_t, ffi.Pointer, ffi.Int, ffi.Pointer, ) > >('wait4'); late final _wait4 = _wait4Ptr .asFunction< int Function(int, ffi.Pointer, int, ffi.Pointer) >(); ffi.Pointer alloca(int arg0) { return _alloca(arg0); } late final _allocaPtr = _lookup Function(ffi.Size)>>( 'alloca', ); late final _alloca = _allocaPtr .asFunction Function(int)>(); late final ffi.Pointer ___mb_cur_max = _lookup( '__mb_cur_max', ); int get __mb_cur_max => ___mb_cur_max.value; set __mb_cur_max(int value) => ___mb_cur_max.value = value; ffi.Pointer malloc_type_malloc(int size, int type_id) { return _malloc_type_malloc(size, type_id); } late final _malloc_type_mallocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Size, malloc_type_id_t) > >('malloc_type_malloc'); late final _malloc_type_malloc = _malloc_type_mallocPtr .asFunction Function(int, int)>(); ffi.Pointer malloc_type_calloc(int count, int size, int type_id) { return _malloc_type_calloc(count, size, type_id); } late final _malloc_type_callocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Size, ffi.Size, malloc_type_id_t) > >('malloc_type_calloc'); late final _malloc_type_calloc = _malloc_type_callocPtr .asFunction Function(int, int, int)>(); void malloc_type_free(ffi.Pointer ptr, int type_id) { return _malloc_type_free(ptr, type_id); } late final _malloc_type_freePtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, malloc_type_id_t) > >('malloc_type_free'); late final _malloc_type_free = _malloc_type_freePtr .asFunction, int)>(); ffi.Pointer malloc_type_realloc( ffi.Pointer ptr, int size, int type_id, ) { return _malloc_type_realloc(ptr, size, type_id); } late final _malloc_type_reallocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Size, malloc_type_id_t, ) > >('malloc_type_realloc'); late final _malloc_type_realloc = _malloc_type_reallocPtr .asFunction< ffi.Pointer Function(ffi.Pointer, int, int) >(); ffi.Pointer malloc_type_valloc(int size, int type_id) { return _malloc_type_valloc(size, type_id); } late final _malloc_type_vallocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Size, malloc_type_id_t) > >('malloc_type_valloc'); late final _malloc_type_valloc = _malloc_type_vallocPtr .asFunction Function(int, int)>(); ffi.Pointer malloc_type_aligned_alloc( int alignment, int size, int type_id, ) { return _malloc_type_aligned_alloc(alignment, size, type_id); } late final _malloc_type_aligned_allocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Size, ffi.Size, malloc_type_id_t) > >('malloc_type_aligned_alloc'); late final _malloc_type_aligned_alloc = _malloc_type_aligned_allocPtr .asFunction Function(int, int, int)>(); int malloc_type_posix_memalign( ffi.Pointer> memptr, int alignment, int size, int type_id, ) { return _malloc_type_posix_memalign(memptr, alignment, size, type_id); } late final _malloc_type_posix_memalignPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Size, ffi.Size, malloc_type_id_t, ) > >('malloc_type_posix_memalign'); late final _malloc_type_posix_memalign = _malloc_type_posix_memalignPtr .asFunction< int Function(ffi.Pointer>, int, int, int) >(); ffi.Pointer malloc_type_zone_malloc( ffi.Pointer zone, int size, int type_id, ) { return _malloc_type_zone_malloc(zone, size, type_id); } late final _malloc_type_zone_mallocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Size, malloc_type_id_t, ) > >('malloc_type_zone_malloc'); late final _malloc_type_zone_malloc = _malloc_type_zone_mallocPtr .asFunction< ffi.Pointer Function(ffi.Pointer, int, int) >(); ffi.Pointer malloc_type_zone_calloc( ffi.Pointer zone, int count, int size, int type_id, ) { return _malloc_type_zone_calloc(zone, count, size, type_id); } late final _malloc_type_zone_callocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Size, ffi.Size, malloc_type_id_t, ) > >('malloc_type_zone_calloc'); late final _malloc_type_zone_calloc = _malloc_type_zone_callocPtr .asFunction< ffi.Pointer Function( ffi.Pointer, int, int, int, ) >(); void malloc_type_zone_free( ffi.Pointer zone, ffi.Pointer ptr, int type_id, ) { return _malloc_type_zone_free(zone, ptr, type_id); } late final _malloc_type_zone_freePtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Pointer, malloc_type_id_t, ) > >('malloc_type_zone_free'); late final _malloc_type_zone_free = _malloc_type_zone_freePtr .asFunction< void Function(ffi.Pointer, ffi.Pointer, int) >(); ffi.Pointer malloc_type_zone_realloc( ffi.Pointer zone, ffi.Pointer ptr, int size, int type_id, ) { return _malloc_type_zone_realloc(zone, ptr, size, type_id); } late final _malloc_type_zone_reallocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ffi.Size, malloc_type_id_t, ) > >('malloc_type_zone_realloc'); late final _malloc_type_zone_realloc = _malloc_type_zone_reallocPtr .asFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, int, int, ) >(); ffi.Pointer malloc_type_zone_valloc( ffi.Pointer zone, int size, int type_id, ) { return _malloc_type_zone_valloc(zone, size, type_id); } late final _malloc_type_zone_vallocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Size, malloc_type_id_t, ) > >('malloc_type_zone_valloc'); late final _malloc_type_zone_valloc = _malloc_type_zone_vallocPtr .asFunction< ffi.Pointer Function(ffi.Pointer, int, int) >(); ffi.Pointer malloc_type_zone_memalign( ffi.Pointer zone, int alignment, int size, int type_id, ) { return _malloc_type_zone_memalign(zone, alignment, size, type_id); } late final _malloc_type_zone_memalignPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Size, ffi.Size, malloc_type_id_t, ) > >('malloc_type_zone_memalign'); late final _malloc_type_zone_memalign = _malloc_type_zone_memalignPtr .asFunction< ffi.Pointer Function( ffi.Pointer, int, int, int, ) >(); ffi.Pointer malloc(int __size) { return _malloc(__size); } late final _mallocPtr = _lookup Function(ffi.Size)>>( 'malloc', ); late final _malloc = _mallocPtr .asFunction Function(int)>(); ffi.Pointer calloc(int __count, int __size) { return _calloc(__count, __size); } late final _callocPtr = _lookup< ffi.NativeFunction Function(ffi.Size, ffi.Size)> >('calloc'); late final _calloc = _callocPtr .asFunction Function(int, int)>(); void free(ffi.Pointer arg0) { return _free(arg0); } late final _freePtr = _lookup)>>( 'free', ); late final _free = _freePtr .asFunction)>(); ffi.Pointer realloc(ffi.Pointer __ptr, int __size) { return _realloc(__ptr, __size); } late final _reallocPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Pointer, ffi.Size) > >('realloc'); late final _realloc = _reallocPtr .asFunction Function(ffi.Pointer, int)>(); ffi.Pointer reallocf(ffi.Pointer __ptr, int __size) { return _reallocf(__ptr, __size); } late final _reallocfPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Pointer, ffi.Size) > >('reallocf'); late final _reallocf = _reallocfPtr .asFunction Function(ffi.Pointer, int)>(); ffi.Pointer valloc(int __size) { return _valloc(__size); } late final _vallocPtr = _lookup Function(ffi.Size)>>( 'valloc', ); late final _valloc = _vallocPtr .asFunction Function(int)>(); ffi.Pointer aligned_alloc(int __alignment, int __size) { return _aligned_alloc(__alignment, __size); } late final _aligned_allocPtr = _lookup< ffi.NativeFunction Function(ffi.Size, ffi.Size)> >('aligned_alloc'); late final _aligned_alloc = _aligned_allocPtr .asFunction Function(int, int)>(); int posix_memalign( ffi.Pointer> __memptr, int __alignment, int __size, ) { return _posix_memalign(__memptr, __alignment, __size); } late final _posix_memalignPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Size, ffi.Size, ) > >('posix_memalign'); late final _posix_memalign = _posix_memalignPtr .asFunction>, int, int)>(); void abort() { return _abort(); } late final _abortPtr = _lookup>( 'abort', ); late final _abort = _abortPtr.asFunction(); int abs(int arg0) { return _abs(arg0); } late final _absPtr = _lookup>( 'abs', ); late final _abs = _absPtr.asFunction(); int atexit(ffi.Pointer> arg0) { return _atexit(arg0); } late final _atexitPtr = _lookup< ffi.NativeFunction< ffi.Int Function(ffi.Pointer>) > >('atexit'); late final _atexit = _atexitPtr .asFunction< int Function(ffi.Pointer>) >(); int at_quick_exit(ffi.Pointer> arg0) { return _at_quick_exit(arg0); } late final _at_quick_exitPtr = _lookup< ffi.NativeFunction< ffi.Int Function(ffi.Pointer>) > >('at_quick_exit'); late final _at_quick_exit = _at_quick_exitPtr .asFunction< int Function(ffi.Pointer>) >(); double atof(ffi.Pointer arg0) { return _atof(arg0); } late final _atofPtr = _lookup)>>( 'atof', ); late final _atof = _atofPtr .asFunction)>(); int atoi(ffi.Pointer arg0) { return _atoi(arg0); } late final _atoiPtr = _lookup)>>( 'atoi', ); late final _atoi = _atoiPtr.asFunction)>(); int atol(ffi.Pointer arg0) { return _atol(arg0); } late final _atolPtr = _lookup)>>( 'atol', ); late final _atol = _atolPtr.asFunction)>(); int atoll(ffi.Pointer arg0) { return _atoll(arg0); } late final _atollPtr = _lookup)>>( 'atoll', ); late final _atoll = _atollPtr .asFunction)>(); ffi.Pointer bsearch( ffi.Pointer __key, ffi.Pointer __base, int __nel, int __width, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > > __compar, ) { return _bsearch(__key, __base, __nel, __width, __compar); } late final _bsearchPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ffi.Size, ffi.Size, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) > >('bsearch'); late final _bsearch = _bsearchPtr .asFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, int, int, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) >(); div_t div(int arg0, int arg1) { return _div(arg0, arg1); } late final _divPtr = _lookup>('div'); late final _div = _divPtr.asFunction(); void exit(int arg0) { return _exit(arg0); } late final _exitPtr = _lookup>( 'exit', ); late final _exit = _exitPtr.asFunction(); ffi.Pointer getenv(ffi.Pointer arg0) { return _getenv(arg0); } late final _getenvPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Pointer) > >('getenv'); late final _getenv = _getenvPtr .asFunction Function(ffi.Pointer)>(); int labs(int arg0) { return _labs(arg0); } late final _labsPtr = _lookup>('labs'); late final _labs = _labsPtr.asFunction(); ldiv_t ldiv(int arg0, int arg1) { return _ldiv(arg0, arg1); } late final _ldivPtr = _lookup>('ldiv'); late final _ldiv = _ldivPtr.asFunction(); int llabs(int arg0) { return _llabs(arg0); } late final _llabsPtr = _lookup>('llabs'); late final _llabs = _llabsPtr.asFunction(); lldiv_t lldiv(int arg0, int arg1) { return _lldiv(arg0, arg1); } late final _lldivPtr = _lookup>( 'lldiv', ); late final _lldiv = _lldivPtr.asFunction(); int mblen(ffi.Pointer __s, int __n) { return _mblen(__s, __n); } late final _mblenPtr = _lookup< ffi.NativeFunction, ffi.Size)> >('mblen'); late final _mblen = _mblenPtr .asFunction, int)>(); int mbstowcs( ffi.Pointer arg0, ffi.Pointer arg1, int arg2, ) { return _mbstowcs(arg0, arg1, arg2); } late final _mbstowcsPtr = _lookup< ffi.NativeFunction< ffi.Size Function( ffi.Pointer, ffi.Pointer, ffi.Size, ) > >('mbstowcs'); late final _mbstowcs = _mbstowcsPtr .asFunction< int Function(ffi.Pointer, ffi.Pointer, int) >(); int mbtowc( ffi.Pointer arg0, ffi.Pointer arg1, int arg2, ) { return _mbtowc(arg0, arg1, arg2); } late final _mbtowcPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Size, ) > >('mbtowc'); late final _mbtowc = _mbtowcPtr .asFunction< int Function(ffi.Pointer, ffi.Pointer, int) >(); void qsort( ffi.Pointer __base, int __nel, int __width, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > > __compar, ) { return _qsort(__base, __nel, __width, __compar); } late final _qsortPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Size, ffi.Size, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) > >('qsort'); late final _qsort = _qsortPtr .asFunction< void Function( ffi.Pointer, int, int, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) >(); void quick_exit(int arg0) { return _quick_exit(arg0); } late final _quick_exitPtr = _lookup>('quick_exit'); late final _quick_exit = _quick_exitPtr.asFunction(); int rand() { return _rand(); } late final _randPtr = _lookup>('rand'); late final _rand = _randPtr.asFunction(); void srand(int arg0) { return _srand(arg0); } late final _srandPtr = _lookup>('srand'); late final _srand = _srandPtr.asFunction(); double strtod( ffi.Pointer arg0, ffi.Pointer> arg1, ) { return _strtod(arg0, arg1); } late final _strtodPtr = _lookup< ffi.NativeFunction< ffi.Double Function( ffi.Pointer, ffi.Pointer>, ) > >('strtod'); late final _strtod = _strtodPtr .asFunction< double Function( ffi.Pointer, ffi.Pointer>, ) >(); double strtof( ffi.Pointer arg0, ffi.Pointer> arg1, ) { return _strtof(arg0, arg1); } late final _strtofPtr = _lookup< ffi.NativeFunction< ffi.Float Function( ffi.Pointer, ffi.Pointer>, ) > >('strtof'); late final _strtof = _strtofPtr .asFunction< double Function( ffi.Pointer, ffi.Pointer>, ) >(); int strtol( ffi.Pointer __str, ffi.Pointer> __endptr, int __base, ) { return _strtol(__str, __endptr, __base); } late final _strtolPtr = _lookup< ffi.NativeFunction< ffi.Long Function( ffi.Pointer, ffi.Pointer>, ffi.Int, ) > >('strtol'); late final _strtol = _strtolPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer>, int, ) >(); int strtoll( ffi.Pointer __str, ffi.Pointer> __endptr, int __base, ) { return _strtoll(__str, __endptr, __base); } late final _strtollPtr = _lookup< ffi.NativeFunction< ffi.LongLong Function( ffi.Pointer, ffi.Pointer>, ffi.Int, ) > >('strtoll'); late final _strtoll = _strtollPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer>, int, ) >(); int strtoul( ffi.Pointer __str, ffi.Pointer> __endptr, int __base, ) { return _strtoul(__str, __endptr, __base); } late final _strtoulPtr = _lookup< ffi.NativeFunction< ffi.UnsignedLong Function( ffi.Pointer, ffi.Pointer>, ffi.Int, ) > >('strtoul'); late final _strtoul = _strtoulPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer>, int, ) >(); int strtoull( ffi.Pointer __str, ffi.Pointer> __endptr, int __base, ) { return _strtoull(__str, __endptr, __base); } late final _strtoullPtr = _lookup< ffi.NativeFunction< ffi.UnsignedLongLong Function( ffi.Pointer, ffi.Pointer>, ffi.Int, ) > >('strtoull'); late final _strtoull = _strtoullPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer>, int, ) >(); int system(ffi.Pointer arg0) { return _system(arg0); } late final _systemPtr = _lookup)>>( 'system', ); late final _system = _systemPtr .asFunction)>(); int wcstombs( ffi.Pointer arg0, ffi.Pointer arg1, int arg2, ) { return _wcstombs(arg0, arg1, arg2); } late final _wcstombsPtr = _lookup< ffi.NativeFunction< ffi.Size Function( ffi.Pointer, ffi.Pointer, ffi.Size, ) > >('wcstombs'); late final _wcstombs = _wcstombsPtr .asFunction< int Function(ffi.Pointer, ffi.Pointer, int) >(); int wctomb(ffi.Pointer arg0, int arg1) { return _wctomb(arg0, arg1); } late final _wctombPtr = _lookup< ffi.NativeFunction, ffi.WChar)> >('wctomb'); late final _wctomb = _wctombPtr .asFunction, int)>(); void _Exit(int arg0) { return __Exit(arg0); } late final __ExitPtr = _lookup>('_Exit'); late final __Exit = __ExitPtr.asFunction(); int a64l(ffi.Pointer arg0) { return _a64l(arg0); } late final _a64lPtr = _lookup)>>( 'a64l', ); late final _a64l = _a64lPtr.asFunction)>(); double drand48() { return _drand48(); } late final _drand48Ptr = _lookup>( 'drand48', ); late final _drand48 = _drand48Ptr.asFunction(); ffi.Pointer ecvt( double arg0, int arg1, ffi.Pointer arg2, ffi.Pointer arg3, ) { return _ecvt(arg0, arg1, arg2, arg3); } late final _ecvtPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Double, ffi.Int, ffi.Pointer, ffi.Pointer, ) > >('ecvt'); late final _ecvt = _ecvtPtr .asFunction< ffi.Pointer Function( double, int, ffi.Pointer, ffi.Pointer, ) >(); double erand48(ffi.Pointer arg0) { return _erand48(arg0); } late final _erand48Ptr = _lookup< ffi.NativeFunction)> >('erand48'); late final _erand48 = _erand48Ptr .asFunction)>(); ffi.Pointer fcvt( double arg0, int arg1, ffi.Pointer arg2, ffi.Pointer arg3, ) { return _fcvt(arg0, arg1, arg2, arg3); } late final _fcvtPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Double, ffi.Int, ffi.Pointer, ffi.Pointer, ) > >('fcvt'); late final _fcvt = _fcvtPtr .asFunction< ffi.Pointer Function( double, int, ffi.Pointer, ffi.Pointer, ) >(); ffi.Pointer gcvt( double arg0, int arg1, ffi.Pointer arg2, ) { return _gcvt(arg0, arg1, arg2); } late final _gcvtPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Double, ffi.Int, ffi.Pointer, ) > >('gcvt'); late final _gcvt = _gcvtPtr .asFunction< ffi.Pointer Function(double, int, ffi.Pointer) >(); int getsubopt( ffi.Pointer> arg0, ffi.Pointer> arg1, ffi.Pointer> arg2, ) { return _getsubopt(arg0, arg1, arg2); } late final _getsuboptPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Pointer>, ffi.Pointer>, ) > >('getsubopt'); late final _getsubopt = _getsuboptPtr .asFunction< int Function( ffi.Pointer>, ffi.Pointer>, ffi.Pointer>, ) >(); int grantpt(int arg0) { return _grantpt(arg0); } late final _grantptPtr = _lookup>('grantpt'); late final _grantpt = _grantptPtr.asFunction(); ffi.Pointer initstate( int arg0, ffi.Pointer arg1, int arg2, ) { return _initstate(arg0, arg1, arg2); } late final _initstatePtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.UnsignedInt, ffi.Pointer, ffi.Size, ) > >('initstate'); late final _initstate = _initstatePtr .asFunction< ffi.Pointer Function(int, ffi.Pointer, int) >(); int jrand48(ffi.Pointer arg0) { return _jrand48(arg0); } late final _jrand48Ptr = _lookup< ffi.NativeFunction)> >('jrand48'); late final _jrand48 = _jrand48Ptr .asFunction)>(); ffi.Pointer l64a(int arg0) { return _l64a(arg0); } late final _l64aPtr = _lookup Function(ffi.Long)>>( 'l64a', ); late final _l64a = _l64aPtr.asFunction Function(int)>(); void lcong48(ffi.Pointer arg0) { return _lcong48(arg0); } late final _lcong48Ptr = _lookup< ffi.NativeFunction)> >('lcong48'); late final _lcong48 = _lcong48Ptr .asFunction)>(); int lrand48() { return _lrand48(); } late final _lrand48Ptr = _lookup>( 'lrand48', ); late final _lrand48 = _lrand48Ptr.asFunction(); ffi.Pointer mktemp(ffi.Pointer arg0) { return _mktemp(arg0); } late final _mktempPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Pointer) > >('mktemp'); late final _mktemp = _mktempPtr .asFunction Function(ffi.Pointer)>(); int mkstemp(ffi.Pointer arg0) { return _mkstemp(arg0); } late final _mkstempPtr = _lookup)>>( 'mkstemp', ); late final _mkstemp = _mkstempPtr .asFunction)>(); int mrand48() { return _mrand48(); } late final _mrand48Ptr = _lookup>( 'mrand48', ); late final _mrand48 = _mrand48Ptr.asFunction(); int nrand48(ffi.Pointer arg0) { return _nrand48(arg0); } late final _nrand48Ptr = _lookup< ffi.NativeFunction)> >('nrand48'); late final _nrand48 = _nrand48Ptr .asFunction)>(); int posix_openpt(int arg0) { return _posix_openpt(arg0); } late final _posix_openptPtr = _lookup>('posix_openpt'); late final _posix_openpt = _posix_openptPtr.asFunction(); ffi.Pointer ptsname(int arg0) { return _ptsname(arg0); } late final _ptsnamePtr = _lookup Function(ffi.Int)>>( 'ptsname', ); late final _ptsname = _ptsnamePtr .asFunction Function(int)>(); int ptsname_r(int fildes, ffi.Pointer buffer, int buflen) { return _ptsname_r(fildes, buffer, buflen); } late final _ptsname_rPtr = _lookup< ffi.NativeFunction< ffi.Int Function(ffi.Int, ffi.Pointer, ffi.Size) > >('ptsname_r'); late final _ptsname_r = _ptsname_rPtr .asFunction, int)>(); int putenv(ffi.Pointer arg0) { return _putenv(arg0); } late final _putenvPtr = _lookup)>>( 'putenv', ); late final _putenv = _putenvPtr .asFunction)>(); int random() { return _random(); } late final _randomPtr = _lookup>( 'random', ); late final _random = _randomPtr.asFunction(); int rand_r(ffi.Pointer arg0) { return _rand_r(arg0); } late final _rand_rPtr = _lookup< ffi.NativeFunction)> >('rand_r'); late final _rand_r = _rand_rPtr .asFunction)>(); ffi.Pointer realpath( ffi.Pointer arg0, ffi.Pointer arg1, ) { return _realpath(arg0, arg1); } late final _realpathPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ) > >('realpath'); late final _realpath = _realpathPtr .asFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ) >(); ffi.Pointer seed48(ffi.Pointer arg0) { return _seed48(arg0); } late final _seed48Ptr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ) > >('seed48'); late final _seed48 = _seed48Ptr .asFunction< ffi.Pointer Function(ffi.Pointer) >(); int setenv( ffi.Pointer __name, ffi.Pointer __value, int __overwrite, ) { return _setenv(__name, __value, __overwrite); } late final _setenvPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Int, ) > >('setenv'); late final _setenv = _setenvPtr .asFunction< int Function(ffi.Pointer, ffi.Pointer, int) >(); void setkey(ffi.Pointer arg0) { return _setkey(arg0); } late final _setkeyPtr = _lookup)>>( 'setkey', ); late final _setkey = _setkeyPtr .asFunction)>(); ffi.Pointer setstate(ffi.Pointer arg0) { return _setstate(arg0); } late final _setstatePtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Pointer) > >('setstate'); late final _setstate = _setstatePtr .asFunction Function(ffi.Pointer)>(); void srand48(int arg0) { return _srand48(arg0); } late final _srand48Ptr = _lookup>('srand48'); late final _srand48 = _srand48Ptr.asFunction(); void srandom(int arg0) { return _srandom(arg0); } late final _srandomPtr = _lookup>( 'srandom', ); late final _srandom = _srandomPtr.asFunction(); int unlockpt(int arg0) { return _unlockpt(arg0); } late final _unlockptPtr = _lookup>('unlockpt'); late final _unlockpt = _unlockptPtr.asFunction(); int unsetenv(ffi.Pointer arg0) { return _unsetenv(arg0); } late final _unsetenvPtr = _lookup)>>( 'unsetenv', ); late final _unsetenv = _unsetenvPtr .asFunction)>(); int arc4random() { return _arc4random(); } late final _arc4randomPtr = _lookup>('arc4random'); late final _arc4random = _arc4randomPtr.asFunction(); void arc4random_addrandom(ffi.Pointer arg0, int arg1) { return _arc4random_addrandom(arg0, arg1); } late final _arc4random_addrandomPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Int) > >('arc4random_addrandom'); late final _arc4random_addrandom = _arc4random_addrandomPtr .asFunction, int)>(); void arc4random_buf(ffi.Pointer __buf, int __nbytes) { return _arc4random_buf(__buf, __nbytes); } late final _arc4random_bufPtr = _lookup< ffi.NativeFunction, ffi.Size)> >('arc4random_buf'); late final _arc4random_buf = _arc4random_bufPtr .asFunction, int)>(); void arc4random_stir() { return _arc4random_stir(); } late final _arc4random_stirPtr = _lookup>('arc4random_stir'); late final _arc4random_stir = _arc4random_stirPtr .asFunction(); int arc4random_uniform(int __upper_bound) { return _arc4random_uniform(__upper_bound); } late final _arc4random_uniformPtr = _lookup>( 'arc4random_uniform', ); late final _arc4random_uniform = _arc4random_uniformPtr .asFunction(); ffi.Pointer cgetcap( ffi.Pointer arg0, ffi.Pointer arg1, int arg2, ) { return _cgetcap(arg0, arg1, arg2); } late final _cgetcapPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ffi.Int, ) > >('cgetcap'); late final _cgetcap = _cgetcapPtr .asFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, int, ) >(); int cgetclose() { return _cgetclose(); } late final _cgetclosePtr = _lookup>( 'cgetclose', ); late final _cgetclose = _cgetclosePtr.asFunction(); int cgetent( ffi.Pointer> arg0, ffi.Pointer> arg1, ffi.Pointer arg2, ) { return _cgetent(arg0, arg1, arg2); } late final _cgetentPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Pointer>, ffi.Pointer, ) > >('cgetent'); late final _cgetent = _cgetentPtr .asFunction< int Function( ffi.Pointer>, ffi.Pointer>, ffi.Pointer, ) >(); int cgetfirst( ffi.Pointer> arg0, ffi.Pointer> arg1, ) { return _cgetfirst(arg0, arg1); } late final _cgetfirstPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Pointer>, ) > >('cgetfirst'); late final _cgetfirst = _cgetfirstPtr .asFunction< int Function( ffi.Pointer>, ffi.Pointer>, ) >(); int cgetmatch(ffi.Pointer arg0, ffi.Pointer arg1) { return _cgetmatch(arg0, arg1); } late final _cgetmatchPtr = _lookup< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >('cgetmatch'); late final _cgetmatch = _cgetmatchPtr .asFunction, ffi.Pointer)>(); int cgetnext( ffi.Pointer> arg0, ffi.Pointer> arg1, ) { return _cgetnext(arg0, arg1); } late final _cgetnextPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Pointer>, ) > >('cgetnext'); late final _cgetnext = _cgetnextPtr .asFunction< int Function( ffi.Pointer>, ffi.Pointer>, ) >(); int cgetnum( ffi.Pointer arg0, ffi.Pointer arg1, ffi.Pointer arg2, ) { return _cgetnum(arg0, arg1, arg2); } late final _cgetnumPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) > >('cgetnum'); late final _cgetnum = _cgetnumPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) >(); int cgetset(ffi.Pointer arg0) { return _cgetset(arg0); } late final _cgetsetPtr = _lookup)>>( 'cgetset', ); late final _cgetset = _cgetsetPtr .asFunction)>(); int cgetstr( ffi.Pointer arg0, ffi.Pointer arg1, ffi.Pointer> arg2, ) { return _cgetstr(arg0, arg1, arg2); } late final _cgetstrPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer>, ) > >('cgetstr'); late final _cgetstr = _cgetstrPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer>, ) >(); int cgetustr( ffi.Pointer arg0, ffi.Pointer arg1, ffi.Pointer> arg2, ) { return _cgetustr(arg0, arg1, arg2); } late final _cgetustrPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer>, ) > >('cgetustr'); late final _cgetustr = _cgetustrPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer>, ) >(); int daemon(int arg0, int arg1) { return _daemon(arg0, arg1); } late final _daemonPtr = _lookup>('daemon'); late final _daemon = _daemonPtr.asFunction(); ffi.Pointer devname(int arg0, int arg1) { return _devname(arg0, arg1); } late final _devnamePtr = _lookup< ffi.NativeFunction Function(dev_t, mode_t)> >('devname'); late final _devname = _devnamePtr .asFunction Function(int, int)>(); ffi.Pointer devname_r( int arg0, int arg1, ffi.Pointer buf, int len, ) { return _devname_r(arg0, arg1, buf, len); } late final _devname_rPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( dev_t, mode_t, ffi.Pointer, ffi.Int, ) > >('devname_r'); late final _devname_r = _devname_rPtr .asFunction< ffi.Pointer Function(int, int, ffi.Pointer, int) >(); ffi.Pointer getbsize( ffi.Pointer arg0, ffi.Pointer arg1, ) { return _getbsize(arg0, arg1); } late final _getbsizePtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ) > >('getbsize'); late final _getbsize = _getbsizePtr .asFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ) >(); int getloadavg(ffi.Pointer arg0, int arg1) { return _getloadavg(arg0, arg1); } late final _getloadavgPtr = _lookup< ffi.NativeFunction, ffi.Int)> >('getloadavg'); late final _getloadavg = _getloadavgPtr .asFunction, int)>(); ffi.Pointer getprogname() { return _getprogname(); } late final _getprognamePtr = _lookup Function()>>( 'getprogname', ); late final _getprogname = _getprognamePtr .asFunction Function()>(); void setprogname(ffi.Pointer arg0) { return _setprogname(arg0); } late final _setprognamePtr = _lookup)>>( 'setprogname', ); late final _setprogname = _setprognamePtr .asFunction)>(); int heapsort( ffi.Pointer __base, int __nel, int __width, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > > __compar, ) { return _heapsort(__base, __nel, __width, __compar); } late final _heapsortPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Size, ffi.Size, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) > >('heapsort'); late final _heapsort = _heapsortPtr .asFunction< int Function( ffi.Pointer, int, int, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) >(); int mergesort( ffi.Pointer __base, int __nel, int __width, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > > __compar, ) { return _mergesort(__base, __nel, __width, __compar); } late final _mergesortPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Size, ffi.Size, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) > >('mergesort'); late final _mergesort = _mergesortPtr .asFunction< int Function( ffi.Pointer, int, int, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) >(); void psort( ffi.Pointer __base, int __nel, int __width, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > > __compar, ) { return _psort(__base, __nel, __width, __compar); } late final _psortPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Size, ffi.Size, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) > >('psort'); late final _psort = _psortPtr .asFunction< void Function( ffi.Pointer, int, int, ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer, ffi.Pointer) > >, ) >(); void psort_r( ffi.Pointer __base, int __nel, int __width, ffi.Pointer arg3, ffi.Pointer< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) > > __compar, ) { return _psort_r(__base, __nel, __width, arg3, __compar); } late final _psort_rPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Size, ffi.Size, ffi.Pointer, ffi.Pointer< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) > >, ) > >('psort_r'); late final _psort_r = _psort_rPtr .asFunction< void Function( ffi.Pointer, int, int, ffi.Pointer, ffi.Pointer< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) > >, ) >(); void qsort_r( ffi.Pointer __base, int __nel, int __width, ffi.Pointer arg3, ffi.Pointer< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) > > __compar, ) { return _qsort_r(__base, __nel, __width, arg3, __compar); } late final _qsort_rPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Size, ffi.Size, ffi.Pointer, ffi.Pointer< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) > >, ) > >('qsort_r'); late final _qsort_r = _qsort_rPtr .asFunction< void Function( ffi.Pointer, int, int, ffi.Pointer, ffi.Pointer< ffi.NativeFunction< ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ) > >, ) >(); int radixsort( ffi.Pointer> __base, int __nel, ffi.Pointer __table, int __endbyte, ) { return _radixsort(__base, __nel, __table, __endbyte); } late final _radixsortPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Int, ffi.Pointer, ffi.UnsignedInt, ) > >('radixsort'); late final _radixsort = _radixsortPtr .asFunction< int Function( ffi.Pointer>, int, ffi.Pointer, int, ) >(); int rpmatch(ffi.Pointer arg0) { return _rpmatch(arg0); } late final _rpmatchPtr = _lookup)>>( 'rpmatch', ); late final _rpmatch = _rpmatchPtr .asFunction)>(); int sradixsort( ffi.Pointer> __base, int __nel, ffi.Pointer __table, int __endbyte, ) { return _sradixsort(__base, __nel, __table, __endbyte); } late final _sradixsortPtr = _lookup< ffi.NativeFunction< ffi.Int Function( ffi.Pointer>, ffi.Int, ffi.Pointer, ffi.UnsignedInt, ) > >('sradixsort'); late final _sradixsort = _sradixsortPtr .asFunction< int Function( ffi.Pointer>, int, ffi.Pointer, int, ) >(); void sranddev() { return _sranddev(); } late final _sranddevPtr = _lookup>( 'sranddev', ); late final _sranddev = _sranddevPtr.asFunction(); void srandomdev() { return _srandomdev(); } late final _srandomdevPtr = _lookup>( 'srandomdev', ); late final _srandomdev = _srandomdevPtr.asFunction(); int strtonum( ffi.Pointer __numstr, int __minval, int __maxval, ffi.Pointer> __errstrp, ) { return _strtonum(__numstr, __minval, __maxval, __errstrp); } late final _strtonumPtr = _lookup< ffi.NativeFunction< ffi.LongLong Function( ffi.Pointer, ffi.LongLong, ffi.LongLong, ffi.Pointer>, ) > >('strtonum'); late final _strtonum = _strtonumPtr .asFunction< int Function( ffi.Pointer, int, int, ffi.Pointer>, ) >(); int strtoq( ffi.Pointer __str, ffi.Pointer> __endptr, int __base, ) { return _strtoq(__str, __endptr, __base); } late final _strtoqPtr = _lookup< ffi.NativeFunction< ffi.LongLong Function( ffi.Pointer, ffi.Pointer>, ffi.Int, ) > >('strtoq'); late final _strtoq = _strtoqPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer>, int, ) >(); int strtouq( ffi.Pointer __str, ffi.Pointer> __endptr, int __base, ) { return _strtouq(__str, __endptr, __base); } late final _strtouqPtr = _lookup< ffi.NativeFunction< ffi.UnsignedLongLong Function( ffi.Pointer, ffi.Pointer>, ffi.Int, ) > >('strtouq'); late final _strtouq = _strtouqPtr .asFunction< int Function( ffi.Pointer, ffi.Pointer>, int, ) >(); late final ffi.Pointer> _suboptarg = _lookup>('suboptarg'); ffi.Pointer get suboptarg => _suboptarg.value; set suboptarg(ffi.Pointer value) => _suboptarg.value = value; void protect(protect_func fn, ffi.Pointer tun_interface, int fd) { return _protect(fn, tun_interface, fd); } late final _protectPtr = _lookup< ffi.NativeFunction< ffi.Void Function(protect_func, ffi.Pointer, ffi.Int) > >('protect'); late final _protect = _protectPtr .asFunction, int)>(); ffi.Pointer resolve_process( resolve_process_func fn, ffi.Pointer tun_interface, int protocol, ffi.Pointer source, ffi.Pointer target, int uid, ) { return _resolve_process(fn, tun_interface, protocol, source, target, uid); } late final _resolve_processPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( resolve_process_func, ffi.Pointer, ffi.Int, ffi.Pointer, ffi.Pointer, ffi.Int, ) > >('resolve_process'); late final _resolve_process = _resolve_processPtr .asFunction< ffi.Pointer Function( resolve_process_func, ffi.Pointer, int, ffi.Pointer, ffi.Pointer, int, ) >(); void release_object(release_object_func fn, ffi.Pointer obj) { return _release_object(fn, obj); } late final _release_objectPtr = _lookup< ffi.NativeFunction< ffi.Void Function(release_object_func, ffi.Pointer) > >('release_object'); late final _release_object = _release_objectPtr .asFunction)>(); void registerCallbacks( protect_func markSocketFunc, resolve_process_func resolveProcessFunc, release_object_func releaseObjectFunc, ) { return _registerCallbacks( markSocketFunc, resolveProcessFunc, releaseObjectFunc, ); } late final _registerCallbacksPtr = _lookup< ffi.NativeFunction< ffi.Void Function( protect_func, resolve_process_func, release_object_func, ) > >('registerCallbacks'); late final _registerCallbacks = _registerCallbacksPtr .asFunction< void Function(protect_func, resolve_process_func, release_object_func) >(); void initNativeApiBridge(ffi.Pointer api) { return _initNativeApiBridge(api); } late final _initNativeApiBridgePtr = _lookup)>>( 'initNativeApiBridge', ); late final _initNativeApiBridge = _initNativeApiBridgePtr .asFunction)>(); void attachMessagePort(int mPort) { return _attachMessagePort(mPort); } late final _attachMessagePortPtr = _lookup>( 'attachMessagePort', ); late final _attachMessagePort = _attachMessagePortPtr .asFunction(); ffi.Pointer getTraffic() { return _getTraffic(); } late final _getTrafficPtr = _lookup Function()>>( 'getTraffic', ); late final _getTraffic = _getTrafficPtr .asFunction Function()>(); ffi.Pointer getTotalTraffic() { return _getTotalTraffic(); } late final _getTotalTrafficPtr = _lookup Function()>>( 'getTotalTraffic', ); late final _getTotalTraffic = _getTotalTrafficPtr .asFunction Function()>(); void freeCString(ffi.Pointer s) { return _freeCString(s); } late final _freeCStringPtr = _lookup)>>( 'freeCString', ); late final _freeCString = _freeCStringPtr .asFunction)>(); void invokeAction(ffi.Pointer paramsChar, int port) { return _invokeAction(paramsChar, port); } late final _invokeActionPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.LongLong) > >('invokeAction'); late final _invokeAction = _invokeActionPtr .asFunction, int)>(); ffi.Pointer getConfig(ffi.Pointer s) { return _getConfig(s); } late final _getConfigPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Pointer) > >('getConfig'); late final _getConfig = _getConfigPtr .asFunction Function(ffi.Pointer)>(); void startListener() { return _startListener(); } late final _startListenerPtr = _lookup>('startListener'); late final _startListener = _startListenerPtr.asFunction(); void stopListener() { return _stopListener(); } late final _stopListenerPtr = _lookup>('stopListener'); late final _stopListener = _stopListenerPtr.asFunction(); void quickStart( ffi.Pointer initParamsChar, ffi.Pointer paramsChar, ffi.Pointer stateParamsChar, int port, ) { return _quickStart(initParamsChar, paramsChar, stateParamsChar, port); } late final _quickStartPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.LongLong, ) > >('quickStart'); late final _quickStart = _quickStartPtr .asFunction< void Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, int, ) >(); int startTUN(int fd, ffi.Pointer callback) { return _startTUN(fd, callback); } late final _startTUNPtr = _lookup< ffi.NativeFunction)> >('startTUN'); late final _startTUN = _startTUNPtr .asFunction)>(); ffi.Pointer getRunTime() { return _getRunTime(); } late final _getRunTimePtr = _lookup Function()>>( 'getRunTime', ); late final _getRunTime = _getRunTimePtr .asFunction Function()>(); void stopTun() { return _stopTun(); } late final _stopTunPtr = _lookup>( 'stopTun', ); late final _stopTun = _stopTunPtr.asFunction(); ffi.Pointer getCurrentProfileName() { return _getCurrentProfileName(); } late final _getCurrentProfileNamePtr = _lookup Function()>>( 'getCurrentProfileName', ); late final _getCurrentProfileName = _getCurrentProfileNamePtr .asFunction Function()>(); ffi.Pointer getAndroidVpnOptions() { return _getAndroidVpnOptions(); } late final _getAndroidVpnOptionsPtr = _lookup Function()>>( 'getAndroidVpnOptions', ); late final _getAndroidVpnOptions = _getAndroidVpnOptionsPtr .asFunction Function()>(); void setState(ffi.Pointer s) { return _setState(s); } late final _setStatePtr = _lookup)>>( 'setState', ); late final _setState = _setStatePtr .asFunction)>(); void updateDns(ffi.Pointer s) { return _updateDns(s); } late final _updateDnsPtr = _lookup)>>( 'updateDns', ); late final _updateDns = _updateDnsPtr .asFunction)>(); } typedef __int8_t = ffi.SignedChar; typedef Dart__int8_t = int; typedef __uint8_t = ffi.UnsignedChar; typedef Dart__uint8_t = int; typedef __int16_t = ffi.Short; typedef Dart__int16_t = int; typedef __uint16_t = ffi.UnsignedShort; typedef Dart__uint16_t = int; typedef __int32_t = ffi.Int; typedef Dart__int32_t = int; typedef __uint32_t = ffi.UnsignedInt; typedef Dart__uint32_t = int; typedef __int64_t = ffi.LongLong; typedef Dart__int64_t = int; typedef __uint64_t = ffi.UnsignedLongLong; typedef Dart__uint64_t = int; typedef __darwin_intptr_t = ffi.Long; typedef Dart__darwin_intptr_t = int; typedef __darwin_natural_t = ffi.UnsignedInt; typedef Dart__darwin_natural_t = int; typedef __darwin_ct_rune_t = ffi.Int; typedef Dart__darwin_ct_rune_t = int; final class __mbstate_t extends ffi.Union { @ffi.Array.multi([128]) external ffi.Array __mbstate8; @ffi.LongLong() external int _mbstateL; } typedef __darwin_mbstate_t = __mbstate_t; typedef __darwin_ptrdiff_t = ffi.Long; typedef Dart__darwin_ptrdiff_t = int; typedef __darwin_size_t = ffi.UnsignedLong; typedef Dart__darwin_size_t = int; typedef __builtin_va_list = ffi.Pointer; typedef __darwin_va_list = __builtin_va_list; typedef __darwin_wchar_t = ffi.Int; typedef Dart__darwin_wchar_t = int; typedef __darwin_rune_t = __darwin_wchar_t; typedef __darwin_wint_t = ffi.Int; typedef Dart__darwin_wint_t = int; typedef __darwin_clock_t = ffi.UnsignedLong; typedef Dart__darwin_clock_t = int; typedef __darwin_socklen_t = __uint32_t; typedef __darwin_ssize_t = ffi.Long; typedef Dart__darwin_ssize_t = int; typedef __darwin_time_t = ffi.Long; typedef Dart__darwin_time_t = int; typedef __darwin_blkcnt_t = __int64_t; typedef __darwin_blksize_t = __int32_t; typedef __darwin_dev_t = __int32_t; typedef __darwin_fsblkcnt_t = ffi.UnsignedInt; typedef Dart__darwin_fsblkcnt_t = int; typedef __darwin_fsfilcnt_t = ffi.UnsignedInt; typedef Dart__darwin_fsfilcnt_t = int; typedef __darwin_gid_t = __uint32_t; typedef __darwin_id_t = __uint32_t; typedef __darwin_ino64_t = __uint64_t; typedef __darwin_ino_t = __darwin_ino64_t; typedef __darwin_mach_port_name_t = __darwin_natural_t; typedef __darwin_mach_port_t = __darwin_mach_port_name_t; typedef __darwin_mode_t = __uint16_t; typedef __darwin_off_t = __int64_t; typedef __darwin_pid_t = __int32_t; typedef __darwin_sigset_t = __uint32_t; typedef __darwin_suseconds_t = __int32_t; typedef __darwin_uid_t = __uint32_t; typedef __darwin_useconds_t = __uint32_t; final class __darwin_pthread_handler_rec extends ffi.Struct { external ffi.Pointer< ffi.NativeFunction)> > __routine; external ffi.Pointer __arg; external ffi.Pointer<__darwin_pthread_handler_rec> __next; } final class _opaque_pthread_attr_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([56]) external ffi.Array __opaque; } final class _opaque_pthread_cond_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([40]) external ffi.Array __opaque; } final class _opaque_pthread_condattr_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([8]) external ffi.Array __opaque; } final class _opaque_pthread_mutex_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([56]) external ffi.Array __opaque; } final class _opaque_pthread_mutexattr_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([8]) external ffi.Array __opaque; } final class _opaque_pthread_once_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([8]) external ffi.Array __opaque; } final class _opaque_pthread_rwlock_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([192]) external ffi.Array __opaque; } final class _opaque_pthread_rwlockattr_t extends ffi.Struct { @ffi.Long() external int __sig; @ffi.Array.multi([16]) external ffi.Array __opaque; } final class _opaque_pthread_t extends ffi.Struct { @ffi.Long() external int __sig; external ffi.Pointer<__darwin_pthread_handler_rec> __cleanup_stack; @ffi.Array.multi([8176]) external ffi.Array __opaque; } typedef __darwin_pthread_attr_t = _opaque_pthread_attr_t; typedef __darwin_pthread_cond_t = _opaque_pthread_cond_t; typedef __darwin_pthread_condattr_t = _opaque_pthread_condattr_t; typedef __darwin_pthread_key_t = ffi.UnsignedLong; typedef Dart__darwin_pthread_key_t = int; typedef __darwin_pthread_mutex_t = _opaque_pthread_mutex_t; typedef __darwin_pthread_mutexattr_t = _opaque_pthread_mutexattr_t; typedef __darwin_pthread_once_t = _opaque_pthread_once_t; typedef __darwin_pthread_rwlock_t = _opaque_pthread_rwlock_t; typedef __darwin_pthread_rwlockattr_t = _opaque_pthread_rwlockattr_t; typedef __darwin_pthread_t = ffi.Pointer<_opaque_pthread_t>; typedef __darwin_nl_item = ffi.Int; typedef Dart__darwin_nl_item = int; typedef __darwin_wctrans_t = ffi.Int; typedef Dart__darwin_wctrans_t = int; typedef __darwin_wctype_t = __uint32_t; typedef u_int8_t = ffi.UnsignedChar; typedef Dartu_int8_t = int; typedef u_int16_t = ffi.UnsignedShort; typedef Dartu_int16_t = int; typedef u_int32_t = ffi.UnsignedInt; typedef Dartu_int32_t = int; typedef u_int64_t = ffi.UnsignedLongLong; typedef Dartu_int64_t = int; typedef register_t = ffi.Int64; typedef Dartregister_t = int; typedef user_addr_t = u_int64_t; typedef user_size_t = u_int64_t; typedef user_ssize_t = ffi.Int64; typedef Dartuser_ssize_t = int; typedef user_long_t = ffi.Int64; typedef Dartuser_long_t = int; typedef user_ulong_t = u_int64_t; typedef user_time_t = ffi.Int64; typedef Dartuser_time_t = int; typedef user_off_t = ffi.Int64; typedef Dartuser_off_t = int; typedef syscall_arg_t = u_int64_t; typedef ptrdiff_t = __darwin_ptrdiff_t; typedef rsize_t = __darwin_size_t; typedef wint_t = __darwin_wint_t; final class _GoString_ extends ffi.Struct { external ffi.Pointer p; @ptrdiff_t() external int n; } enum idtype_t { P_ALL(0), P_PID(1), P_PGID(2); final int value; const idtype_t(this.value); static idtype_t fromValue(int value) => switch (value) { 0 => P_ALL, 1 => P_PID, 2 => P_PGID, _ => throw ArgumentError('Unknown value for idtype_t: $value'), }; } typedef pid_t = __darwin_pid_t; typedef id_t = __darwin_id_t; typedef sig_atomic_t = ffi.Int; typedef Dartsig_atomic_t = int; final class __darwin_arm_exception_state extends ffi.Struct { @__uint32_t() external int __exception; @__uint32_t() external int __fsr; @__uint32_t() external int __far; } final class __darwin_arm_exception_state64 extends ffi.Struct { @__uint64_t() external int __far; @__uint32_t() external int __esr; @__uint32_t() external int __exception; } final class __darwin_arm_exception_state64_v2 extends ffi.Struct { @__uint64_t() external int __far; @__uint64_t() external int __esr; } final class __darwin_arm_thread_state extends ffi.Struct { @ffi.Array.multi([13]) external ffi.Array<__uint32_t> __r; @__uint32_t() external int __sp; @__uint32_t() external int __lr; @__uint32_t() external int __pc; @__uint32_t() external int __cpsr; } final class __darwin_arm_thread_state64 extends ffi.Struct { @ffi.Array.multi([29]) external ffi.Array<__uint64_t> __x; @__uint64_t() external int __fp; @__uint64_t() external int __lr; @__uint64_t() external int __sp; @__uint64_t() external int __pc; @__uint32_t() external int __cpsr; @__uint32_t() external int __pad; } final class __darwin_arm_vfp_state extends ffi.Struct { @ffi.Array.multi([64]) external ffi.Array<__uint32_t> __r; @__uint32_t() external int __fpscr; } final class __darwin_arm_neon_state64 extends ffi.Opaque {} final class __darwin_arm_neon_state extends ffi.Opaque {} final class __arm_pagein_state extends ffi.Struct { @ffi.Int() external int __pagein_error; } final class __arm_legacy_debug_state extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __bvr; @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __bcr; @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __wvr; @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __wcr; } final class __darwin_arm_debug_state32 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __bvr; @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __bcr; @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __wvr; @ffi.Array.multi([16]) external ffi.Array<__uint32_t> __wcr; @__uint64_t() external int __mdscr_el1; } final class __darwin_arm_debug_state64 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array<__uint64_t> __bvr; @ffi.Array.multi([16]) external ffi.Array<__uint64_t> __bcr; @ffi.Array.multi([16]) external ffi.Array<__uint64_t> __wvr; @ffi.Array.multi([16]) external ffi.Array<__uint64_t> __wcr; @__uint64_t() external int __mdscr_el1; } final class __darwin_arm_cpmu_state64 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array<__uint64_t> __ctrs; } final class __darwin_mcontext32 extends ffi.Struct { external __darwin_arm_exception_state __es; external __darwin_arm_thread_state __ss; external __darwin_arm_vfp_state __fs; } final class __darwin_mcontext64 extends ffi.Opaque {} typedef mcontext_t = ffi.Pointer<__darwin_mcontext64>; typedef pthread_attr_t = __darwin_pthread_attr_t; final class __darwin_sigaltstack extends ffi.Struct { external ffi.Pointer ss_sp; @__darwin_size_t() external int ss_size; @ffi.Int() external int ss_flags; } typedef stack_t = __darwin_sigaltstack; final class __darwin_ucontext extends ffi.Struct { @ffi.Int() external int uc_onstack; @__darwin_sigset_t() external int uc_sigmask; external __darwin_sigaltstack uc_stack; external ffi.Pointer<__darwin_ucontext> uc_link; @__darwin_size_t() external int uc_mcsize; external ffi.Pointer<__darwin_mcontext64> uc_mcontext; } typedef ucontext_t = __darwin_ucontext; typedef sigset_t = __darwin_sigset_t; typedef uid_t = __darwin_uid_t; final class sigval extends ffi.Union { @ffi.Int() external int sival_int; external ffi.Pointer sival_ptr; } final class sigevent extends ffi.Struct { @ffi.Int() external int sigev_notify; @ffi.Int() external int sigev_signo; external sigval sigev_value; external ffi.Pointer> sigev_notify_function; external ffi.Pointer sigev_notify_attributes; } final class __siginfo extends ffi.Struct { @ffi.Int() external int si_signo; @ffi.Int() external int si_errno; @ffi.Int() external int si_code; @pid_t() external int si_pid; @uid_t() external int si_uid; @ffi.Int() external int si_status; external ffi.Pointer si_addr; external sigval si_value; @ffi.Long() external int si_band; @ffi.Array.multi([7]) external ffi.Array __pad; } typedef siginfo_t = __siginfo; final class __sigaction_u extends ffi.Union { external ffi.Pointer> __sa_handler; external ffi.Pointer< ffi.NativeFunction< ffi.Void Function(ffi.Int, ffi.Pointer<__siginfo>, ffi.Pointer) > > __sa_sigaction; } final class __sigaction extends ffi.Struct { external __sigaction_u __sigaction_u$1; external ffi.Pointer< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Int, ffi.Int, ffi.Pointer, ffi.Pointer, ) > > sa_tramp; @sigset_t() external int sa_mask; @ffi.Int() external int sa_flags; } final class sigaction extends ffi.Struct { external __sigaction_u __sigaction_u$1; @sigset_t() external int sa_mask; @ffi.Int() external int sa_flags; } typedef sig_tFunction = ffi.Void Function(ffi.Int); typedef Dartsig_tFunction = void Function(int); typedef sig_t = ffi.Pointer>; final class sigvec extends ffi.Struct { external ffi.Pointer> sv_handler; @ffi.Int() external int sv_mask; @ffi.Int() external int sv_flags; } final class sigstack extends ffi.Struct { external ffi.Pointer ss_sp; @ffi.Int() external int ss_onstack; } typedef int_least8_t = ffi.Int8; typedef Dartint_least8_t = int; typedef int_least16_t = ffi.Int16; typedef Dartint_least16_t = int; typedef int_least32_t = ffi.Int32; typedef Dartint_least32_t = int; typedef int_least64_t = ffi.Int64; typedef Dartint_least64_t = int; typedef uint_least8_t = ffi.Uint8; typedef Dartuint_least8_t = int; typedef uint_least16_t = ffi.Uint16; typedef Dartuint_least16_t = int; typedef uint_least32_t = ffi.Uint32; typedef Dartuint_least32_t = int; typedef uint_least64_t = ffi.Uint64; typedef Dartuint_least64_t = int; typedef int_fast8_t = ffi.Int8; typedef Dartint_fast8_t = int; typedef int_fast16_t = ffi.Int16; typedef Dartint_fast16_t = int; typedef int_fast32_t = ffi.Int32; typedef Dartint_fast32_t = int; typedef int_fast64_t = ffi.Int64; typedef Dartint_fast64_t = int; typedef uint_fast8_t = ffi.Uint8; typedef Dartuint_fast8_t = int; typedef uint_fast16_t = ffi.Uint16; typedef Dartuint_fast16_t = int; typedef uint_fast32_t = ffi.Uint32; typedef Dartuint_fast32_t = int; typedef uint_fast64_t = ffi.Uint64; typedef Dartuint_fast64_t = int; typedef intmax_t = ffi.Long; typedef Dartintmax_t = int; typedef uintmax_t = ffi.UnsignedLong; typedef Dartuintmax_t = int; final class timeval extends ffi.Struct { @__darwin_time_t() external int tv_sec; @__darwin_suseconds_t() external int tv_usec; } typedef rlim_t = __uint64_t; final class rusage extends ffi.Struct { external timeval ru_utime; external timeval ru_stime; @ffi.Long() external int ru_maxrss; @ffi.Long() external int ru_ixrss; @ffi.Long() external int ru_idrss; @ffi.Long() external int ru_isrss; @ffi.Long() external int ru_minflt; @ffi.Long() external int ru_majflt; @ffi.Long() external int ru_nswap; @ffi.Long() external int ru_inblock; @ffi.Long() external int ru_oublock; @ffi.Long() external int ru_msgsnd; @ffi.Long() external int ru_msgrcv; @ffi.Long() external int ru_nsignals; @ffi.Long() external int ru_nvcsw; @ffi.Long() external int ru_nivcsw; } typedef rusage_info_t = ffi.Pointer; final class rusage_info_v0 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array ri_uuid; @ffi.Uint64() external int ri_user_time; @ffi.Uint64() external int ri_system_time; @ffi.Uint64() external int ri_pkg_idle_wkups; @ffi.Uint64() external int ri_interrupt_wkups; @ffi.Uint64() external int ri_pageins; @ffi.Uint64() external int ri_wired_size; @ffi.Uint64() external int ri_resident_size; @ffi.Uint64() external int ri_phys_footprint; @ffi.Uint64() external int ri_proc_start_abstime; @ffi.Uint64() external int ri_proc_exit_abstime; } final class rusage_info_v1 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array ri_uuid; @ffi.Uint64() external int ri_user_time; @ffi.Uint64() external int ri_system_time; @ffi.Uint64() external int ri_pkg_idle_wkups; @ffi.Uint64() external int ri_interrupt_wkups; @ffi.Uint64() external int ri_pageins; @ffi.Uint64() external int ri_wired_size; @ffi.Uint64() external int ri_resident_size; @ffi.Uint64() external int ri_phys_footprint; @ffi.Uint64() external int ri_proc_start_abstime; @ffi.Uint64() external int ri_proc_exit_abstime; @ffi.Uint64() external int ri_child_user_time; @ffi.Uint64() external int ri_child_system_time; @ffi.Uint64() external int ri_child_pkg_idle_wkups; @ffi.Uint64() external int ri_child_interrupt_wkups; @ffi.Uint64() external int ri_child_pageins; @ffi.Uint64() external int ri_child_elapsed_abstime; } final class rusage_info_v2 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array ri_uuid; @ffi.Uint64() external int ri_user_time; @ffi.Uint64() external int ri_system_time; @ffi.Uint64() external int ri_pkg_idle_wkups; @ffi.Uint64() external int ri_interrupt_wkups; @ffi.Uint64() external int ri_pageins; @ffi.Uint64() external int ri_wired_size; @ffi.Uint64() external int ri_resident_size; @ffi.Uint64() external int ri_phys_footprint; @ffi.Uint64() external int ri_proc_start_abstime; @ffi.Uint64() external int ri_proc_exit_abstime; @ffi.Uint64() external int ri_child_user_time; @ffi.Uint64() external int ri_child_system_time; @ffi.Uint64() external int ri_child_pkg_idle_wkups; @ffi.Uint64() external int ri_child_interrupt_wkups; @ffi.Uint64() external int ri_child_pageins; @ffi.Uint64() external int ri_child_elapsed_abstime; @ffi.Uint64() external int ri_diskio_bytesread; @ffi.Uint64() external int ri_diskio_byteswritten; } final class rusage_info_v3 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array ri_uuid; @ffi.Uint64() external int ri_user_time; @ffi.Uint64() external int ri_system_time; @ffi.Uint64() external int ri_pkg_idle_wkups; @ffi.Uint64() external int ri_interrupt_wkups; @ffi.Uint64() external int ri_pageins; @ffi.Uint64() external int ri_wired_size; @ffi.Uint64() external int ri_resident_size; @ffi.Uint64() external int ri_phys_footprint; @ffi.Uint64() external int ri_proc_start_abstime; @ffi.Uint64() external int ri_proc_exit_abstime; @ffi.Uint64() external int ri_child_user_time; @ffi.Uint64() external int ri_child_system_time; @ffi.Uint64() external int ri_child_pkg_idle_wkups; @ffi.Uint64() external int ri_child_interrupt_wkups; @ffi.Uint64() external int ri_child_pageins; @ffi.Uint64() external int ri_child_elapsed_abstime; @ffi.Uint64() external int ri_diskio_bytesread; @ffi.Uint64() external int ri_diskio_byteswritten; @ffi.Uint64() external int ri_cpu_time_qos_default; @ffi.Uint64() external int ri_cpu_time_qos_maintenance; @ffi.Uint64() external int ri_cpu_time_qos_background; @ffi.Uint64() external int ri_cpu_time_qos_utility; @ffi.Uint64() external int ri_cpu_time_qos_legacy; @ffi.Uint64() external int ri_cpu_time_qos_user_initiated; @ffi.Uint64() external int ri_cpu_time_qos_user_interactive; @ffi.Uint64() external int ri_billed_system_time; @ffi.Uint64() external int ri_serviced_system_time; } final class rusage_info_v4 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array ri_uuid; @ffi.Uint64() external int ri_user_time; @ffi.Uint64() external int ri_system_time; @ffi.Uint64() external int ri_pkg_idle_wkups; @ffi.Uint64() external int ri_interrupt_wkups; @ffi.Uint64() external int ri_pageins; @ffi.Uint64() external int ri_wired_size; @ffi.Uint64() external int ri_resident_size; @ffi.Uint64() external int ri_phys_footprint; @ffi.Uint64() external int ri_proc_start_abstime; @ffi.Uint64() external int ri_proc_exit_abstime; @ffi.Uint64() external int ri_child_user_time; @ffi.Uint64() external int ri_child_system_time; @ffi.Uint64() external int ri_child_pkg_idle_wkups; @ffi.Uint64() external int ri_child_interrupt_wkups; @ffi.Uint64() external int ri_child_pageins; @ffi.Uint64() external int ri_child_elapsed_abstime; @ffi.Uint64() external int ri_diskio_bytesread; @ffi.Uint64() external int ri_diskio_byteswritten; @ffi.Uint64() external int ri_cpu_time_qos_default; @ffi.Uint64() external int ri_cpu_time_qos_maintenance; @ffi.Uint64() external int ri_cpu_time_qos_background; @ffi.Uint64() external int ri_cpu_time_qos_utility; @ffi.Uint64() external int ri_cpu_time_qos_legacy; @ffi.Uint64() external int ri_cpu_time_qos_user_initiated; @ffi.Uint64() external int ri_cpu_time_qos_user_interactive; @ffi.Uint64() external int ri_billed_system_time; @ffi.Uint64() external int ri_serviced_system_time; @ffi.Uint64() external int ri_logical_writes; @ffi.Uint64() external int ri_lifetime_max_phys_footprint; @ffi.Uint64() external int ri_instructions; @ffi.Uint64() external int ri_cycles; @ffi.Uint64() external int ri_billed_energy; @ffi.Uint64() external int ri_serviced_energy; @ffi.Uint64() external int ri_interval_max_phys_footprint; @ffi.Uint64() external int ri_runnable_time; } final class rusage_info_v5 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array ri_uuid; @ffi.Uint64() external int ri_user_time; @ffi.Uint64() external int ri_system_time; @ffi.Uint64() external int ri_pkg_idle_wkups; @ffi.Uint64() external int ri_interrupt_wkups; @ffi.Uint64() external int ri_pageins; @ffi.Uint64() external int ri_wired_size; @ffi.Uint64() external int ri_resident_size; @ffi.Uint64() external int ri_phys_footprint; @ffi.Uint64() external int ri_proc_start_abstime; @ffi.Uint64() external int ri_proc_exit_abstime; @ffi.Uint64() external int ri_child_user_time; @ffi.Uint64() external int ri_child_system_time; @ffi.Uint64() external int ri_child_pkg_idle_wkups; @ffi.Uint64() external int ri_child_interrupt_wkups; @ffi.Uint64() external int ri_child_pageins; @ffi.Uint64() external int ri_child_elapsed_abstime; @ffi.Uint64() external int ri_diskio_bytesread; @ffi.Uint64() external int ri_diskio_byteswritten; @ffi.Uint64() external int ri_cpu_time_qos_default; @ffi.Uint64() external int ri_cpu_time_qos_maintenance; @ffi.Uint64() external int ri_cpu_time_qos_background; @ffi.Uint64() external int ri_cpu_time_qos_utility; @ffi.Uint64() external int ri_cpu_time_qos_legacy; @ffi.Uint64() external int ri_cpu_time_qos_user_initiated; @ffi.Uint64() external int ri_cpu_time_qos_user_interactive; @ffi.Uint64() external int ri_billed_system_time; @ffi.Uint64() external int ri_serviced_system_time; @ffi.Uint64() external int ri_logical_writes; @ffi.Uint64() external int ri_lifetime_max_phys_footprint; @ffi.Uint64() external int ri_instructions; @ffi.Uint64() external int ri_cycles; @ffi.Uint64() external int ri_billed_energy; @ffi.Uint64() external int ri_serviced_energy; @ffi.Uint64() external int ri_interval_max_phys_footprint; @ffi.Uint64() external int ri_runnable_time; @ffi.Uint64() external int ri_flags; } final class rusage_info_v6 extends ffi.Struct { @ffi.Array.multi([16]) external ffi.Array ri_uuid; @ffi.Uint64() external int ri_user_time; @ffi.Uint64() external int ri_system_time; @ffi.Uint64() external int ri_pkg_idle_wkups; @ffi.Uint64() external int ri_interrupt_wkups; @ffi.Uint64() external int ri_pageins; @ffi.Uint64() external int ri_wired_size; @ffi.Uint64() external int ri_resident_size; @ffi.Uint64() external int ri_phys_footprint; @ffi.Uint64() external int ri_proc_start_abstime; @ffi.Uint64() external int ri_proc_exit_abstime; @ffi.Uint64() external int ri_child_user_time; @ffi.Uint64() external int ri_child_system_time; @ffi.Uint64() external int ri_child_pkg_idle_wkups; @ffi.Uint64() external int ri_child_interrupt_wkups; @ffi.Uint64() external int ri_child_pageins; @ffi.Uint64() external int ri_child_elapsed_abstime; @ffi.Uint64() external int ri_diskio_bytesread; @ffi.Uint64() external int ri_diskio_byteswritten; @ffi.Uint64() external int ri_cpu_time_qos_default; @ffi.Uint64() external int ri_cpu_time_qos_maintenance; @ffi.Uint64() external int ri_cpu_time_qos_background; @ffi.Uint64() external int ri_cpu_time_qos_utility; @ffi.Uint64() external int ri_cpu_time_qos_legacy; @ffi.Uint64() external int ri_cpu_time_qos_user_initiated; @ffi.Uint64() external int ri_cpu_time_qos_user_interactive; @ffi.Uint64() external int ri_billed_system_time; @ffi.Uint64() external int ri_serviced_system_time; @ffi.Uint64() external int ri_logical_writes; @ffi.Uint64() external int ri_lifetime_max_phys_footprint; @ffi.Uint64() external int ri_instructions; @ffi.Uint64() external int ri_cycles; @ffi.Uint64() external int ri_billed_energy; @ffi.Uint64() external int ri_serviced_energy; @ffi.Uint64() external int ri_interval_max_phys_footprint; @ffi.Uint64() external int ri_runnable_time; @ffi.Uint64() external int ri_flags; @ffi.Uint64() external int ri_user_ptime; @ffi.Uint64() external int ri_system_ptime; @ffi.Uint64() external int ri_pinstructions; @ffi.Uint64() external int ri_pcycles; @ffi.Uint64() external int ri_energy_nj; @ffi.Uint64() external int ri_penergy_nj; @ffi.Uint64() external int ri_secure_time_in_system; @ffi.Uint64() external int ri_secure_ptime_in_system; @ffi.Uint64() external int ri_neural_footprint; @ffi.Uint64() external int ri_lifetime_max_neural_footprint; @ffi.Uint64() external int ri_interval_max_neural_footprint; @ffi.Array.multi([9]) external ffi.Array ri_reserved; } typedef rusage_info_current = rusage_info_v6; final class rlimit extends ffi.Struct { @rlim_t() external int rlim_cur; @rlim_t() external int rlim_max; } final class proc_rlimit_control_wakeupmon extends ffi.Struct { @ffi.Uint32() external int wm_flags; @ffi.Int32() external int wm_rate; } final class wait extends ffi.Opaque {} typedef ct_rune_t = __darwin_ct_rune_t; typedef rune_t = __darwin_rune_t; final class div_t extends ffi.Struct { @ffi.Int() external int quot; @ffi.Int() external int rem; } final class ldiv_t extends ffi.Struct { @ffi.Long() external int quot; @ffi.Long() external int rem; } final class lldiv_t extends ffi.Struct { @ffi.LongLong() external int quot; @ffi.LongLong() external int rem; } typedef malloc_type_id_t = ffi.UnsignedLongLong; typedef Dartmalloc_type_id_t = int; final class _malloc_zone_t extends ffi.Opaque {} typedef malloc_zone_t = _malloc_zone_t; typedef dev_t = __darwin_dev_t; typedef mode_t = __darwin_mode_t; typedef release_object_funcFunction = ffi.Void Function(ffi.Pointer obj); typedef Dartrelease_object_funcFunction = void Function(ffi.Pointer obj); typedef release_object_func = ffi.Pointer>; typedef protect_funcFunction = ffi.Void Function(ffi.Pointer tun_interface, ffi.Int fd); typedef Dartprotect_funcFunction = void Function(ffi.Pointer tun_interface, int fd); typedef protect_func = ffi.Pointer>; typedef resolve_process_funcFunction = ffi.Pointer Function( ffi.Pointer tun_interface, ffi.Int protocol, ffi.Pointer source, ffi.Pointer target, ffi.Int uid, ); typedef Dartresolve_process_funcFunction = ffi.Pointer Function( ffi.Pointer tun_interface, int protocol, ffi.Pointer source, ffi.Pointer target, int uid, ); typedef resolve_process_func = ffi.Pointer>; typedef GoInt8 = ffi.SignedChar; typedef DartGoInt8 = int; typedef GoUint8 = ffi.UnsignedChar; typedef DartGoUint8 = int; typedef GoInt16 = ffi.Short; typedef DartGoInt16 = int; typedef GoUint16 = ffi.UnsignedShort; typedef DartGoUint16 = int; typedef GoInt32 = ffi.Int; typedef DartGoInt32 = int; typedef GoUint32 = ffi.UnsignedInt; typedef DartGoUint32 = int; typedef GoInt64 = ffi.LongLong; typedef DartGoInt64 = int; typedef GoUint64 = ffi.UnsignedLongLong; typedef DartGoUint64 = int; typedef GoInt = GoInt64; typedef GoUint = GoUint64; typedef GoUintptr = ffi.Size; typedef DartGoUintptr = int; typedef GoFloat32 = ffi.Float; typedef DartGoFloat32 = double; typedef GoFloat64 = ffi.Double; typedef DartGoFloat64 = double; typedef GoString = _GoString_; typedef GoMap = ffi.Pointer; typedef GoChan = ffi.Pointer; final class GoInterface extends ffi.Struct { external ffi.Pointer t; external ffi.Pointer v; } final class GoSlice extends ffi.Struct { external ffi.Pointer data; @GoInt() external int len; @GoInt() external int cap; } const int __has_safe_buffers = 1; const int __DARWIN_ONLY_64_BIT_INO_T = 1; const int __DARWIN_ONLY_UNIX_CONFORMANCE = 1; const int __DARWIN_ONLY_VERS_1050 = 1; const int __DARWIN_UNIX03 = 1; const int __DARWIN_64_BIT_INO_T = 1; const int __DARWIN_VERS_1050 = 1; const int __DARWIN_NON_CANCELABLE = 0; const String __DARWIN_SUF_EXTSN = '\$DARWIN_EXTSN'; const int __DARWIN_C_ANSI = 4096; const int __DARWIN_C_FULL = 900000; const int __DARWIN_C_LEVEL = 900000; const int __STDC_WANT_LIB_EXT1__ = 1; const int __DARWIN_NO_LONG_LONG = 0; const int _DARWIN_FEATURE_64_BIT_INODE = 1; const int _DARWIN_FEATURE_ONLY_64_BIT_INODE = 1; const int _DARWIN_FEATURE_ONLY_VERS_1050 = 1; const int _DARWIN_FEATURE_ONLY_UNIX_CONFORMANCE = 1; const int _DARWIN_FEATURE_UNIX_CONFORMANCE = 3; const int __has_ptrcheck = 0; const int __DARWIN_NULL = 0; const int __PTHREAD_SIZE__ = 8176; const int __PTHREAD_ATTR_SIZE__ = 56; const int __PTHREAD_MUTEXATTR_SIZE__ = 8; const int __PTHREAD_MUTEX_SIZE__ = 56; const int __PTHREAD_CONDATTR_SIZE__ = 8; const int __PTHREAD_COND_SIZE__ = 40; const int __PTHREAD_ONCE_SIZE__ = 8; const int __PTHREAD_RWLOCK_SIZE__ = 192; const int __PTHREAD_RWLOCKATTR_SIZE__ = 16; const int __DARWIN_WCHAR_MAX = 2147483647; const int __DARWIN_WCHAR_MIN = -2147483648; const int __DARWIN_WEOF = -1; const int _FORTIFY_SOURCE = 2; const int NULL = 0; const int USER_ADDR_NULL = 0; const int __API_TO_BE_DEPRECATED = 100000; const int __API_TO_BE_DEPRECATED_MACOS = 100000; const int __API_TO_BE_DEPRECATED_IOS = 100000; const int __API_TO_BE_DEPRECATED_MACCATALYST = 100000; const int __API_TO_BE_DEPRECATED_WATCHOS = 100000; const int __API_TO_BE_DEPRECATED_TVOS = 100000; const int __API_TO_BE_DEPRECATED_DRIVERKIT = 100000; const int __API_TO_BE_DEPRECATED_VISIONOS = 100000; const int __MAC_10_0 = 1000; const int __MAC_10_1 = 1010; const int __MAC_10_2 = 1020; const int __MAC_10_3 = 1030; const int __MAC_10_4 = 1040; const int __MAC_10_5 = 1050; const int __MAC_10_6 = 1060; const int __MAC_10_7 = 1070; const int __MAC_10_8 = 1080; const int __MAC_10_9 = 1090; const int __MAC_10_10 = 101000; const int __MAC_10_10_2 = 101002; const int __MAC_10_10_3 = 101003; const int __MAC_10_11 = 101100; const int __MAC_10_11_2 = 101102; const int __MAC_10_11_3 = 101103; const int __MAC_10_11_4 = 101104; const int __MAC_10_12 = 101200; const int __MAC_10_12_1 = 101201; const int __MAC_10_12_2 = 101202; const int __MAC_10_12_4 = 101204; const int __MAC_10_13 = 101300; const int __MAC_10_13_1 = 101301; const int __MAC_10_13_2 = 101302; const int __MAC_10_13_4 = 101304; const int __MAC_10_14 = 101400; const int __MAC_10_14_1 = 101401; const int __MAC_10_14_4 = 101404; const int __MAC_10_14_5 = 101405; const int __MAC_10_14_6 = 101406; const int __MAC_10_15 = 101500; const int __MAC_10_15_1 = 101501; const int __MAC_10_15_4 = 101504; const int __MAC_10_16 = 101600; const int __MAC_11_0 = 110000; const int __MAC_11_1 = 110100; const int __MAC_11_3 = 110300; const int __MAC_11_4 = 110400; const int __MAC_11_5 = 110500; const int __MAC_11_6 = 110600; const int __MAC_12_0 = 120000; const int __MAC_12_1 = 120100; const int __MAC_12_2 = 120200; const int __MAC_12_3 = 120300; const int __MAC_12_4 = 120400; const int __MAC_12_5 = 120500; const int __MAC_12_6 = 120600; const int __MAC_12_7 = 120700; const int __MAC_13_0 = 130000; const int __MAC_13_1 = 130100; const int __MAC_13_2 = 130200; const int __MAC_13_3 = 130300; const int __MAC_13_4 = 130400; const int __MAC_13_5 = 130500; const int __MAC_13_6 = 130600; const int __MAC_14_0 = 140000; const int __MAC_14_1 = 140100; const int __MAC_14_2 = 140200; const int __MAC_14_3 = 140300; const int __MAC_14_4 = 140400; const int __MAC_14_5 = 140500; const int __MAC_15_0 = 150000; const int __MAC_15_1 = 150100; const int __MAC_15_2 = 150200; const int __IPHONE_2_0 = 20000; const int __IPHONE_2_1 = 20100; const int __IPHONE_2_2 = 20200; const int __IPHONE_3_0 = 30000; const int __IPHONE_3_1 = 30100; const int __IPHONE_3_2 = 30200; const int __IPHONE_4_0 = 40000; const int __IPHONE_4_1 = 40100; const int __IPHONE_4_2 = 40200; const int __IPHONE_4_3 = 40300; const int __IPHONE_5_0 = 50000; const int __IPHONE_5_1 = 50100; const int __IPHONE_6_0 = 60000; const int __IPHONE_6_1 = 60100; const int __IPHONE_7_0 = 70000; const int __IPHONE_7_1 = 70100; const int __IPHONE_8_0 = 80000; const int __IPHONE_8_1 = 80100; const int __IPHONE_8_2 = 80200; const int __IPHONE_8_3 = 80300; const int __IPHONE_8_4 = 80400; const int __IPHONE_9_0 = 90000; const int __IPHONE_9_1 = 90100; const int __IPHONE_9_2 = 90200; const int __IPHONE_9_3 = 90300; const int __IPHONE_10_0 = 100000; const int __IPHONE_10_1 = 100100; const int __IPHONE_10_2 = 100200; const int __IPHONE_10_3 = 100300; const int __IPHONE_11_0 = 110000; const int __IPHONE_11_1 = 110100; const int __IPHONE_11_2 = 110200; const int __IPHONE_11_3 = 110300; const int __IPHONE_11_4 = 110400; const int __IPHONE_12_0 = 120000; const int __IPHONE_12_1 = 120100; const int __IPHONE_12_2 = 120200; const int __IPHONE_12_3 = 120300; const int __IPHONE_12_4 = 120400; const int __IPHONE_13_0 = 130000; const int __IPHONE_13_1 = 130100; const int __IPHONE_13_2 = 130200; const int __IPHONE_13_3 = 130300; const int __IPHONE_13_4 = 130400; const int __IPHONE_13_5 = 130500; const int __IPHONE_13_6 = 130600; const int __IPHONE_13_7 = 130700; const int __IPHONE_14_0 = 140000; const int __IPHONE_14_1 = 140100; const int __IPHONE_14_2 = 140200; const int __IPHONE_14_3 = 140300; const int __IPHONE_14_5 = 140500; const int __IPHONE_14_4 = 140400; const int __IPHONE_14_6 = 140600; const int __IPHONE_14_7 = 140700; const int __IPHONE_14_8 = 140800; const int __IPHONE_15_0 = 150000; const int __IPHONE_15_1 = 150100; const int __IPHONE_15_2 = 150200; const int __IPHONE_15_3 = 150300; const int __IPHONE_15_4 = 150400; const int __IPHONE_15_5 = 150500; const int __IPHONE_15_6 = 150600; const int __IPHONE_15_7 = 150700; const int __IPHONE_15_8 = 150800; const int __IPHONE_16_0 = 160000; const int __IPHONE_16_1 = 160100; const int __IPHONE_16_2 = 160200; const int __IPHONE_16_3 = 160300; const int __IPHONE_16_4 = 160400; const int __IPHONE_16_5 = 160500; const int __IPHONE_16_6 = 160600; const int __IPHONE_16_7 = 160700; const int __IPHONE_17_0 = 170000; const int __IPHONE_17_1 = 170100; const int __IPHONE_17_2 = 170200; const int __IPHONE_17_3 = 170300; const int __IPHONE_17_4 = 170400; const int __IPHONE_17_5 = 170500; const int __IPHONE_18_0 = 180000; const int __IPHONE_18_1 = 180100; const int __IPHONE_18_2 = 180200; const int __WATCHOS_1_0 = 10000; const int __WATCHOS_2_0 = 20000; const int __WATCHOS_2_1 = 20100; const int __WATCHOS_2_2 = 20200; const int __WATCHOS_3_0 = 30000; const int __WATCHOS_3_1 = 30100; const int __WATCHOS_3_1_1 = 30101; const int __WATCHOS_3_2 = 30200; const int __WATCHOS_4_0 = 40000; const int __WATCHOS_4_1 = 40100; const int __WATCHOS_4_2 = 40200; const int __WATCHOS_4_3 = 40300; const int __WATCHOS_5_0 = 50000; const int __WATCHOS_5_1 = 50100; const int __WATCHOS_5_2 = 50200; const int __WATCHOS_5_3 = 50300; const int __WATCHOS_6_0 = 60000; const int __WATCHOS_6_1 = 60100; const int __WATCHOS_6_2 = 60200; const int __WATCHOS_7_0 = 70000; const int __WATCHOS_7_1 = 70100; const int __WATCHOS_7_2 = 70200; const int __WATCHOS_7_3 = 70300; const int __WATCHOS_7_4 = 70400; const int __WATCHOS_7_5 = 70500; const int __WATCHOS_7_6 = 70600; const int __WATCHOS_8_0 = 80000; const int __WATCHOS_8_1 = 80100; const int __WATCHOS_8_3 = 80300; const int __WATCHOS_8_4 = 80400; const int __WATCHOS_8_5 = 80500; const int __WATCHOS_8_6 = 80600; const int __WATCHOS_8_7 = 80700; const int __WATCHOS_8_8 = 80800; const int __WATCHOS_9_0 = 90000; const int __WATCHOS_9_1 = 90100; const int __WATCHOS_9_2 = 90200; const int __WATCHOS_9_3 = 90300; const int __WATCHOS_9_4 = 90400; const int __WATCHOS_9_5 = 90500; const int __WATCHOS_9_6 = 90600; const int __WATCHOS_10_0 = 100000; const int __WATCHOS_10_1 = 100100; const int __WATCHOS_10_2 = 100200; const int __WATCHOS_10_3 = 100300; const int __WATCHOS_10_4 = 100400; const int __WATCHOS_10_5 = 100500; const int __WATCHOS_11_0 = 110000; const int __WATCHOS_11_1 = 110100; const int __WATCHOS_11_2 = 110200; const int __TVOS_9_0 = 90000; const int __TVOS_9_1 = 90100; const int __TVOS_9_2 = 90200; const int __TVOS_10_0 = 100000; const int __TVOS_10_0_1 = 100001; const int __TVOS_10_1 = 100100; const int __TVOS_10_2 = 100200; const int __TVOS_11_0 = 110000; const int __TVOS_11_1 = 110100; const int __TVOS_11_2 = 110200; const int __TVOS_11_3 = 110300; const int __TVOS_11_4 = 110400; const int __TVOS_12_0 = 120000; const int __TVOS_12_1 = 120100; const int __TVOS_12_2 = 120200; const int __TVOS_12_3 = 120300; const int __TVOS_12_4 = 120400; const int __TVOS_13_0 = 130000; const int __TVOS_13_2 = 130200; const int __TVOS_13_3 = 130300; const int __TVOS_13_4 = 130400; const int __TVOS_14_0 = 140000; const int __TVOS_14_1 = 140100; const int __TVOS_14_2 = 140200; const int __TVOS_14_3 = 140300; const int __TVOS_14_5 = 140500; const int __TVOS_14_6 = 140600; const int __TVOS_14_7 = 140700; const int __TVOS_15_0 = 150000; const int __TVOS_15_1 = 150100; const int __TVOS_15_2 = 150200; const int __TVOS_15_3 = 150300; const int __TVOS_15_4 = 150400; const int __TVOS_15_5 = 150500; const int __TVOS_15_6 = 150600; const int __TVOS_16_0 = 160000; const int __TVOS_16_1 = 160100; const int __TVOS_16_2 = 160200; const int __TVOS_16_3 = 160300; const int __TVOS_16_4 = 160400; const int __TVOS_16_5 = 160500; const int __TVOS_16_6 = 160600; const int __TVOS_17_0 = 170000; const int __TVOS_17_1 = 170100; const int __TVOS_17_2 = 170200; const int __TVOS_17_3 = 170300; const int __TVOS_17_4 = 170400; const int __TVOS_17_5 = 170500; const int __TVOS_18_0 = 180000; const int __TVOS_18_1 = 180100; const int __TVOS_18_2 = 180200; const int __BRIDGEOS_2_0 = 20000; const int __BRIDGEOS_3_0 = 30000; const int __BRIDGEOS_3_1 = 30100; const int __BRIDGEOS_3_4 = 30400; const int __BRIDGEOS_4_0 = 40000; const int __BRIDGEOS_4_1 = 40100; const int __BRIDGEOS_5_0 = 50000; const int __BRIDGEOS_5_1 = 50100; const int __BRIDGEOS_5_3 = 50300; const int __BRIDGEOS_6_0 = 60000; const int __BRIDGEOS_6_2 = 60200; const int __BRIDGEOS_6_4 = 60400; const int __BRIDGEOS_6_5 = 60500; const int __BRIDGEOS_6_6 = 60600; const int __BRIDGEOS_7_0 = 70000; const int __BRIDGEOS_7_1 = 70100; const int __BRIDGEOS_7_2 = 70200; const int __BRIDGEOS_7_3 = 70300; const int __BRIDGEOS_7_4 = 70400; const int __BRIDGEOS_7_6 = 70600; const int __BRIDGEOS_8_0 = 80000; const int __BRIDGEOS_8_1 = 80100; const int __BRIDGEOS_8_2 = 80200; const int __BRIDGEOS_8_3 = 80300; const int __BRIDGEOS_8_4 = 80400; const int __BRIDGEOS_8_5 = 80500; const int __BRIDGEOS_9_0 = 90000; const int __BRIDGEOS_9_1 = 90100; const int __BRIDGEOS_9_2 = 90200; const int __DRIVERKIT_19_0 = 190000; const int __DRIVERKIT_20_0 = 200000; const int __DRIVERKIT_21_0 = 210000; const int __DRIVERKIT_22_0 = 220000; const int __DRIVERKIT_22_4 = 220400; const int __DRIVERKIT_22_5 = 220500; const int __DRIVERKIT_22_6 = 220600; const int __DRIVERKIT_23_0 = 230000; const int __DRIVERKIT_23_1 = 230100; const int __DRIVERKIT_23_2 = 230200; const int __DRIVERKIT_23_3 = 230300; const int __DRIVERKIT_23_4 = 230400; const int __DRIVERKIT_23_5 = 230500; const int __DRIVERKIT_24_0 = 240000; const int __DRIVERKIT_24_1 = 240100; const int __DRIVERKIT_24_2 = 240200; const int __VISIONOS_1_0 = 10000; const int __VISIONOS_1_1 = 10100; const int __VISIONOS_1_2 = 10200; const int __VISIONOS_2_0 = 20000; const int __VISIONOS_2_1 = 20100; const int __VISIONOS_2_2 = 20200; const int MAC_OS_X_VERSION_10_0 = 1000; const int MAC_OS_X_VERSION_10_1 = 1010; const int MAC_OS_X_VERSION_10_2 = 1020; const int MAC_OS_X_VERSION_10_3 = 1030; const int MAC_OS_X_VERSION_10_4 = 1040; const int MAC_OS_X_VERSION_10_5 = 1050; const int MAC_OS_X_VERSION_10_6 = 1060; const int MAC_OS_X_VERSION_10_7 = 1070; const int MAC_OS_X_VERSION_10_8 = 1080; const int MAC_OS_X_VERSION_10_9 = 1090; const int MAC_OS_X_VERSION_10_10 = 101000; const int MAC_OS_X_VERSION_10_10_2 = 101002; const int MAC_OS_X_VERSION_10_10_3 = 101003; const int MAC_OS_X_VERSION_10_11 = 101100; const int MAC_OS_X_VERSION_10_11_2 = 101102; const int MAC_OS_X_VERSION_10_11_3 = 101103; const int MAC_OS_X_VERSION_10_11_4 = 101104; const int MAC_OS_X_VERSION_10_12 = 101200; const int MAC_OS_X_VERSION_10_12_1 = 101201; const int MAC_OS_X_VERSION_10_12_2 = 101202; const int MAC_OS_X_VERSION_10_12_4 = 101204; const int MAC_OS_X_VERSION_10_13 = 101300; const int MAC_OS_X_VERSION_10_13_1 = 101301; const int MAC_OS_X_VERSION_10_13_2 = 101302; const int MAC_OS_X_VERSION_10_13_4 = 101304; const int MAC_OS_X_VERSION_10_14 = 101400; const int MAC_OS_X_VERSION_10_14_1 = 101401; const int MAC_OS_X_VERSION_10_14_4 = 101404; const int MAC_OS_X_VERSION_10_14_5 = 101405; const int MAC_OS_X_VERSION_10_14_6 = 101406; const int MAC_OS_X_VERSION_10_15 = 101500; const int MAC_OS_X_VERSION_10_15_1 = 101501; const int MAC_OS_X_VERSION_10_15_4 = 101504; const int MAC_OS_X_VERSION_10_16 = 101600; const int MAC_OS_VERSION_11_0 = 110000; const int MAC_OS_VERSION_11_1 = 110100; const int MAC_OS_VERSION_11_3 = 110300; const int MAC_OS_VERSION_11_4 = 110400; const int MAC_OS_VERSION_11_5 = 110500; const int MAC_OS_VERSION_11_6 = 110600; const int MAC_OS_VERSION_12_0 = 120000; const int MAC_OS_VERSION_12_1 = 120100; const int MAC_OS_VERSION_12_2 = 120200; const int MAC_OS_VERSION_12_3 = 120300; const int MAC_OS_VERSION_12_4 = 120400; const int MAC_OS_VERSION_12_5 = 120500; const int MAC_OS_VERSION_12_6 = 120600; const int MAC_OS_VERSION_12_7 = 120700; const int MAC_OS_VERSION_13_0 = 130000; const int MAC_OS_VERSION_13_1 = 130100; const int MAC_OS_VERSION_13_2 = 130200; const int MAC_OS_VERSION_13_3 = 130300; const int MAC_OS_VERSION_13_4 = 130400; const int MAC_OS_VERSION_13_5 = 130500; const int MAC_OS_VERSION_13_6 = 130600; const int MAC_OS_VERSION_14_0 = 140000; const int MAC_OS_VERSION_14_1 = 140100; const int MAC_OS_VERSION_14_2 = 140200; const int MAC_OS_VERSION_14_3 = 140300; const int MAC_OS_VERSION_14_4 = 140400; const int MAC_OS_VERSION_14_5 = 140500; const int MAC_OS_VERSION_15_0 = 150000; const int MAC_OS_VERSION_15_1 = 150100; const int MAC_OS_VERSION_15_2 = 150200; const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000; const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150200; const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1; const int __DARWIN_NSIG = 32; const int NSIG = 32; const int _ARM_SIGNAL_ = 1; const int SIGHUP = 1; const int SIGINT = 2; const int SIGQUIT = 3; const int SIGILL = 4; const int SIGTRAP = 5; const int SIGABRT = 6; const int SIGIOT = 6; const int SIGEMT = 7; const int SIGFPE = 8; const int SIGKILL = 9; const int SIGBUS = 10; const int SIGSEGV = 11; const int SIGSYS = 12; const int SIGPIPE = 13; const int SIGALRM = 14; const int SIGTERM = 15; const int SIGURG = 16; const int SIGSTOP = 17; const int SIGTSTP = 18; const int SIGCONT = 19; const int SIGCHLD = 20; const int SIGTTIN = 21; const int SIGTTOU = 22; const int SIGIO = 23; const int SIGXCPU = 24; const int SIGXFSZ = 25; const int SIGVTALRM = 26; const int SIGPROF = 27; const int SIGWINCH = 28; const int SIGINFO = 29; const int SIGUSR1 = 30; const int SIGUSR2 = 31; const int __DARWIN_OPAQUE_ARM_THREAD_STATE64 = 0; const int SIGEV_NONE = 0; const int SIGEV_SIGNAL = 1; const int SIGEV_THREAD = 3; const int ILL_NOOP = 0; const int ILL_ILLOPC = 1; const int ILL_ILLTRP = 2; const int ILL_PRVOPC = 3; const int ILL_ILLOPN = 4; const int ILL_ILLADR = 5; const int ILL_PRVREG = 6; const int ILL_COPROC = 7; const int ILL_BADSTK = 8; const int FPE_NOOP = 0; const int FPE_FLTDIV = 1; const int FPE_FLTOVF = 2; const int FPE_FLTUND = 3; const int FPE_FLTRES = 4; const int FPE_FLTINV = 5; const int FPE_FLTSUB = 6; const int FPE_INTDIV = 7; const int FPE_INTOVF = 8; const int SEGV_NOOP = 0; const int SEGV_MAPERR = 1; const int SEGV_ACCERR = 2; const int BUS_NOOP = 0; const int BUS_ADRALN = 1; const int BUS_ADRERR = 2; const int BUS_OBJERR = 3; const int TRAP_BRKPT = 1; const int TRAP_TRACE = 2; const int CLD_NOOP = 0; const int CLD_EXITED = 1; const int CLD_KILLED = 2; const int CLD_DUMPED = 3; const int CLD_TRAPPED = 4; const int CLD_STOPPED = 5; const int CLD_CONTINUED = 6; const int POLL_IN = 1; const int POLL_OUT = 2; const int POLL_MSG = 3; const int POLL_ERR = 4; const int POLL_PRI = 5; const int POLL_HUP = 6; const int SA_ONSTACK = 1; const int SA_RESTART = 2; const int SA_RESETHAND = 4; const int SA_NOCLDSTOP = 8; const int SA_NODEFER = 16; const int SA_NOCLDWAIT = 32; const int SA_SIGINFO = 64; const int SA_USERTRAMP = 256; const int SA_64REGSET = 512; const int SA_USERSPACE_MASK = 127; const int SIG_BLOCK = 1; const int SIG_UNBLOCK = 2; const int SIG_SETMASK = 3; const int SI_USER = 65537; const int SI_QUEUE = 65538; const int SI_TIMER = 65539; const int SI_ASYNCIO = 65540; const int SI_MESGQ = 65541; const int SS_ONSTACK = 1; const int SS_DISABLE = 4; const int MINSIGSTKSZ = 32768; const int SIGSTKSZ = 131072; const int SV_ONSTACK = 1; const int SV_INTERRUPT = 2; const int SV_RESETHAND = 4; const int SV_NODEFER = 16; const int SV_NOCLDSTOP = 8; const int SV_SIGINFO = 64; const int __WORDSIZE = 64; const int INT8_MAX = 127; const int INT16_MAX = 32767; const int INT32_MAX = 2147483647; const int INT64_MAX = 9223372036854775807; const int INT8_MIN = -128; const int INT16_MIN = -32768; const int INT32_MIN = -2147483648; const int INT64_MIN = -9223372036854775808; const int UINT8_MAX = 255; const int UINT16_MAX = 65535; const int UINT32_MAX = 4294967295; const int UINT64_MAX = -1; const int INT_LEAST8_MIN = -128; const int INT_LEAST16_MIN = -32768; const int INT_LEAST32_MIN = -2147483648; const int INT_LEAST64_MIN = -9223372036854775808; const int INT_LEAST8_MAX = 127; const int INT_LEAST16_MAX = 32767; const int INT_LEAST32_MAX = 2147483647; const int INT_LEAST64_MAX = 9223372036854775807; const int UINT_LEAST8_MAX = 255; const int UINT_LEAST16_MAX = 65535; const int UINT_LEAST32_MAX = 4294967295; const int UINT_LEAST64_MAX = -1; const int INT_FAST8_MIN = -128; const int INT_FAST16_MIN = -32768; const int INT_FAST32_MIN = -2147483648; const int INT_FAST64_MIN = -9223372036854775808; const int INT_FAST8_MAX = 127; const int INT_FAST16_MAX = 32767; const int INT_FAST32_MAX = 2147483647; const int INT_FAST64_MAX = 9223372036854775807; const int UINT_FAST8_MAX = 255; const int UINT_FAST16_MAX = 65535; const int UINT_FAST32_MAX = 4294967295; const int UINT_FAST64_MAX = -1; const int INTPTR_MAX = 9223372036854775807; const int INTPTR_MIN = -9223372036854775808; const int UINTPTR_MAX = -1; const int INTMAX_MAX = 9223372036854775807; const int UINTMAX_MAX = -1; const int INTMAX_MIN = -9223372036854775808; const int PTRDIFF_MIN = -9223372036854775808; const int PTRDIFF_MAX = 9223372036854775807; const int SIZE_MAX = -1; const int RSIZE_MAX = 9223372036854775807; const int WCHAR_MAX = 2147483647; const int WCHAR_MIN = -2147483648; const int WINT_MIN = -2147483648; const int WINT_MAX = 2147483647; const int SIG_ATOMIC_MIN = -2147483648; const int SIG_ATOMIC_MAX = 2147483647; const int PRIO_PROCESS = 0; const int PRIO_PGRP = 1; const int PRIO_USER = 2; const int PRIO_DARWIN_THREAD = 3; const int PRIO_DARWIN_PROCESS = 4; const int PRIO_MIN = -20; const int PRIO_MAX = 20; const int PRIO_DARWIN_BG = 4096; const int PRIO_DARWIN_NONUI = 4097; const int RUSAGE_SELF = 0; const int RUSAGE_CHILDREN = -1; const int RUSAGE_INFO_V0 = 0; const int RUSAGE_INFO_V1 = 1; const int RUSAGE_INFO_V2 = 2; const int RUSAGE_INFO_V3 = 3; const int RUSAGE_INFO_V4 = 4; const int RUSAGE_INFO_V5 = 5; const int RUSAGE_INFO_V6 = 6; const int RUSAGE_INFO_CURRENT = 6; const int RU_PROC_RUNS_RESLIDE = 1; const int RLIM_INFINITY = 9223372036854775807; const int RLIM_SAVED_MAX = 9223372036854775807; const int RLIM_SAVED_CUR = 9223372036854775807; const int RLIMIT_CPU = 0; const int RLIMIT_FSIZE = 1; const int RLIMIT_DATA = 2; const int RLIMIT_STACK = 3; const int RLIMIT_CORE = 4; const int RLIMIT_AS = 5; const int RLIMIT_RSS = 5; const int RLIMIT_MEMLOCK = 6; const int RLIMIT_NPROC = 7; const int RLIMIT_NOFILE = 8; const int RLIM_NLIMITS = 9; const int _RLIMIT_POSIX_FLAG = 4096; const int RLIMIT_WAKEUPS_MONITOR = 1; const int RLIMIT_CPU_USAGE_MONITOR = 2; const int RLIMIT_THREAD_CPULIMITS = 3; const int RLIMIT_FOOTPRINT_INTERVAL = 4; const int WAKEMON_ENABLE = 1; const int WAKEMON_DISABLE = 2; const int WAKEMON_GET_PARAMS = 4; const int WAKEMON_SET_DEFAULTS = 8; const int WAKEMON_MAKE_FATAL = 16; const int CPUMON_MAKE_FATAL = 4096; const int FOOTPRINT_INTERVAL_RESET = 1; const int IOPOL_TYPE_DISK = 0; const int IOPOL_TYPE_VFS_ATIME_UPDATES = 2; const int IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES = 3; const int IOPOL_TYPE_VFS_STATFS_NO_DATA_VOLUME = 4; const int IOPOL_TYPE_VFS_TRIGGER_RESOLVE = 5; const int IOPOL_TYPE_VFS_IGNORE_CONTENT_PROTECTION = 6; const int IOPOL_TYPE_VFS_IGNORE_PERMISSIONS = 7; const int IOPOL_TYPE_VFS_SKIP_MTIME_UPDATE = 8; const int IOPOL_TYPE_VFS_ALLOW_LOW_SPACE_WRITES = 9; const int IOPOL_TYPE_VFS_DISALLOW_RW_FOR_O_EVTONLY = 10; const int IOPOL_SCOPE_PROCESS = 0; const int IOPOL_SCOPE_THREAD = 1; const int IOPOL_SCOPE_DARWIN_BG = 2; const int IOPOL_DEFAULT = 0; const int IOPOL_IMPORTANT = 1; const int IOPOL_PASSIVE = 2; const int IOPOL_THROTTLE = 3; const int IOPOL_UTILITY = 4; const int IOPOL_STANDARD = 5; const int IOPOL_APPLICATION = 5; const int IOPOL_NORMAL = 1; const int IOPOL_ATIME_UPDATES_DEFAULT = 0; const int IOPOL_ATIME_UPDATES_OFF = 1; const int IOPOL_MATERIALIZE_DATALESS_FILES_DEFAULT = 0; const int IOPOL_MATERIALIZE_DATALESS_FILES_OFF = 1; const int IOPOL_MATERIALIZE_DATALESS_FILES_ON = 2; const int IOPOL_VFS_STATFS_NO_DATA_VOLUME_DEFAULT = 0; const int IOPOL_VFS_STATFS_FORCE_NO_DATA_VOLUME = 1; const int IOPOL_VFS_TRIGGER_RESOLVE_DEFAULT = 0; const int IOPOL_VFS_TRIGGER_RESOLVE_OFF = 1; const int IOPOL_VFS_CONTENT_PROTECTION_DEFAULT = 0; const int IOPOL_VFS_CONTENT_PROTECTION_IGNORE = 1; const int IOPOL_VFS_IGNORE_PERMISSIONS_OFF = 0; const int IOPOL_VFS_IGNORE_PERMISSIONS_ON = 1; const int IOPOL_VFS_SKIP_MTIME_UPDATE_OFF = 0; const int IOPOL_VFS_SKIP_MTIME_UPDATE_ON = 1; const int IOPOL_VFS_ALLOW_LOW_SPACE_WRITES_OFF = 0; const int IOPOL_VFS_ALLOW_LOW_SPACE_WRITES_ON = 1; const int IOPOL_VFS_DISALLOW_RW_FOR_O_EVTONLY_DEFAULT = 0; const int IOPOL_VFS_DISALLOW_RW_FOR_O_EVTONLY_ON = 1; const int IOPOL_VFS_NOCACHE_WRITE_FS_BLKSIZE_DEFAULT = 0; const int IOPOL_VFS_NOCACHE_WRITE_FS_BLKSIZE_ON = 1; const int WNOHANG = 1; const int WUNTRACED = 2; const int WCOREFLAG = 128; const int _WSTOPPED = 127; const int WEXITED = 4; const int WSTOPPED = 8; const int WCONTINUED = 16; const int WNOWAIT = 32; const int WAIT_ANY = -1; const int WAIT_MYPGRP = 0; const int _QUAD_HIGHWORD = 1; const int _QUAD_LOWWORD = 0; const int __DARWIN_LITTLE_ENDIAN = 1234; const int __DARWIN_BIG_ENDIAN = 4321; const int __DARWIN_PDP_ENDIAN = 3412; const int LITTLE_ENDIAN = 1234; const int BIG_ENDIAN = 4321; const int PDP_ENDIAN = 3412; const int __DARWIN_BYTE_ORDER = 1234; const int BYTE_ORDER = 1234; const int EXIT_FAILURE = 1; const int EXIT_SUCCESS = 0; const int RAND_MAX = 2147483647; ================================================ FILE: lib/clash/interface.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; import 'package:bett_box/clash/message.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; mixin ClashInterface { Future init(InitParams params); Future preload(); Future shutdown(); Future get isInit; Future forceGc(); FutureOr validateConfig(String data); FutureOr getConfig(String path); Future asyncTestDelay(String url, String proxyName); FutureOr updateConfig(UpdateParams updateParams); FutureOr setupConfig(SetupParams setupParams); FutureOr getProxies(); FutureOr changeProxy(ChangeProxyParams changeProxyParams); Future startListener(); Future stopListener(); FutureOr getExternalProviders(); FutureOr? getExternalProvider(String externalProviderName); Future updateGeoData(UpdateGeoDataParams params); Future sideLoadExternalProvider({ required String providerName, required String data, }); Future updateExternalProvider(String providerName); FutureOr getTraffic(); FutureOr getTotalTraffic(); FutureOr getCountryCode(String ip); FutureOr getMemory(); FutureOr resetTraffic(); FutureOr startLog(); FutureOr stopLog(); Future crash(); FutureOr getConnections(); FutureOr closeConnection(String id); FutureOr closeConnections(); FutureOr resetConnections(); Future setState(CoreState state); FutureOr flushFakeIP(); FutureOr flushDnsCache(); } mixin AndroidClashInterface { Future updateDns(String value); Future getAndroidVpnOptions(); Future getCurrentProfileName(); Future getRunTime(); } abstract class ClashHandlerInterface with ClashInterface { Map callbackCompleterMap = {}; Future handleResult(ActionResult result) async { final completer = callbackCompleterMap[result.id]; try { switch (result.method) { case ActionMethod.message: clashMessage.controller.add(result.data); completer?.complete(true); return; case ActionMethod.getConfig: completer?.complete(result.toResult); return; default: completer?.complete(result.data); return; } } catch (e) { commonPrint.log('${result.id} error $e'); } } void sendMessage(String message); FutureOr reStart(); FutureOr destroy(); Future invoke({ required ActionMethod method, dynamic data, Duration? timeout, FutureOr Function()? onTimeout, T? defaultValue, }) async { final id = '${method.name}#${utils.id}'; callbackCompleterMap[id] = Completer(); dynamic mDefaultValue = defaultValue; if (mDefaultValue == null) { if (T == String) { mDefaultValue = ''; } else if (T == bool) { mDefaultValue = false; } else if (T == Map) { mDefaultValue = {}; } } sendMessage(json.encode(Action(id: id, method: method, data: data))); return (callbackCompleterMap[id] as Completer).safeFuture( timeout: timeout, onLast: () { callbackCompleterMap.remove(id); }, onTimeout: onTimeout ?? () { return mDefaultValue; }, functionName: id, ); } @override Future init(InitParams params) { return invoke( method: ActionMethod.initClash, data: json.encode(params), ); } @override Future setState(CoreState state) { return invoke( method: ActionMethod.setState, data: json.encode(state), ); } @override shutdown() async { return await invoke( method: ActionMethod.shutdown, timeout: Duration(seconds: 1), ); } @override Future get isInit { return invoke(method: ActionMethod.getIsInit); } @override Future forceGc() { return invoke(method: ActionMethod.forceGc); } @override FutureOr validateConfig(String data) { return invoke(method: ActionMethod.validateConfig, data: data); } @override Future updateConfig(UpdateParams updateParams) async { return await invoke( method: ActionMethod.updateConfig, data: json.encode(updateParams), timeout: Duration(minutes: 2), ); } @override Future getConfig(String path) async { final res = await invoke( method: ActionMethod.getConfig, data: path, timeout: Duration(minutes: 2), defaultValue: Result.success({}), ); return res; } @override Future setupConfig(SetupParams setupParams) async { final data = await Isolate.run(() => json.encode(setupParams)); return await invoke( method: ActionMethod.setupConfig, data: data, timeout: Duration(minutes: 2), ); } @override Future crash() { return invoke(method: ActionMethod.crash); } @override Future getProxies() { return invoke( method: ActionMethod.getProxies, timeout: Duration(seconds: 5), ); } @override FutureOr changeProxy(ChangeProxyParams changeProxyParams) { return invoke( method: ActionMethod.changeProxy, data: json.encode(changeProxyParams), ); } @override FutureOr getExternalProviders() { return invoke(method: ActionMethod.getExternalProviders); } @override FutureOr getExternalProvider(String externalProviderName) { return invoke( method: ActionMethod.getExternalProvider, data: externalProviderName, ); } @override Future updateGeoData(UpdateGeoDataParams params) { return invoke( method: ActionMethod.updateGeoData, data: json.encode(params), timeout: Duration(minutes: 1), ); } @override Future sideLoadExternalProvider({ required String providerName, required String data, }) { return invoke( method: ActionMethod.sideLoadExternalProvider, data: json.encode({'providerName': providerName, 'data': data}), ); } @override Future updateExternalProvider(String providerName) { return invoke( method: ActionMethod.updateExternalProvider, data: providerName, timeout: Duration(minutes: 1), ); } @override FutureOr getConnections() { return invoke(method: ActionMethod.getConnections); } @override Future closeConnections() { return invoke(method: ActionMethod.closeConnections); } @override Future resetConnections() { return invoke(method: ActionMethod.resetConnections); } @override Future closeConnection(String id) { return invoke(method: ActionMethod.closeConnection, data: id); } @override FutureOr getTotalTraffic() { return invoke(method: ActionMethod.getTotalTraffic); } @override FutureOr getTraffic() { return invoke(method: ActionMethod.getTraffic); } @override resetTraffic() { invoke(method: ActionMethod.resetTraffic); } @override startLog() { invoke(method: ActionMethod.startLog); } @override stopLog() { invoke(method: ActionMethod.stopLog); } @override Future startListener() { return invoke(method: ActionMethod.startListener); } @override stopListener() { return invoke(method: ActionMethod.stopListener); } @override Future asyncTestDelay(String url, String proxyName) { final delayParams = { 'proxy-name': proxyName, 'timeout': httpTimeoutDuration.inMilliseconds, 'test-url': url, }; return invoke( method: ActionMethod.asyncTestDelay, data: json.encode(delayParams), timeout: Duration(milliseconds: 6000), onTimeout: () { return json.encode(Delay(name: proxyName, value: -1, url: url)); }, ); } @override FutureOr getCountryCode(String ip) { return invoke(method: ActionMethod.getCountryCode, data: ip); } @override FutureOr getMemory() { return invoke(method: ActionMethod.getMemory); } @override FutureOr flushFakeIP() { return invoke(method: ActionMethod.flushFakeIP); } @override FutureOr flushDnsCache() { return invoke(method: ActionMethod.flushDnsCache); } } ================================================ FILE: lib/clash/lib.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:ffi'; import 'dart:isolate'; import 'dart:ui'; import 'package:ffi/ffi.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/plugins/service.dart'; import 'package:bett_box/state.dart'; import 'generated/clash_ffi.dart'; import 'interface.dart'; class ClashLib extends ClashHandlerInterface with AndroidClashInterface { static ClashLib? _instance; Completer _canSendCompleter = Completer(); SendPort? sendPort; final receiverPort = ReceivePort(); ClashLib._internal() { _initService(); } @override preload() { return _canSendCompleter.future; } Future _initService() async { _registerMainPort(receiverPort.sendPort); receiverPort.listen((message) { if (message is SendPort) { if (_canSendCompleter.isCompleted) { sendPort = null; _canSendCompleter = Completer(); } sendPort = message; _canSendCompleter.complete(true); } else { handleResult(ActionResult.fromJson(json.decode(message))); } }); final alreadyRunning = await service?.isServiceEngineRunning() ?? false; if (alreadyRunning) { await service?.reconnectIpc(); } else { await service?.destroy(); await service?.init(); } await _waitForIpc(); } Future _waitForIpc() async { for (var attempt = 0; attempt < 3; attempt++) { final connected = await _canSendCompleter.future .timeout(const Duration(seconds: 2), onTimeout: () => false); if (connected) return; commonPrint.log('ClashLib: IPC attempt ${attempt + 1}/3 failed, retrying...'); _canSendCompleter = Completer(); await service?.reconnectIpc(); } commonPrint.log('ClashLib: IPC failed after 3 attempts'); } void _registerMainPort(SendPort sendPort) { IsolateNameServer.removePortNameMapping(mainIsolate); IsolateNameServer.registerPortWithName(sendPort, mainIsolate); } factory ClashLib() { _instance ??= ClashLib._internal(); return _instance!; } @override destroy() async { await service?.destroy(); return true; } @override reStart() { _initService(); } @override Future shutdown() async { await super.shutdown(); destroy(); return true; } @override sendMessage(String message) async { await _canSendCompleter.future; try { sendPort?.send(message); } catch (e) { commonPrint.log('ClashLib: sendMessage failed: $e, reconnecting IPC'); sendPort = null; _canSendCompleter = Completer(); await service?.reconnectIpc(); await _waitForIpc(); sendPort?.send(message); } } // @override // Future stopTun() { // return invoke( // method: ActionMethod.stopTun, // ); // } @override Future getAndroidVpnOptions() async { final res = await invoke(method: ActionMethod.getAndroidVpnOptions); if (res.isEmpty) return null; return AndroidVpnOptions.fromJson(json.decode(res)); } @override Future updateDns(String value) { return invoke(method: ActionMethod.updateDns, data: value); } @override Future getRunTime() async { final runTimeString = await invoke(method: ActionMethod.getRunTime); if (runTimeString.isEmpty) return null; return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); } @override Future getCurrentProfileName() { return invoke(method: ActionMethod.getCurrentProfileName); } } class ClashLibHandler { static ClashLibHandler? _instance; late final ClashFFI clashFFI; late final DynamicLibrary lib; ClashLibHandler._internal() { lib = DynamicLibrary.open('libclash.so'); clashFFI = ClashFFI(lib); clashFFI.initNativeApiBridge(NativeApi.initializeApiDLData); } factory ClashLibHandler() { _instance ??= ClashLibHandler._internal(); return _instance!; } Future invokeAction(String actionParams) { final completer = Completer(); final receiver = ReceivePort(); receiver.listen((message) { if (!completer.isCompleted) { completer.complete(message); receiver.close(); } }); final actionParamsChar = actionParams.toNativeUtf8().cast(); clashFFI.invokeAction(actionParamsChar, receiver.sendPort.nativePort); malloc.free(actionParamsChar); return completer.future; } void attachMessagePort(int messagePort) { clashFFI.attachMessagePort(messagePort); } void updateDns(String dns) { final dnsChar = dns.toNativeUtf8().cast(); clashFFI.updateDns(dnsChar); malloc.free(dnsChar); } void setState(CoreState state) { final stateChar = json.encode(state).toNativeUtf8().cast(); clashFFI.setState(stateChar); malloc.free(stateChar); } String getCurrentProfileName() { final currentProfileRaw = clashFFI.getCurrentProfileName(); final currentProfile = currentProfileRaw.cast().toDartString(); clashFFI.freeCString(currentProfileRaw); return currentProfile; } AndroidVpnOptions getAndroidVpnOptions() { final vpnOptionsRaw = clashFFI.getAndroidVpnOptions(); final vpnOptions = json.decode(vpnOptionsRaw.cast().toDartString()); clashFFI.freeCString(vpnOptionsRaw); return AndroidVpnOptions.fromJson(vpnOptions); } Traffic getTraffic() { final trafficRaw = clashFFI.getTraffic(); final trafficString = trafficRaw.cast().toDartString(); clashFFI.freeCString(trafficRaw); if (trafficString.isEmpty) return Traffic(); return Traffic.fromMap(json.decode(trafficString)); } Traffic getTotalTraffic(bool value) { final trafficRaw = clashFFI.getTotalTraffic(); final trafficString = trafficRaw.cast().toDartString(); clashFFI.freeCString(trafficRaw); if (trafficString.isEmpty) return Traffic(); return Traffic.fromMap(json.decode(trafficString)); } Future startListener() async { clashFFI.startListener(); return true; } Future stopListener() async { clashFFI.stopListener(); return true; } DateTime? getRunTime() { final runTimeRaw = clashFFI.getRunTime(); final runTimeString = runTimeRaw.cast().toDartString(); clashFFI.freeCString(runTimeRaw); if (runTimeString.isEmpty) { return null; } return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); } Future> getConfig(String id) async { final path = await appPath.getProfilePath(id); final pathChar = path.toNativeUtf8().cast(); final configRaw = clashFFI.getConfig(pathChar); final configString = configRaw.cast().toDartString(); malloc.free(pathChar); clashFFI.freeCString(configRaw); if (configString.isEmpty) return {}; return json.decode(configString); } Future quickStart( InitParams initParams, SetupParams setupParams, CoreState state, ) { final completer = Completer(); final receiver = ReceivePort(); receiver.listen((message) { if (!completer.isCompleted) { completer.complete(message); receiver.close(); } }); final params = json.encode(setupParams); final initValue = json.encode(initParams); final stateParams = json.encode(state); final initParamsChar = initValue.toNativeUtf8().cast(); final paramsChar = params.toNativeUtf8().cast(); final stateParamsChar = stateParams.toNativeUtf8().cast(); clashFFI.quickStart( initParamsChar, paramsChar, stateParamsChar, receiver.sendPort.nativePort, ); malloc.free(initParamsChar); malloc.free(paramsChar); malloc.free(stateParamsChar); return completer.future; } } ClashLib? get clashLib => system.isAndroid && !globalState.isService ? ClashLib() : null; ClashLibHandler? get clashLibHandler => system.isAndroid && globalState.isService ? ClashLibHandler() : null; ================================================ FILE: lib/clash/message.dart ================================================ import 'dart:async'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:flutter/foundation.dart'; class ClashMessage { final controller = StreamController>.broadcast(); ClashMessage._() { controller.stream.listen((message) { if (message.isEmpty) return; final m = AppMessage.fromJson(message); for (final AppMessageListener listener in _listeners) { switch (m.type) { case AppMessageType.log: listener.onLog(Log.fromJson(m.data)); case AppMessageType.delay: listener.onDelay(Delay.fromJson(m.data)); case AppMessageType.request: listener.onRequest(TrackerInfo.fromJson(m.data)); case AppMessageType.loaded: listener.onLoaded(m.data); } } }); } static final ClashMessage instance = ClashMessage._(); final ObserverList _listeners = ObserverList(); bool get hasListeners { return _listeners.isNotEmpty; } void addListener(AppMessageListener listener) { _listeners.add(listener); } void removeListener(AppMessageListener listener) { _listeners.remove(listener); } } final clashMessage = ClashMessage.instance; ================================================ FILE: lib/clash/service.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:bett_box/clash/interface.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/models/core.dart'; import 'package:bett_box/state.dart'; class ClashService extends ClashHandlerInterface { static ClashService? _instance; Completer serverCompleter = Completer(); Completer socketCompleter = Completer(); bool isStarting = false; bool _isDestroying = false; Process? process; factory ClashService() { _instance ??= ClashService._internal(); return _instance!; } ClashService._internal() { _initServer(); reStart(); } Future _initServer() async { runZonedGuarded( () async { final address = !system.isWindows ? InternetAddress(unixSocketPath, type: InternetAddressType.unix) : InternetAddress(localhost, type: InternetAddressType.IPv4); await _deleteSocketFile(); final server = await ServerSocket.bind(address, 0, shared: true); serverCompleter.complete(server); await for (final socket in server) { await _destroySocket(); socketCompleter.complete(socket); socket .transform(uint8ListToListIntConverter) .transform(utf8.decoder) .transform(LineSplitter()) .listen((data) { handleResult(ActionResult.fromJson(json.decode(data.trim()))); }); } }, (error, stack) { commonPrint.log(error.toString()); if (error is SocketException && !_isDestroying && !globalState.isExiting) { globalState.showNotifier(error.toString()); // globalState.appController.restartCore(); } }, ); } @override reStart() async { if (isStarting) return; isStarting = true; _isDestroying = false; await _destroySocket(); process?.kill(); for (var i = 0; i < 5; i++) { if (process == null) break; try { process!.exitCode; break; } on StateError { await Future.delayed(const Duration(milliseconds: 100)); } } process = null; socketCompleter = Completer(); final serverSocket = await serverCompleter.future; final arg = system.isWindows ? '${serverSocket.port}' : serverSocket.address.address; if (system.isWindows) { final serviceOk = await windows?.registerService() ?? false; if (serviceOk) { final isSuccess = await request.startCoreByHelper(arg); if (isSuccess) { await _waitForCoreReady(); isStarting = false; return; } } } final homeDirPath = await appPath.homeDirPath; final environment = Map.from(Platform.environment); environment['SAFE_PATHS'] = homeDirPath; process = await Process.start(appPath.corePath, [ arg, ], environment: environment); process?.stdout.listen((_) {}); process?.stderr.listen((e) { final error = utf8.decode(e); if (error.isNotEmpty) commonPrint.log(error); }); await _waitForCoreReady(); isStarting = false; } Future _waitForCoreReady() async { const maxAttempts = 10; const interval = Duration(milliseconds: 500); for (var attempt = 1; attempt <= maxAttempts; attempt++) { if (socketCompleter.isCompleted) return; await Future.delayed(interval); } commonPrint.log( 'Core ready timeout after ${maxAttempts * interval.inMilliseconds}ms', ); } @override destroy() async { _isDestroying = true; final server = await serverCompleter.future; await server.close(); await _deleteSocketFile(); return true; } @override sendMessage(String message) async { if (_isDestroying || globalState.isExiting) { return; } final socket = await socketCompleter.future; try { socket.writeln(message); } on SocketException catch (e) { if (_isDestroying || globalState.isExiting) { commonPrint.log('Ignore socket error during shutdown: $e'); return; } rethrow; } } Future _deleteSocketFile() async { if (!system.isWindows) { final file = File(unixSocketPath); if (await file.exists()) { await file.delete(); } } } Future _destroySocket() async { if (socketCompleter.isCompleted) { final lastSocket = await socketCompleter.future; await lastSocket.close(); socketCompleter = Completer(); } } @override shutdown() async { _isDestroying = true; if (system.isWindows) { await request.stopCoreByHelper(); } await _destroySocket(); process?.kill(); process = null; return true; } @override Future preload() async { await serverCompleter.future; return true; } } final clashService = system.isDesktop ? ClashService() : null; ================================================ FILE: lib/common/android.dart ================================================ import 'package:bett_box/plugins/app.dart'; import 'package:bett_box/state.dart'; import 'system.dart'; class Android { Future init() async { app.onExit = () async { await globalState.appController.savePreferences(); }; } } final android = system.isAndroid ? Android() : null; ================================================ FILE: lib/common/app_localizations.dart ================================================ import 'package:bett_box/l10n/l10n.dart'; final appLocalizations = AppLocalizations.current; ================================================ FILE: lib/common/archive.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:archive/archive_io.dart'; import 'package:path/path.dart'; extension ArchiveExt on Archive { Future addDirectoryToArchive(String dirPath, String parentPath) async { final dir = Directory(dirPath); final entities = await dir.list(recursive: false).toList(); for (final entity in entities) { final relativePath = relative(entity.path, from: parentPath).replaceAll('\\', '/'); if (entity is File) { final data = await entity.readAsBytes(); final archiveFile = ArchiveFile(relativePath, data.length, data); addFile(archiveFile); } else if (entity is Directory) { await addDirectoryToArchive(entity.path, parentPath); } } } void add(String name, T raw) { final data = json.encode(raw); addFile(ArchiveFile(name, data.length, utf8.encode(data))); } } ================================================ FILE: lib/common/color.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; extension ColorExtension on Color { Color get opacity80 { return withAlpha(204); } Color get opacity60 { return withAlpha(153); } Color get opacity50 { return withAlpha(128); } Color get opacity38 { return withAlpha(97); } Color get opacity30 { return withAlpha(77); } Color get opacity12 { return withAlpha(31); } Color get opacity15 { return withAlpha(38); } Color get opacity10 { return withAlpha(15); } Color get opacity3 { return withAlpha(76); } Color get opacity0 { return withAlpha(0); } int get value32bit { return _floatToInt8(a) << 24 | _floatToInt8(r) << 16 | _floatToInt8(g) << 8 | _floatToInt8(b) << 0; } int get alpha8bit => (0xff000000 & value32bit) >> 24; int get red8bit => (0x00ff0000 & value32bit) >> 16; int get green8bit => (0x0000ff00 & value32bit) >> 8; int get blue8bit => (0x000000ff & value32bit) >> 0; int _floatToInt8(double x) { return (x * 255.0).round() & 0xff; } Color lighten([double amount = 10]) { if (amount <= 0) return this; if (amount > 100) return Colors.white; final HSLColor hsl = this == const Color(0xFF000000) ? HSLColor.fromColor(this).withSaturation(0) : HSLColor.fromColor(this); return hsl .withLightness(min(1, max(0, hsl.lightness + amount / 100))) .toColor(); } String get hex { final value = toARGB32(); final red = (value >> 16) & 0xFF; final green = (value >> 8) & 0xFF; final blue = value & 0xFF; return '#${red.toRadixString(16).padLeft(2, '0')}' '${green.toRadixString(16).padLeft(2, '0')}' '${blue.toRadixString(16).padLeft(2, '0')}' .toUpperCase(); } Color darken([final int amount = 10]) { if (amount <= 0) return this; if (amount > 100) return Colors.black; final HSLColor hsl = HSLColor.fromColor(this); return hsl .withLightness(min(1, max(0, hsl.lightness - amount / 100))) .toColor(); } Color blendDarken(BuildContext context, {double factor = 0.1}) { final brightness = Theme.of(context).brightness; return Color.lerp( this, brightness == Brightness.dark ? Colors.white : Colors.black, factor, )!; } Color blendLighten(BuildContext context, {double factor = 0.1}) { final brightness = Theme.of(context).brightness; return Color.lerp( this, brightness == Brightness.dark ? Colors.black : Colors.white, factor, )!; } } extension ColorSchemeExtension on ColorScheme { ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack ? copyWith( surface: Colors.black, surfaceContainer: surfaceContainer.darken(5), ) : this; } ================================================ FILE: lib/common/common.dart ================================================ export 'android.dart'; export 'app_localizations.dart'; export 'color.dart'; export 'constant.dart'; export 'context.dart'; export 'converter.dart'; export 'datetime.dart'; export 'fixed.dart'; export 'function.dart'; export 'future.dart'; export 'http.dart'; export 'icons.dart'; export 'iterable.dart'; export 'js_runtime_manager.dart'; export 'keyboard.dart'; export 'launch.dart'; export 'link.dart'; export 'lock.dart'; export 'measure.dart'; export 'mixin.dart'; export 'navigation.dart'; export 'navigator.dart'; export 'network.dart'; export 'num.dart'; export 'package.dart'; export 'path.dart'; export 'picker.dart'; export 'preferences.dart'; export 'print.dart'; export 'protocol.dart'; export 'proxy.dart'; export 'render.dart'; export 'request.dart'; export 'scroll.dart'; export 'string.dart'; export 'system.dart'; export 'task.dart'; export 'text.dart'; export 'tray.dart'; export 'ui_manager.dart'; export 'utils.dart'; export 'window.dart'; ================================================ FILE: lib/common/constant.dart ================================================ import 'dart:math'; import 'dart:ui'; import 'package:collection/collection.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:flutter/material.dart'; const appName = 'Bettbox'; const appHelperService = 'BettboxHelperService'; const coreName = 'clash.meta'; const tunDeviceName = 'Bettbox'; const browserUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36'; const packageName = 'com.appshub.bettbox'; final unixSocketPath = '/tmp/BettboxSocket_${Random().nextInt(10000)}.sock'; const helperPort = 45678; const maxTextScale = 1.4; const minTextScale = 0.8; final baseInfoEdgeInsets = EdgeInsets.symmetric( vertical: 16.ap, horizontal: 16.ap, ); final defaultTextScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor; const httpTimeoutDuration = Duration(milliseconds: 5000); const moreDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100); const midDuration = Duration(milliseconds: 200); const commonDuration = Duration(milliseconds: 300); const defaultUpdateDuration = Duration(days: 1); const mmdbFileName = 'geoip.metadb'; const asnFileName = 'ASN.mmdb'; const geoIpFileName = 'GeoIP.dat'; const geoSiteFileName = 'GeoSite.dat'; final double kHeaderHeight = system.isDesktop ? !system.isMacOS ? 40 : 28 : 0; const profilesDirectoryName = 'profiles'; const localhost = '127.0.0.1'; const clashConfigKey = 'clash_config'; const configKey = 'config'; const customSidebarIconKey = 'custom_sidebar_icon'; const customDashboardTitleKey = 'custom_dashboard_title'; const double dialogCommonWidth = 300; const repository = 'appshubcc/Bettbox'; const defaultExternalController = '127.0.0.1:9090'; const maxMobileWidth = 600; const maxLaptopWidth = 840; const defaultTestUrl = 'https://g.cn/generate_204'; // Preset test URLs const presetTestUrls = [ 'https://g.cn/generate_204', 'https://www.gstatic.com/generate_204', 'https://www.google.com/generate_204', 'https://cp.cloudflare.com/generate_204', 'https://www.apple.com/library/test/success.html', ]; // Preset NTP servers const defaultNtpServer = 'ntp.aliyun.com'; const presetNtpServers = [ 'ntp.aliyun.com', 'time.apple.com', 'ntp.tencent.com', 'time.windows.com', 'time.cloudflare.com', ]; class CommonFilters { static final ImageFilter blur = ImageFilter.blur( sigmaX: 2, sigmaY: 2, tileMode: TileMode.mirror, ); } final commonFilter = CommonFilters.blur; const navigationItemListEquality = ListEquality(); const trackerInfoListEquality = ListEquality(); const stringListEquality = ListEquality(); const intListEquality = ListEquality(); const logListEquality = ListEquality(); const groupListEquality = ListEquality(); const externalProviderListEquality = ListEquality(); const packageListEquality = ListEquality(); const hotKeyActionListEquality = ListEquality(); const stringAndStringMapEquality = MapEquality(); const stringAndStringMapEntryIterableEquality = IterableEquality>(); const delayMapEquality = MapEquality>(); const stringSetEquality = SetEquality(); const keyboardModifierListEquality = SetEquality(); const viewModeColumnsMap = { ViewMode.mobile: [2, 1], ViewMode.laptop: [3, 2], ViewMode.desktop: [4, 3], }; const proxiesListStoreKey = PageStorageKey('proxies_list'); const toolsStoreKey = PageStorageKey('tools'); const profilesStoreKey = PageStorageKey('profiles'); const defaultPrimaryColor = 0xFF00796B; double getWidgetHeight(num lines) { return max(lines * 84 + (lines - 1) * 16, 0).ap; } const maxLength = 256; final mainIsolate = 'BettboxMainIsolate'; final serviceIsolate = 'BettboxServiceIsolate'; const defaultPrimaryColors = [ 0xFF191919, 0xFF1976D2, defaultPrimaryColor, 0xFFE91E63, 0xFF7B1FA2, 0xFFD97706, 0xFF455A64, ]; const scriptTemplate = ''' const main = (config) => { return config; }'''; ================================================ FILE: lib/common/context.dart ================================================ import 'package:bett_box/manager/message_manager.dart'; import 'package:bett_box/widgets/scaffold.dart'; import 'package:flutter/material.dart'; extension BuildContextExtension on BuildContext { CommonScaffoldState? get commonScaffoldState { return findAncestorStateOfType(); } Future? showNotifier(String text, {VoidCallback? onAction, String? actionLabel, bool showCountdown = false}) { return findAncestorStateOfType() ?.message(text, onAction: onAction, actionLabel: actionLabel, showCountdown: showCountdown); } void showSnackBar(String message, {SnackBarAction? action}) { final width = viewWidth; EdgeInsets margin; if (width < 600) { margin = const EdgeInsets.only(bottom: 16, right: 16, left: 16); } else { margin = EdgeInsets.only(bottom: 16, left: 16, right: width - 316); } ScaffoldMessenger.of(this).showSnackBar( SnackBar( action: action, content: Text(message), behavior: SnackBarBehavior.floating, duration: const Duration(milliseconds: 1500), margin: margin, ), ); } Size get appSize { return MediaQuery.of(this).size; } double get viewWidth { return appSize.width; } ColorScheme get colorScheme => Theme.of(this).colorScheme; TextTheme get textTheme => Theme.of(this).textTheme; T? findLastStateOfType() { T? state; visitor(Element element) { if (!element.mounted) { return; } if (element is StatefulElement) { if (element.state is T) { state = element.state as T; } } element.visitChildren(visitor); } visitor(this as Element); return state; } } class BackHandleInherited extends InheritedWidget { final Function handleBack; const BackHandleInherited({ super.key, required this.handleBack, required super.child, }); static BackHandleInherited? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType(); @override bool updateShouldNotify(BackHandleInherited oldWidget) { return handleBack != oldWidget.handleBack; } } ================================================ FILE: lib/common/converter.dart ================================================ import 'dart:convert'; import 'dart:typed_data'; class Uint8ListToListIntConverter extends Converter> { @override List convert(Uint8List input) { return input.toList(); } @override Sink startChunkedConversion(Sink> sink) { return _Uint8ListToListIntConverterSink(sink); } } class _Uint8ListToListIntConverterSink implements Sink { const _Uint8ListToListIntConverterSink(this._target); final Sink> _target; @override void add(Uint8List data) { _target.add(data.toList()); } @override void close() { _target.close(); } } final uint8ListToListIntConverter = Uint8ListToListIntConverter(); ================================================ FILE: lib/common/datetime.dart ================================================ import 'package:bett_box/common/app_localizations.dart'; extension DateTimeExtension on DateTime { bool get isBeforeNow { return isBefore(DateTime.now()); } bool isBeforeSecure(DateTime? dateTime) { if (dateTime == null) { return false; } return true; } String get lastUpdateTimeDesc { final currentDateTime = DateTime.now(); final difference = currentDateTime.difference(this); final days = difference.inDays; if (days >= 365) { return '${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}'; } if (days >= 30) { return '${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}'; } if (days >= 1) { return '$days ${appLocalizations.days}${appLocalizations.ago}'; } final hours = difference.inHours; if (hours >= 1) { return '$hours ${appLocalizations.hours}${appLocalizations.ago}'; } final minutes = difference.inMinutes; if (minutes >= 1) { return '$minutes ${appLocalizations.minutes}${appLocalizations.ago}'; } return appLocalizations.just; } String get show { return toLocal().toString().substring(0, 10); } String get showFull { return toLocal().toString().substring(0, 19); } String get showTime { return toLocal().toString().substring(10, 19); } } ================================================ FILE: lib/common/dav_client.dart ================================================ import 'dart:async'; import 'dart:typed_data'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/models/models.dart'; import 'package:webdav_client/webdav_client.dart'; class DAVClient { late Client client; Completer pingCompleter = Completer(); late String fileName; DAVClient(DAV dav) { client = newClient(dav.uri, user: dav.user, password: dav.password); fileName = dav.fileName; client.setHeaders({'accept-charset': 'utf-8', 'Content-Type': 'text/xml'}); // 增加超时时间以适应慢速网络 client.setConnectTimeout(15000); // 15秒连接超时 client.setSendTimeout(120000); // 2分钟发送超时(大文件) client.setReceiveTimeout(120000); // 2分钟接收超时(大文件) pingCompleter.complete(_ping()); } Future _ping() async { try { await client.ping(); commonPrint.log('WebDAV ping successful'); return true; } catch (e) { commonPrint.log('WebDAV ping failed: $e'); return false; } } String get root => '/$appName'; String get backupFile => '$root/$fileName'; /// 备份数据到 WebDAV(带重试机制) Future backup(Uint8List data) async { return await _retryOperation(() async { commonPrint.log( 'WebDAV backup: uploading ${data.length} bytes to $backupFile', ); // 确保目录存在 try { await client.mkdir(root); } catch (e) { // 目录可能已存在,忽略错误 commonPrint.log('WebDAV mkdir warning (may already exist): $e'); } // 上传文件 await client.write(backupFile, data); commonPrint.log('WebDAV backup successful'); return true; }, operationName: 'backup'); } /// 从 WebDAV 恢复数据(带重试机制) Future> recovery() async { return await _retryOperation(() async { commonPrint.log('WebDAV recovery: downloading from $backupFile'); // 确保目录存在 try { await client.mkdir(root); } catch (e) { commonPrint.log('WebDAV mkdir warning: $e'); } // 下载文件 final data = await client.read(backupFile); commonPrint.log('WebDAV recovery successful: ${data.length} bytes'); return data; }, operationName: 'recovery'); } /// 重试机制:最多重试3次,指数退避 Future _retryOperation( Future Function() operation, { required String operationName, int maxAttempts = 3, }) async { int attempt = 0; Duration delay = const Duration(seconds: 2); while (attempt < maxAttempts) { attempt++; try { return await operation(); } catch (e) { final isLastAttempt = attempt >= maxAttempts; if (isLastAttempt) { // 最后一次尝试失败,抛出详细错误 commonPrint.log( 'WebDAV $operationName failed after $maxAttempts attempts: $e', ); throw 'WebDAV $operationName failed: ${_formatError(e)}'; } // 非最后一次尝试,等待后重试 commonPrint.log( 'WebDAV $operationName attempt $attempt failed: $e, retrying in ${delay.inSeconds}s...', ); await Future.delayed(delay); // 指数退避:2s -> 4s -> 8s delay *= 2; } } throw 'WebDAV $operationName failed: unexpected error'; } /// 格式化错误信息,提供更友好的提示 String _formatError(dynamic error) { final errorStr = error.toString(); // 常见错误的友好提示 if (errorStr.contains('SocketException') || errorStr.contains('Connection')) { return 'Network connection failed. Please check your internet connection and WebDAV server address.'; } if (errorStr.contains('401') || errorStr.contains('Unauthorized')) { return 'Authentication failed. Please check your username and password.'; } if (errorStr.contains('403') || errorStr.contains('Forbidden')) { return 'Access denied. Please check your account permissions.'; } if (errorStr.contains('404') || errorStr.contains('Not Found')) { return 'Backup file not found on server.'; } if (errorStr.contains('timeout') || errorStr.contains('Timeout')) { return 'Operation timed out. Please check your network connection or try again later.'; } if (errorStr.contains('507') || errorStr.contains('Insufficient Storage')) { return 'Server storage is full. Please free up space on your WebDAV server.'; } // 返回原始错误(如果无法识别) return errorStr; } } ================================================ FILE: lib/common/fixed.dart ================================================ import 'iterable.dart'; typedef ValueCallback = T Function(); class FixedList { final int maxLength; final List _list; FixedList(this.maxLength, {List? list}) : _list = (list ?? [])..truncate(maxLength); void add(T item) { _list.add(item); _list.truncate(maxLength); } void clear() { _list.clear(); } List get list => List.unmodifiable(_list); int get length => _list.length; T operator [](int index) => _list[index]; FixedList copyWith() { return FixedList(maxLength, list: _list); } } class FixedMap { int maxLength; late Map _map; FixedMap(this.maxLength, {Map? map}) { _map = map ?? {}; } V updateCacheValue(K key, ValueCallback callback) { final realValue = _map.updateCacheValue(key, callback); _adjustMap(); return realValue; } void clear() { _map.clear(); } void updateMaxLength(int size) { maxLength = size; _adjustMap(); } void updateMap(Map map) { _map = map; _adjustMap(); } void _adjustMap() { if (_map.length > maxLength) { _map = Map.fromEntries(map.entries.toList()..truncate(maxLength)); } } V? get(K key) => _map[key]; bool containsKey(K key) => _map.containsKey(key); int get length => _map.length; Map get map => Map.unmodifiable(_map); } ================================================ FILE: lib/common/flclash_database_extractor.dart ================================================ import 'dart:convert'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; /// FlClash 数据库提取工具 class FlClashDatabaseExtractor { /// 从 FlClash 数据库文件提取 Profiles static Future> extractProfiles(String dbPath) async { Database? db; try { if (system.isDesktop) { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; } db = await openDatabase(dbPath, readOnly: true, singleInstance: false); final List> results = await db.query('profiles'); final profiles = []; for (final row in results) { try { profiles.add(_convertRowToProfile(row)); } catch (e) { commonPrint.log('Failed to convert profile row: $e, row=$row'); } } return profiles; } catch (e) { commonPrint.log('Failed to extract profiles from database: $e'); rethrow; } finally { await db?.close(); } } /// 将数据库行转换为 Profile 对象 static Profile _convertRowToProfile(Map row) { final id = row['id'].toString(); final label = (row['label'] as String?) ?? 'Subscription $id'; final url = (row['url'] as String?) ?? ''; final currentGroupName = row['current_group_name'] as String?; // 解析更新时间 (Drift 默认存储为秒) final lastUpdateDateSecs = row['last_update_date']; DateTime? lastUpdateDate; if (lastUpdateDateSecs != null) { final secs = lastUpdateDateSecs is int ? lastUpdateDateSecs : int.tryParse(lastUpdateDateSecs.toString()); if (secs != null) { lastUpdateDate = DateTime.fromMillisecondsSinceEpoch(secs * 1000); } } final autoUpdateRaw = row['auto_update']; final autoUpdate = autoUpdateRaw == 1 || autoUpdateRaw == true; final autoUpdateDurationMillis = (row['auto_update_duration_millis'] as int?) ?? 0; final autoUpdateDuration = Duration(milliseconds: autoUpdateDurationMillis); final subscriptionInfo = _parseSubscriptionInfo(row['subscription_info']); final selectedMap = _parseSelectedMap(row['selected_map']); final unfoldSet = _parseUnfoldSet(row['unfold_set']); final overwriteTypeStr = row['overwrite_type'] as String?; final overwriteType = _parseOverwriteType(overwriteTypeStr); commonPrint.log('Converted profile: id=$id, label=$label'); return Profile( id: id, label: label, url: url, currentGroupName: currentGroupName, lastUpdateDate: lastUpdateDate, autoUpdate: autoUpdate, autoUpdateDuration: autoUpdateDuration, subscriptionInfo: subscriptionInfo, selectedMap: selectedMap, unfoldSet: unfoldSet, overrideData: OverrideData( enable: false, rule: OverrideRule(type: overwriteType), ), ); } static SubscriptionInfo? _parseSubscriptionInfo(dynamic jsonStr) { if (jsonStr == null || jsonStr is! String || jsonStr.isEmpty) return null; try { return SubscriptionInfo.fromJson(json.decode(jsonStr) as Map); } catch (e) { commonPrint.log('Failed to parse subscription_info: $e'); return null; } } static Map _parseSelectedMap(dynamic jsonStr) { if (jsonStr == null || jsonStr is! String || jsonStr.isEmpty) return {}; try { return Map.from(json.decode(jsonStr) as Map); } catch (e) { commonPrint.log('Failed to parse selected_map: $e'); return {}; } } static Set _parseUnfoldSet(dynamic jsonStr) { if (jsonStr == null || jsonStr is! String || jsonStr.isEmpty) return {}; try { return Set.from(json.decode(jsonStr) as List); } catch (e) { commonPrint.log('Failed to parse unfold_set: $e'); return {}; } } static OverrideRuleType _parseOverwriteType(String? typeStr) { return typeStr == 'script' ? OverrideRuleType.override : OverrideRuleType.added; } } ================================================ FILE: lib/common/function.dart ================================================ import 'dart:async'; import 'package:bett_box/enum/enum.dart'; class Debouncer { final Map _operations = {}; void call( FunctionTag tag, Function func, { List? args, Duration duration = const Duration(milliseconds: 600), }) { final timer = _operations[tag]; if (timer != null) { timer.cancel(); } _operations[tag] = Timer(duration, () { _operations[tag]?.cancel(); _operations.remove(tag); Function.apply(func, args); }); } void cancel(dynamic tag) { _operations[tag]?.cancel(); _operations[tag] = null; } } class Throttler { final Map _operations = {}; bool call( FunctionTag tag, Function func, { List? args, Duration duration = const Duration(milliseconds: 600), }) { final timer = _operations[tag]; if (timer != null) { return true; } _operations[tag] = Timer(duration, () { _operations[tag]?.cancel(); _operations.remove(tag); Function.apply(func, args); }); return false; } void cancel(dynamic tag) { _operations[tag]?.cancel(); _operations[tag] = null; } } Future retry({ required Future Function() task, int maxAttempts = 3, required bool Function(T res) retryIf, Duration delay = Duration.zero, }) async { int attempts = 0; while (attempts < maxAttempts) { final res = await task(); if (!retryIf(res) || attempts >= maxAttempts) { return res; } attempts++; } throw 'unknown error'; } final debouncer = Debouncer(); final throttler = Throttler(); ================================================ FILE: lib/common/future.dart ================================================ import 'dart:async'; import 'dart:ui'; import 'package:bett_box/common/common.dart'; extension CompleterExt on Completer { Future safeFuture({ Duration? timeout, VoidCallback? onLast, FutureOr Function()? onTimeout, required String functionName, }) { final realTimeout = timeout ?? const Duration(seconds: 30); Timer(realTimeout + commonDuration, () { if (onLast != null) { onLast(); } }); return future.withTimeout( timeout: realTimeout, functionName: functionName, onTimeout: onTimeout, ); } } extension FutureExt on Future { Future withTimeout({ required Duration timeout, required String functionName, FutureOr Function()? onTimeout, }) { return this.timeout( timeout, onTimeout: () async { if (onTimeout != null) { return onTimeout(); } else { throw TimeoutException('$functionName timeout'); } }, ); } } ================================================ FILE: lib/common/helper_auth.dart ================================================ import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:ffi/ffi.dart'; import 'package:win32/win32.dart'; import 'path.dart'; const _cryptProtectUiForbidden = 0x1; class HelperAuthManager { static String? _authKey; static String? getAuthKey() => _authKey; static Future ensureAuthKey() async { if (_authKey != null) { return false; } final file = File(await appPath.helperAuthKeyPath); final existingKey = await _readPersistedAuthKey(file); if (existingKey != null) { _authKey = existingKey; return false; } _authKey = _generateRandomKey(); await _persistAuthKey(file, _authKey!); return true; } static Map generateAuthHeaders(String body) { if (_authKey == null) { return {}; } final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; final message = '$timestamp:$body'; final keyBytes = _hexToBytes(_authKey!); final messageBytes = utf8.encode(message); final hmacSha256 = Hmac(sha256, keyBytes); final digest = hmacSha256.convert(messageBytes); final signature = digest.toString(); return {'X-Timestamp': timestamp.toString(), 'X-Signature': signature}; } static Future clearAuthKey() async { _authKey = null; final file = File(await appPath.helperAuthKeyPath); if (await file.exists()) { await file.delete(); } } static String _generateRandomKey() { final random = Random.secure(); final bytes = List.generate(32, (_) => random.nextInt(256)); return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } static Future _readPersistedAuthKey(File file) async { if (!await file.exists()) { return null; } try { final encoded = await file.readAsString(); if (encoded.isEmpty) { return null; } final encrypted = base64Decode(encoded); final decrypted = Platform.isWindows ? _unprotectForCurrentUser(Uint8List.fromList(encrypted)) : Uint8List.fromList(encrypted); final key = utf8.decode(decrypted); if (!_isValidHexKey(key)) { return null; } return key; } catch (_) { return null; } } static Future _persistAuthKey(File file, String key) async { final raw = Uint8List.fromList(utf8.encode(key)); final encrypted = Platform.isWindows ? _protectForCurrentUser(raw) : raw; await file.parent.create(recursive: true); await file.writeAsString(base64Encode(encrypted), flush: true); } static bool _isValidHexKey(String value) { return RegExp(r'^[0-9a-f]{64}$').hasMatch(value); } static List _hexToBytes(String hex) { final result = []; for (var i = 0; i < hex.length; i += 2) { result.add(int.parse(hex.substring(i, i + 2), radix: 16)); } return result; } static Uint8List _protectForCurrentUser(Uint8List input) { final dataIn = calloc(); final dataOut = calloc(); final inputBuffer = calloc(input.length); try { inputBuffer.asTypedList(input.length).setAll(0, input); dataIn.ref.cbData = input.length; dataIn.ref.pbData = inputBuffer; final result = CryptProtectData( dataIn, nullptr, nullptr, nullptr, nullptr, _cryptProtectUiForbidden, dataOut, ); if (result == 0) { throw WindowsException(HRESULT_FROM_WIN32(GetLastError())); } return Uint8List.fromList( dataOut.ref.pbData.asTypedList(dataOut.ref.cbData), ); } finally { if (dataOut.ref.pbData != nullptr) { LocalFree(dataOut.ref.pbData); } calloc.free(inputBuffer); calloc.free(dataIn); calloc.free(dataOut); } } static Uint8List _unprotectForCurrentUser(Uint8List input) { final dataIn = calloc(); final dataOut = calloc(); final inputBuffer = calloc(input.length); try { inputBuffer.asTypedList(input.length).setAll(0, input); dataIn.ref.cbData = input.length; dataIn.ref.pbData = inputBuffer; final result = CryptUnprotectData( dataIn, nullptr, nullptr, nullptr, nullptr, _cryptProtectUiForbidden, dataOut, ); if (result == 0) { throw WindowsException(HRESULT_FROM_WIN32(GetLastError())); } return Uint8List.fromList( dataOut.ref.pbData.asTypedList(dataOut.ref.cbData), ); } finally { if (dataOut.ref.pbData != nullptr) { LocalFree(dataOut.ref.pbData); } calloc.free(inputBuffer); calloc.free(dataIn); calloc.free(dataOut); } } } ================================================ FILE: lib/common/http.dart ================================================ import 'dart:io'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/state.dart'; class BettboxHttpOverrides extends HttpOverrides { static String handleFindProxy(Uri url) { if ([localhost].contains(url.host)) { return 'DIRECT'; } final port = globalState.config.patchClashConfig.mixedPort; final isStart = globalState.appState.runTime != null; if (!isStart) return 'DIRECT'; return 'PROXY localhost:$port'; } @override HttpClient createHttpClient(SecurityContext? context) { final client = super.createHttpClient(context); client.badCertificateCallback = (_, _, _) => true; client.findProxy = handleFindProxy; return client; } } ================================================ FILE: lib/common/icons.dart ================================================ import 'package:flutter/material.dart'; class IconsExt { static const IconData target = IconData(0xe900, fontFamily: 'Icons'); } ================================================ FILE: lib/common/iterable.dart ================================================ extension IterableExt on Iterable { Iterable separated(T separator) sync* { final iterator = this.iterator; if (!iterator.moveNext()) return; yield iterator.current; while (iterator.moveNext()) { yield separator; yield iterator.current; } } Iterable> chunks(int size) sync* { if (length == 0) return; var iterator = this.iterator; while (iterator.moveNext()) { var chunk = [iterator.current]; for (var i = 1; i < size && iterator.moveNext(); i++) { chunk.add(iterator.current); } yield chunk; } } Iterable fill(int length, {required T Function(int count) filler}) sync* { int count = 0; for (var item in this) { yield item; count++; if (count >= length) return; } while (count < length) { yield filler(count); count++; } } Iterable takeLast({int count = 50}) { if (count <= 0) return Iterable.empty(); return count >= length ? this : toList().skip(length - count); } } extension ListExt on List { void truncate(int maxLength) { if (maxLength == 0) { return; } if (length > maxLength) { removeRange(0, length - maxLength); } } List intersection(List list) { return where((item) => list.contains(item)).toList(); } List> batch(int maxConcurrent) { final batches = (length / maxConcurrent).ceil(); final List> res = []; for (int i = 0; i < batches; i++) { if (i != batches - 1) { res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1))); } else { res.add(sublist(i * maxConcurrent, length)); } } return res; } List safeSublist(int start, [int? end]) { if (start <= 0) return this; if (start > length) return []; if (end != null) { return sublist(start, end.clamp(start, length)); } return sublist(start); } T safeGet(int index) { if (length > index) return this[index]; return last; } } extension DoubleListExt on List { int findInterval(num target) { if (isEmpty) return -1; if (target < first) return -1; if (target >= last) return length - 1; int left = 0; int right = length - 1; while (left <= right) { int mid = left + (right - left) ~/ 2; if (mid == length - 1 || (this[mid] <= target && target < this[mid + 1])) { return mid; } else if (target < this[mid]) { right = mid - 1; } else { left = mid + 1; } } return -1; } } extension MapExt on Map { V updateCacheValue(K key, V Function() callback) { if (this[key] == null) { this[key] = callback(); } return this[key]!; } } ================================================ FILE: lib/common/js_runtime_manager.dart ================================================ import 'dart:async'; import 'package:flutter_js/flutter_js.dart'; import 'package:synchronized/synchronized.dart'; class JavaScriptRuntimeManager { static JavascriptRuntime? _instance; static final Lock _lock = Lock(); static int _activeCount = 0; static bool _isDisposing = false; static Timer? _disposeTimer; static const Duration _disposeDelay = Duration(seconds: 10); static Future execute( Future Function(JavascriptRuntime runtime) task, ) async { final runtime = await _acquire(); try { return await task(runtime); } finally { await _release(); } } static Future _acquire() async { return _lock.synchronized(() async { while (_isDisposing) { await Future.delayed(const Duration(milliseconds: 10)); } _disposeTimer?.cancel(); _disposeTimer = null; _activeCount++; _instance ??= getJavascriptRuntime(); return _instance!; }); } static Future _release() async { await _lock.synchronized(() async { _activeCount--; if (_activeCount <= 0 && _instance != null) { _disposeTimer?.cancel(); _disposeTimer = Timer(_disposeDelay, () { dispose(); }); } }); } static Future dispose() async { return _lock.synchronized(() async { if (_activeCount > 0) return; if (_instance != null) { _isDisposing = true; try { _instance!.dispose(); } catch (_) {} _instance = null; _isDisposing = false; } }); } } ================================================ FILE: lib/common/keyboard.dart ================================================ import 'package:flutter/services.dart'; import 'package:uni_platform/uni_platform.dart'; import 'system.dart'; final Map _knownKeyLabels = { PhysicalKeyboardKey.keyA: 'A', PhysicalKeyboardKey.keyB: 'B', PhysicalKeyboardKey.keyC: 'C', PhysicalKeyboardKey.keyD: 'D', PhysicalKeyboardKey.keyE: 'E', PhysicalKeyboardKey.keyF: 'F', PhysicalKeyboardKey.keyG: 'G', PhysicalKeyboardKey.keyH: 'H', PhysicalKeyboardKey.keyI: 'I', PhysicalKeyboardKey.keyJ: 'J', PhysicalKeyboardKey.keyK: 'K', PhysicalKeyboardKey.keyL: 'L', PhysicalKeyboardKey.keyM: 'M', PhysicalKeyboardKey.keyN: 'N', PhysicalKeyboardKey.keyO: 'O', PhysicalKeyboardKey.keyP: 'P', PhysicalKeyboardKey.keyQ: 'Q', PhysicalKeyboardKey.keyR: 'R', PhysicalKeyboardKey.keyS: 'S', PhysicalKeyboardKey.keyT: 'T', PhysicalKeyboardKey.keyU: 'U', PhysicalKeyboardKey.keyV: 'V', PhysicalKeyboardKey.keyW: 'W', PhysicalKeyboardKey.keyX: 'X', PhysicalKeyboardKey.keyY: 'Y', PhysicalKeyboardKey.keyZ: 'Z', PhysicalKeyboardKey.digit1: '1', PhysicalKeyboardKey.digit2: '2', PhysicalKeyboardKey.digit3: '3', PhysicalKeyboardKey.digit4: '4', PhysicalKeyboardKey.digit5: '5', PhysicalKeyboardKey.digit6: '6', PhysicalKeyboardKey.digit7: '7', PhysicalKeyboardKey.digit8: '8', PhysicalKeyboardKey.digit9: '9', PhysicalKeyboardKey.digit0: '0', PhysicalKeyboardKey.enter: 'ENTER', PhysicalKeyboardKey.escape: 'ESCAPE', PhysicalKeyboardKey.backspace: 'BACKSPACE', PhysicalKeyboardKey.tab: 'TAB', PhysicalKeyboardKey.space: 'SPACE', PhysicalKeyboardKey.minus: '-', PhysicalKeyboardKey.equal: '=', PhysicalKeyboardKey.bracketLeft: '[', PhysicalKeyboardKey.bracketRight: ']', PhysicalKeyboardKey.backslash: '\\', PhysicalKeyboardKey.semicolon: ';', PhysicalKeyboardKey.quote: '"', PhysicalKeyboardKey.backquote: '`', PhysicalKeyboardKey.comma: ',', PhysicalKeyboardKey.period: '.', PhysicalKeyboardKey.slash: '/', PhysicalKeyboardKey.capsLock: 'CAPSLOCK', PhysicalKeyboardKey.f1: 'F1', PhysicalKeyboardKey.f2: 'F2', PhysicalKeyboardKey.f3: 'F3', PhysicalKeyboardKey.f4: 'F4', PhysicalKeyboardKey.f5: 'F5', PhysicalKeyboardKey.f6: 'F6', PhysicalKeyboardKey.f7: 'F7', PhysicalKeyboardKey.f8: 'F8', PhysicalKeyboardKey.f9: 'F9', PhysicalKeyboardKey.f10: 'F10', PhysicalKeyboardKey.f11: 'F11', PhysicalKeyboardKey.f12: 'F12', PhysicalKeyboardKey.home: 'HOME', PhysicalKeyboardKey.pageUp: 'PAGEUP', PhysicalKeyboardKey.delete: 'DELETE', PhysicalKeyboardKey.end: 'END', PhysicalKeyboardKey.pageDown: 'PAGEDOWN', PhysicalKeyboardKey.arrowRight: '→', PhysicalKeyboardKey.arrowLeft: '←', PhysicalKeyboardKey.arrowDown: '↓', PhysicalKeyboardKey.arrowUp: '↑', PhysicalKeyboardKey.controlLeft: 'CTRL', PhysicalKeyboardKey.shiftLeft: 'SHIFT', PhysicalKeyboardKey.altLeft: 'ALT', PhysicalKeyboardKey.metaLeft: system.isMacOS ? '⌘' : 'WIN', PhysicalKeyboardKey.controlRight: 'CTRL', PhysicalKeyboardKey.shiftRight: 'SHIFT', PhysicalKeyboardKey.altRight: 'ALT', PhysicalKeyboardKey.metaRight: system.isMacOS ? '⌘' : 'WIN', PhysicalKeyboardKey.fn: 'FN', }; extension KeyboardKeyExt on KeyboardKey { String get label { PhysicalKeyboardKey? physicalKey; if (this is LogicalKeyboardKey) { physicalKey = (this as LogicalKeyboardKey).physicalKey; } else if (this is PhysicalKeyboardKey) { physicalKey = this as PhysicalKeyboardKey; } return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown'; } } ================================================ FILE: lib/common/launch.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'constant.dart'; import 'system.dart'; class AutoLaunch { static AutoLaunch? _instance; AutoLaunch._internal() { launchAtStartup.setup( appName: appName, appPath: Platform.resolvedExecutable, ); } factory AutoLaunch() { _instance ??= AutoLaunch._internal(); return _instance!; } Future get isEnable async { if (system.isWindows) { // Windows 上改为通过任务计划实现开机自启动 try { final result = await Process.run('schtasks', [ '/Query', '/TN', appName, ]); return result.exitCode == 0; } catch (_) { return false; } } return await launchAtStartup.isEnabled(); } Future enable({bool requireNetwork = true}) async { if (system.isWindows) { // 使用任务计划实现 Windows 开机自启动(管理员模式) return await windows?.registerTask( appName, requireNetwork: requireNetwork, ) ?? false; } return await launchAtStartup.enable(); } Future disable() async { if (system.isWindows) { return await windows?.unregisterTask(appName) ?? false; } return await launchAtStartup.disable(); } Future updateStatus( bool isAutoLaunch, { bool requireNetwork = true, }) async { if (kDebugMode) { return; } if (await isEnable == isAutoLaunch) return; // 异步执行,避免阻塞 UI if (isAutoLaunch == true) { unawaited(enable(requireNetwork: requireNetwork)); } else { unawaited(disable()); } } } final autoLaunch = system.isDesktop ? AutoLaunch() : null; ================================================ FILE: lib/common/link.dart ================================================ import 'dart:async'; import 'package:app_links/app_links.dart'; import 'print.dart'; typedef InstallConfigCallBack = void Function(String url); class LinkManager { static LinkManager? _instance; late AppLinks _appLinks; StreamSubscription? subscription; LinkManager._internal() { _appLinks = AppLinks(); } Future initAppLinksListen( Function(String url) installConfigCallBack, ) async { commonPrint.log('initAppLinksListen'); destroy(); subscription = _appLinks.uriLinkStream.listen((uri) { commonPrint.log('onAppLink: $uri'); if (uri.host == 'install-config') { final parameters = uri.queryParameters; final url = parameters['url']; if (url != null) { installConfigCallBack(url); } } }); } void destroy() { subscription?.cancel(); subscription = null; } factory LinkManager() { _instance ??= LinkManager._internal(); return _instance!; } } final linkManager = LinkManager(); ================================================ FILE: lib/common/lock.dart ================================================ import 'dart:io'; import 'package:bett_box/common/common.dart'; class SingleInstanceLock { static SingleInstanceLock? _instance; RandomAccessFile? _accessFile; SingleInstanceLock._internal(); factory SingleInstanceLock() { _instance ??= SingleInstanceLock._internal(); return _instance!; } Future acquire() async { try { final lockFilePath = await appPath.lockFilePath; final lockFile = File(lockFilePath); await lockFile.create(); _accessFile = await lockFile.open(mode: FileMode.write); await _accessFile?.lock(); return true; } catch (_) { return false; } } } final singleInstanceLock = SingleInstanceLock(); ================================================ FILE: lib/common/measure.dart ================================================ import 'package:bett_box/common/common.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class Measure { final TextScaler _textScaler; final BuildContext context; final Map _measureMap; Measure.of(this.context, double textScaleFactor) : _measureMap = {}, _textScaler = TextScaler.linear(textScaleFactor); Size computeTextSize(Text text, {double maxWidth = double.infinity}) { final textPainter = TextPainter( text: TextSpan(text: text.data, style: text.style), maxLines: text.maxLines, textScaler: _textScaler, textDirection: text.textDirection ?? TextDirection.ltr, )..layout(maxWidth: maxWidth); return textPainter.size; } double get bodyMediumHeight { return _measureMap.updateCacheValue( 'bodyMediumHeight', () => computeTextSize( Text('X', style: context.textTheme.bodyMedium), ).height, ); } double get bodyLargeHeight { return _measureMap.updateCacheValue( 'bodyLargeHeight', () => computeTextSize(Text('X', style: context.textTheme.bodyLarge)).height, ); } double get bodySmallHeight { return _measureMap.updateCacheValue( 'bodySmallHeight', () => computeTextSize(Text('X', style: context.textTheme.bodySmall)).height, ); } double get labelSmallHeight { return _measureMap.updateCacheValue( 'labelSmallHeight', () => computeTextSize( Text('X', style: context.textTheme.labelSmall), ).height, ); } double get labelMediumHeight { return _measureMap.updateCacheValue( 'labelMediumHeight', () => computeTextSize( Text('X', style: context.textTheme.labelMedium), ).height, ); } double get titleLargeHeight { return _measureMap.updateCacheValue( 'titleLargeHeight', () => computeTextSize( Text('X', style: context.textTheme.titleLarge), ).height, ); } double get titleMediumHeight { return _measureMap.updateCacheValue( 'titleMediumHeight', () => computeTextSize( Text('X', style: context.textTheme.titleMedium), ).height, ); } } ================================================ FILE: lib/common/mixin.dart ================================================ import 'package:riverpod/riverpod.dart'; mixin AutoDisposeNotifierMixin on AutoDisposeNotifier { set value(T value) { state = value; } @override bool updateShouldNotify(previous, next) { final res = super.updateShouldNotify(previous, next); if (res) { onUpdate(next); } return res; } void onUpdate(T value) {} } ================================================ FILE: lib/common/navigation.dart ================================================ import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/providers/providers.dart'; import 'package:bett_box/views/views.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class Navigation { static Navigation? _instance; List getItems({ bool openLogs = false, bool hasProxies = false, }) { return [ NavigationItem( keep: false, icon: Icon(Icons.space_dashboard), label: PageLabel.dashboard, builder: (_) => const DashboardView(key: GlobalObjectKey(PageLabel.dashboard)), ), NavigationItem( icon: const Icon(Icons.article), label: PageLabel.proxies, builder: (_) => ProviderScope( overrides: [queryProvider.overrideWith(() => Query())], child: const ProxiesView(key: GlobalObjectKey(PageLabel.proxies)), ), modes: hasProxies ? [NavigationItemMode.mobile, NavigationItemMode.desktop] : [], ), NavigationItem( icon: Icon(Icons.folder), label: PageLabel.profiles, builder: (_) => const ProfilesView(key: GlobalObjectKey(PageLabel.profiles)), ), NavigationItem( icon: Icon(Icons.view_timeline), label: PageLabel.requests, builder: (_) => const RequestsView(key: GlobalObjectKey(PageLabel.requests)), description: 'requestsDesc', modes: [NavigationItemMode.desktop, NavigationItemMode.more], ), NavigationItem( icon: Icon(Icons.ballot), label: PageLabel.connections, builder: (_) => const ConnectionsView(key: GlobalObjectKey(PageLabel.connections)), description: 'connectionsDesc', modes: [NavigationItemMode.desktop, NavigationItemMode.more], ), NavigationItem( icon: Icon(Icons.storage), label: PageLabel.resources, description: 'resourcesDesc', builder: (_) => const ResourcesView(key: GlobalObjectKey(PageLabel.resources)), modes: [NavigationItemMode.more], ), NavigationItem( icon: Icon(Icons.functions), label: PageLabel.script, description: 'scriptDesc', builder: (_) => const ScriptsView(key: GlobalObjectKey(PageLabel.script)), modes: [NavigationItemMode.more], ), NavigationItem( icon: const Icon(Icons.adb), label: PageLabel.logs, builder: (_) => const LogsView(key: GlobalObjectKey(PageLabel.logs)), description: 'logsDesc', modes: [NavigationItemMode.desktop, NavigationItemMode.more], ), NavigationItem( icon: Icon(Icons.construction), label: PageLabel.tools, builder: (_) => const ToolsView(key: GlobalObjectKey(PageLabel.tools)), modes: [NavigationItemMode.desktop, NavigationItemMode.mobile], ), ]; } Navigation._internal(); factory Navigation() { _instance ??= Navigation._internal(); return _instance!; } } final navigation = Navigation(); ================================================ FILE: lib/common/navigator.dart ================================================ import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/app.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; class BaseNavigator { static Future push(BuildContext context, Widget child) async { if (globalState.appState.viewMode != ViewMode.mobile) { return await Navigator.of( context, ).push(CommonDesktopRoute(builder: (context) => child)); } return await Navigator.of( context, ).push(MaterialPageRoute(builder: (context) => child)); } } class CommonDesktopRoute extends PageRoute { final Widget Function(BuildContext context) builder; CommonDesktopRoute({required this.builder}); @override Color? get barrierColor => null; @override String? get barrierLabel => null; @override Widget buildPage( BuildContext context, Animation animation, Animation secondaryAnimation, ) { final Widget result = builder(context); return Semantics( scopesRoute: true, explicitChildNodes: true, child: FadeTransition(opacity: animation, child: result), ); } @override bool get maintainState => true; @override Duration get transitionDuration => Duration(milliseconds: 200); @override Duration get reverseTransitionDuration => Duration(milliseconds: 200); } ================================================ FILE: lib/common/network.dart ================================================ import 'dart:io'; extension NetworkInterfaceExt on NetworkInterface { bool get isWifi { final nameLowCase = name.toLowerCase(); if (nameLowCase.contains('wlan') || nameLowCase.contains('wi-fi') || nameLowCase == 'en0' || nameLowCase == 'eth0') { return true; } return false; } bool get includesIPv4 { return addresses.any((addr) => addr.isIPv4); } } extension InternetAddressExt on InternetAddress { bool get isIPv4 { return type == InternetAddressType.IPv4; } } ================================================ FILE: lib/common/network_matcher.dart ================================================ class NetworkMatcher { static int? parseIPv4(String ip) { final parts = ip.trim().split('.'); if (parts.length != 4) return null; int result = 0; for (final part in parts) { final value = int.tryParse(part); if (value == null || value < 0 || value > 255) return null; result = (result << 8) | value; } return result; } static String formatIPv4(int ip) { return '${(ip >> 24) & 0xFF}.${(ip >> 16) & 0xFF}.${(ip >> 8) & 0xFF}.${ip & 0xFF}'; } static (int, int)? parseCIDR(String cidr) { final parts = cidr.trim().split('/'); if (parts.length == 1) { final ip = parseIPv4(parts[0]); return ip != null ? (ip, 32) : null; } if (parts.length != 2) return null; final ip = parseIPv4(parts[0]); if (ip == null) return null; final prefix = int.tryParse(parts[1]); if (prefix == null || prefix < 0 || prefix > 32) return null; final mask = prefix == 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF; return (ip & mask, prefix); } static bool isIPInCIDR(String ip, String cidr) { final ipInt = parseIPv4(ip); if (ipInt == null) return false; final parsed = parseCIDR(cidr); if (parsed == null) return false; final (network, prefix) = parsed; if (prefix == 0) return true; final mask = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF; return (ipInt & mask) == network; } static bool matchRule(String ip, String rule) { final trimmed = rule.trim(); if (trimmed.isEmpty) return false; if (trimmed.contains('/')) { return isIPInCIDR(ip, trimmed); } final ipInt = parseIPv4(ip); final ruleInt = parseIPv4(trimmed); return ipInt != null && ruleInt != null && ipInt == ruleInt; } static bool matchAny(String? ip, String rules) { if (ip == null || ip.isEmpty || rules.isEmpty) return false; return rules.split(',').any((rule) => matchRule(ip, rule)); } static bool isValidRule(String rule) { final trimmed = rule.trim(); if (trimmed.isEmpty) return false; return trimmed.contains('/') ? parseCIDR(trimmed) != null : parseIPv4(trimmed) != null; } static bool isValidRules(String rules) { if (rules.isEmpty) return true; final ruleList = rules.split(','); if (ruleList.length > 2) return false; return ruleList.every((rule) => rule.trim().isEmpty || isValidRule(rule)); } static String? getValidationError( String rules, { String invalidFormatMsg = 'Invalid IP or CIDR format', String tooManyRulesMsg = 'Maximum 2 rules allowed', }) { if (rules.isEmpty) return null; final ruleList = rules.split(','); if (ruleList.length > 2) return tooManyRulesMsg; for (final rule in ruleList) { if (rule.trim().isNotEmpty && !isValidRule(rule)) { return invalidFormatMsg; } } return null; } } ================================================ FILE: lib/common/num.dart ================================================ import 'package:bett_box/state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; extension NumExt on num { String fixed({int decimals = 2}) { return toStringAsFixed(decimals); } double get ap { return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5); } } extension DoubleExt on double { bool moreOrEqual(double value) { return this > value || (value - this).abs() < precisionErrorTolerance + 1; } } extension OffsetExt on Offset { double getCrossAxisOffset(Axis direction) { return direction == Axis.vertical ? dx : dy; } double getMainAxisOffset(Axis direction) { return direction == Axis.vertical ? dy : dx; } bool less(Offset offset) { if (dy < offset.dy) { return true; } if (dy == offset.dy && dx < offset.dx) { return true; } return false; } } extension RectExt on Rect { bool doRectIntersect(Rect rect) { return left < rect.right && right > rect.left && top < rect.bottom && bottom > rect.top; } } ================================================ FILE: lib/common/package.dart ================================================ import 'package:package_info_plus/package_info_plus.dart'; extension PackageInfoExtension on PackageInfo { String get ua => ['Clash.Meta/ClashMetaForAndroid/5.0'].join(' '); } ================================================ FILE: lib/common/path.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:bett_box/common/common.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; class AppPath { static AppPath? _instance; Completer dataDir = Completer(); Completer downloadDir = Completer(); Completer tempDir = Completer(); late String appDirPath; AppPath._internal() { appDirPath = join(dirname(Platform.resolvedExecutable)); getApplicationSupportDirectory().then((value) { dataDir.complete(value); }); getTemporaryDirectory().then((value) { tempDir.complete(value); }); getDownloadsDirectory().then((value) { downloadDir.complete(value); }); } factory AppPath() { _instance ??= AppPath._internal(); return _instance!; } String get executableExtension { return system.isWindows ? '.exe' : ''; } String get executableDirPath { final currentExecutablePath = Platform.resolvedExecutable; return dirname(currentExecutablePath); } String get corePath { return join(executableDirPath, 'BettboxCore$executableExtension'); } String get helperPath { return join(executableDirPath, '$appHelperService$executableExtension'); } Future get downloadDirPath async { final directory = await downloadDir.future; return directory.path; } Future get homeDirPath async { final directory = await dataDir.future; return directory.path; } Future get lockFilePath async { final directory = await dataDir.future; return join(directory.path, 'Bettbox.lock'); } Future get sharedPreferencesPath async { final directory = await dataDir.future; return join(directory.path, 'shared_preferences.json'); } Future get helperAuthKeyPath async { final directory = await dataDir.future; return join(directory.path, 'helper_auth.key'); } Future get profilesPath async { final directory = await dataDir.future; return join(directory.path, profilesDirectoryName); } Future getProfilePath(String id) async { final directory = await profilesPath; return join(directory, '$id.yaml'); } Future getProvidersDirPath(String id) async { final directory = await profilesPath; return join(directory, 'providers', id); } Future getProvidersFilePath( String id, String type, String url, ) async { final directory = await profilesPath; return join(directory, 'providers', id, type, url.toMd5()); } Future get tempPath async { final directory = await tempDir.future; return directory.path; } Future get uiPath async { final directory = await dataDir.future; return join(directory.path, 'ui'); } } final appPath = AppPath(); ================================================ FILE: lib/common/picker.dart ================================================ import 'dart:io'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:bett_box/common/common.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class Picker { Future pickerFile({bool withData = true}) async { final filePickerResult = await FilePicker.platform.pickFiles( withData: withData, allowMultiple: false, initialDirectory: await appPath.downloadDirPath, ); return filePickerResult?.files.first; } Future saveFile(String fileName, Uint8List bytes) async { final path = await FilePicker.platform.saveFile( fileName: fileName, initialDirectory: await appPath.downloadDirPath, bytes: system.isAndroid ? bytes : null, ); if (!system.isAndroid && path != null) { final file = await File(path).create(recursive: true); await file.writeAsBytes(bytes); } return path; } Future pickerConfigQRCode() async { final xFile = await ImagePicker().pickImage(source: ImageSource.gallery); if (xFile == null) { return null; } final controller = MobileScannerController(); final capture = await controller.analyzeImage( xFile.path, formats: [BarcodeFormat.qrCode], ); final result = capture?.barcodes.first.rawValue; if (result == null || !result.isUrl) { throw appLocalizations.pleaseUploadValidQrcode; } return result; } } final picker = Picker(); ================================================ FILE: lib/common/preferences.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:bett_box/models/models.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'constant.dart'; class Preferences { static Preferences? _instance; Completer sharedPreferencesCompleter = Completer(); Future get isInit async => await sharedPreferencesCompleter.future != null; Preferences._internal() { SharedPreferences.getInstance() .then((value) => sharedPreferencesCompleter.complete(value)) .onError((_, _) => sharedPreferencesCompleter.complete(null)); } factory Preferences() { _instance ??= Preferences._internal(); return _instance!; } Future getClashConfig() async { final preferences = await sharedPreferencesCompleter.future; final clashConfigString = preferences?.getString(clashConfigKey); if (clashConfigString == null) return null; final clashConfigMap = json.decode(clashConfigString); return ClashConfig.fromJson(clashConfigMap); } Future getConfig() async { final preferences = await sharedPreferencesCompleter.future; final configString = preferences?.getString(configKey); if (configString == null) return null; final configMap = json.decode(configString); return Config.compatibleFromJson(configMap); } Future saveConfig(Config config) async { final preferences = await sharedPreferencesCompleter.future; return await preferences?.setString(configKey, json.encode(config)) ?? false; } Future clearClashConfig() async { final preferences = await sharedPreferencesCompleter.future; preferences?.remove(clashConfigKey); } Future clearPreferences() async { final sharedPreferencesIns = await sharedPreferencesCompleter.future; sharedPreferencesIns?.clear(); } } final preferences = Preferences(); ================================================ FILE: lib/common/print.dart ================================================ import 'package:bett_box/models/models.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/cupertino.dart'; class CommonPrint { static CommonPrint? _instance; CommonPrint._internal(); factory CommonPrint() { _instance ??= CommonPrint._internal(); return _instance!; } void log(String? text) { final payload = '[APP] $text'; debugPrint(payload); if (!globalState.isInit) { return; } globalState.appController.addLog(Log.app(payload)); } } final commonPrint = CommonPrint(); ================================================ FILE: lib/common/protocol.dart ================================================ import 'dart:io'; import 'package:win32_registry/win32_registry.dart'; class Protocol { static Protocol? _instance; Protocol._internal(); factory Protocol() { _instance ??= Protocol._internal(); return _instance!; } void register(String scheme) { String protocolRegKey = 'Software\\Classes\\$scheme'; RegistryValue protocolRegValue = RegistryValue.string('URL Protocol', ''); String protocolCmdRegKey = 'shell\\open\\command'; RegistryValue protocolCmdRegValue = RegistryValue.string( '', '"${Platform.resolvedExecutable}" "%1"', ); final regKey = Registry.currentUser.createKey(protocolRegKey); regKey.createValue(protocolRegValue); regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); } } final protocol = Protocol(); ================================================ FILE: lib/common/proxy.dart ================================================ import 'package:bett_box/common/system.dart'; import 'package:proxy/proxy.dart'; final proxy = system.isDesktop ? Proxy() : null; ================================================ FILE: lib/common/render.dart ================================================ import 'package:bett_box/common/common.dart'; import 'package:flutter/scheduler.dart'; class Render { static Render? _instance; bool _isPaused = false; final _dispatcher = SchedulerBinding.instance.platformDispatcher; FrameCallback? _beginFrame; VoidCallback? _drawFrame; Render._internal(); factory Render() { _instance ??= Render._internal(); return _instance!; } void pause() { _pause(); } void resume() { _resume(); } void _pause() async { if (!system.isWindows) return; if (_isPaused) return; _isPaused = true; _beginFrame = _dispatcher.onBeginFrame; _drawFrame = _dispatcher.onDrawFrame; _dispatcher.onBeginFrame = null; _dispatcher.onDrawFrame = null; } void _resume() { if (!_isPaused) return; _isPaused = false; _dispatcher.onBeginFrame = _beginFrame; _dispatcher.onDrawFrame = _drawFrame; _dispatcher.scheduleFrame(); } } final Render? render = system.isDesktop ? Render() : null; ================================================ FILE: lib/common/request.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/common/helper_auth.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/cupertino.dart'; class Request { late final Dio _dio; late final Dio _clashDio; String? userAgent; Request() { _dio = Dio(BaseOptions(headers: {'User-Agent': browserUa})); _clashDio = Dio(); _clashDio.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final client = HttpClient(); client.findProxy = (Uri uri) { client.userAgent = globalState.ua; return BettboxHttpOverrides.handleFindProxy(uri); }; return client; }, ); } Future _getResponseForUrl(String url, ResponseType responseType) async { final uri = Uri.parse(url); final userInfo = uri.userInfo; Options? options; if (userInfo.isNotEmpty) { final auth = base64Encode(utf8.encode(userInfo)); options = Options( responseType: responseType, headers: {'Authorization': 'Basic $auth'}, ); url = uri.replace(userInfo: '').toString(); } final response = await _clashDio.get( url, options: options ?? Options(responseType: responseType), ); return response; } Future getFileResponseForUrl(String url) async { return _getResponseForUrl(url, ResponseType.bytes); } Future getTextResponseForUrl(String url) async { return _getResponseForUrl(url, ResponseType.plain); } Future getImage(String url) async { if (url.isEmpty) return null; final response = await _dio.get( url, options: Options(responseType: ResponseType.bytes), ); final data = response.data; if (data == null) return null; return MemoryImage(data); } Future?> checkForUpdate() async { try { final response = await _dio.get( 'https://api.github.com/repos/$repository/releases/latest', options: Options(responseType: ResponseType.json), ); if (response.statusCode != 200) return null; final data = response.data as Map; final remoteVersion = data['tag_name']; final version = globalState.packageInfo.version; final hasUpdate = utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; if (!hasUpdate) return null; return data; } on DioException catch (e) { commonPrint.log('Check update failed: ${e.message}'); return null; } catch (e) { commonPrint.log('Check update error: $e'); return null; } } final List _ipInfoSources = [ 'https://api.cloudflare.com/cdn-cgi/trace', 'https://cp.cloudflare.com/cdn-cgi/trace', ]; final List _domesticIpSources = [ 'https://www.teamviewer.cn/cdn-cgi/trace', 'https://www.cloudflare-cn.com/cdn-cgi/trace', ]; Future> _checkIpFromSources( List sources, CancelToken? cancelToken, Duration? timeout, ) async { final effectiveTimeout = timeout ?? const Duration(seconds: 5); final dio = Dio( BaseOptions( receiveTimeout: effectiveTimeout, connectTimeout: effectiveTimeout, ), ); final Completer> resultCompleter = Completer(); int failureCount = 0; void handleFailure() { if (resultCompleter.isCompleted) return; failureCount++; if (failureCount == sources.length) { resultCompleter.complete(Result.success(null)); } } for (final url in sources) { dio.get( url, cancelToken: cancelToken, options: Options(responseType: ResponseType.plain), ).then((res) { if (resultCompleter.isCompleted) return; if (res.statusCode == HttpStatus.ok && res.data != null) { try { resultCompleter.complete( Result.success(IpInfo.fromCloudflareTrace(res.data!)), ); } catch (_) { handleFailure(); } } else { handleFailure(); } }).catchError((e) { if (resultCompleter.isCompleted) return; if (e is DioException && e.type == DioExceptionType.cancel) { resultCompleter.complete(Result.error('cancelled')); return; } handleFailure(); }); } try { return await resultCompleter.future.timeout( effectiveTimeout, onTimeout: () => Result.success(null), ); } finally { dio.close(force: true); } } Future> checkIp({ CancelToken? cancelToken, Duration? timeout, }) async { return _checkIpFromSources(_ipInfoSources, cancelToken, timeout); } Future> checkIpDomestic({ CancelToken? cancelToken, Duration? timeout, }) async { return _checkIpFromSources(_domesticIpSources, cancelToken, timeout); } Future quickPingHelper() async { try { final response = await _dio .get( 'http://$localhost:$helperPort/ping', options: Options(responseType: ResponseType.plain), ) .timeout(const Duration(milliseconds: 500)); if (response.statusCode != HttpStatus.ok) { return false; } return (response.data as String) == globalState.coreSHA256; } catch (_) { return false; } } Future startCoreByHelper(String arg) async { final helperAlive = await quickPingHelper(); if (!helperAlive) { commonPrint.log('Helper service is not reachable, skipping startCoreByHelper'); return false; } final homeDirPath = await appPath.homeDirPath; final body = json.encode({ 'path': appPath.corePath, 'arg': arg, 'home_dir': homeDirPath, }); final authHeaders = HelperAuthManager.generateAuthHeaders(body); const maxAttempts = 4; const interval = Duration(milliseconds: 500); const requestTimeout = Duration(seconds: 5); for (var attempt = 1; attempt <= maxAttempts; attempt++) { try { final response = await _dio .post( 'http://$localhost:$helperPort/start', data: body, options: Options( responseType: ResponseType.plain, headers: authHeaders, ), ) .timeout(requestTimeout); if (response.statusCode == HttpStatus.ok) { final data = response.data as String; if (data.isEmpty) return true; } } catch (e) { if (attempt == maxAttempts) { commonPrint.log('Failed to start core by helper after $maxAttempts attempts: $e'); return false; } } await Future.delayed(interval); } return false; } Future stopCoreByHelper() async { try { final authHeaders = HelperAuthManager.generateAuthHeaders(''); final response = await _dio .post( 'http://$localhost:$helperPort/stop', options: Options( responseType: ResponseType.plain, headers: authHeaders, ), ) .timeout(const Duration(milliseconds: 2000)); if (response.statusCode != HttpStatus.ok) { return false; } return true; } catch (e) { commonPrint.log('Failed to stop core by helper: $e'); return false; } } } final request = Request(); ================================================ FILE: lib/common/scroll.dart ================================================ import 'dart:math'; import 'dart:ui'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/widgets/scroll.dart'; import 'package:flutter/material.dart'; class BaseScrollBehavior extends MaterialScrollBehavior { @override Set get dragDevices => { PointerDeviceKind.touch, PointerDeviceKind.stylus, PointerDeviceKind.invertedStylus, PointerDeviceKind.trackpad, if (system.isDesktop) PointerDeviceKind.mouse, PointerDeviceKind.unknown, }; } class HiddenBarScrollBehavior extends BaseScrollBehavior { @override Widget buildScrollbar( BuildContext context, Widget child, ScrollableDetails details, ) { return child; } } class ShowBarScrollBehavior extends BaseScrollBehavior { @override Widget buildScrollbar( BuildContext context, Widget child, ScrollableDetails details, ) { return CommonScrollBar(controller: details.controller, child: child); } } class NextClampingScrollPhysics extends ClampingScrollPhysics { const NextClampingScrollPhysics({super.parent}); @override NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) { return NextClampingScrollPhysics(parent: buildParent(ancestor)); } @override Simulation? createBallisticSimulation( ScrollMetrics position, double velocity, ) { final Tolerance tolerance = toleranceFor(position); if (position.outOfRange) { double? end; if (position.pixels > position.maxScrollExtent) { end = position.maxScrollExtent; } if (position.pixels < position.minScrollExtent) { end = position.minScrollExtent; } assert(end != null); return ScrollSpringSimulation( spring, end!, end, min(0.0, velocity), tolerance: tolerance, ); } if (velocity.abs() < tolerance.velocity) { return null; } if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) { return null; } if (velocity < 0.0 && position.pixels <= position.minScrollExtent) { return null; } return ClampingScrollSimulation( position: position.pixels, velocity: velocity, tolerance: tolerance, ); } } class ReverseScrollController extends ScrollController { ReverseScrollController({ super.initialScrollOffset, super.keepScrollOffset, super.debugLabel, }); @override ScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition, ) { return ReverseScrollPosition( physics: physics, context: context, initialPixels: initialScrollOffset, keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ); } } class ReverseScrollPosition extends ScrollPositionWithSingleContext { ReverseScrollPosition({ required super.physics, required super.context, super.initialPixels = 0.0, super.keepScrollOffset, super.oldPosition, super.debugLabel, }); bool _isInit = false; @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { if (!_isInit) { correctPixels(maxScrollExtent); _isInit = true; } return super.applyContentDimensions(minScrollExtent, maxScrollExtent); } } ================================================ FILE: lib/common/state.dart ================================================ ================================================ FILE: lib/common/string.dart ================================================ import 'dart:convert'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'print.dart'; extension StringExtension on String { bool get isUrl { return RegExp(r'^(http|https|ftp)://').hasMatch(this); } dynamic get splitByMultipleSeparators { final parts = split( RegExp(r'[, ;]+'), ).where((part) => part.isNotEmpty).toList(); return parts.length > 1 ? parts : this; } int compareToLower(String other) { return toLowerCase().compareTo(other.toLowerCase()); } List get encodeUtf16LeWithBom { final byteData = ByteData(length * 2); final bom = [0xFF, 0xFE]; for (int i = 0; i < length; i++) { int charCode = codeUnitAt(i); byteData.setUint16(i * 2, charCode, Endian.little); } return bom + byteData.buffer.asUint8List(); } Uint8List? get getBase64 { final regExp = RegExp(r'base64,(.*)'); final match = regExp.firstMatch(this); final realValue = match?.group(1) ?? ''; if (realValue.isEmpty) { return null; } try { return base64.decode(realValue); } catch (e) { return null; } } bool get isSvg { return endsWith('.svg'); } bool get isRegex { try { RegExp(this); return true; } catch (e) { commonPrint.log(e.toString()); return false; } } String toMd5() { final bytes = utf8.encode(this); return md5.convert(bytes).toString(); } } extension StringExtensionSafe on String? { String getSafeValue(String defaultValue) { return this?.isEmpty != false ? defaultValue : this!; } } ================================================ FILE: lib/common/system.dart ================================================ import 'dart:ffi'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:ffi/ffi.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/common/helper_auth.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/plugins/app.dart'; import 'package:bett_box/state.dart'; import 'package:bett_box/widgets/input.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart'; class System { static System? _instance; System._internal(); factory System() { _instance ??= System._internal(); return _instance!; } bool get isDesktop => isWindows || isMacOS || isLinux; bool get isWindows => Platform.isWindows; bool get isMacOS => Platform.isMacOS; bool get isAndroid => Platform.isAndroid; bool get isLinux => Platform.isLinux; Future get version async { final deviceInfo = await DeviceInfoPlugin().deviceInfo; return switch (Platform.operatingSystem) { 'macos' => (deviceInfo as MacOsDeviceInfo).majorVersion, 'android' => (deviceInfo as AndroidDeviceInfo).version.sdkInt, 'windows' => (deviceInfo as WindowsDeviceInfo).majorVersion, String() => 0, }; } Future checkIsAdmin() async { final corePath = appPath.corePath.replaceAll(' ', '\\\\ '); if (system.isWindows) { final result = await windows?.checkService(); return result == WindowsHelperServiceStatus.running; } if (system.isMacOS) { final result = await Process.run('stat', ['-f', '%Su:%Sg %Sp', corePath]); final output = result.stdout.trim(); return output.startsWith('root:admin') && output.contains('rws'); } if (Platform.isLinux) { final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]); final output = result.stdout.trim(); return output.startsWith('root:') && output.contains('rws'); } return true; } Future authorizeCore() async { if (system.isAndroid) return AuthorizeCode.error; final corePath = appPath.corePath.replaceAll(' ', '\\\\ '); if (await checkIsAdmin()) return AuthorizeCode.none; if (system.isWindows) { final result = await windows?.registerService(); return result == true ? AuthorizeCode.success : AuthorizeCode.error; } if (system.isMacOS) { final shell = 'chown root:admin $corePath; chmod +sx $corePath'; final result = await Process.run('osascript', [ '-e', 'do shell script "$shell" with administrator privileges', ]); return result.exitCode == 0 ? AuthorizeCode.success : AuthorizeCode.error; } if (Platform.isLinux) { final shell = Platform.environment['SHELL'] ?? 'bash'; final password = await globalState.showCommonDialog( child: InputDialog( obscureText: true, title: appLocalizations.pleaseInputAdminPassword, value: '', ), ); final result = await Process.run(shell, [ '-c', 'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"', ]); return result.exitCode == 0 ? AuthorizeCode.success : AuthorizeCode.error; } return AuthorizeCode.error; } Future back() async { if (system.isAndroid) await app.moveTaskToBack(); await window?.hide(); } Future exit() async { if (system.isAndroid) await SystemNavigator.pop(); await window?.close(); } } final system = System(); class Windows { static Windows? _instance; late DynamicLibrary _shell32; Windows._internal() { _shell32 = DynamicLibrary.open('shell32.dll'); } factory Windows() { _instance ??= Windows._internal(); return _instance!; } bool runas(String command, String arguments, {bool showWindow = false}) { final commandPtr = command.toNativeUtf16(); final argumentsPtr = arguments.toNativeUtf16(); final operationPtr = 'runas'.toNativeUtf16(); final shellExecute = _shell32 .lookupFunction< Int32 Function( Pointer hwnd, Pointer lpOperation, Pointer lpFile, Pointer lpParameters, Pointer lpDirectory, Int32 nShowCmd, ), int Function( Pointer hwnd, Pointer lpOperation, Pointer lpFile, Pointer lpParameters, Pointer lpDirectory, int nShowCmd, ) >('ShellExecuteW'); // 0 = hide, 1 = show final result = shellExecute( nullptr, operationPtr, commandPtr, argumentsPtr, nullptr, showWindow ? 1 : 0, ); calloc.free(commandPtr); calloc.free(argumentsPtr); calloc.free(operationPtr); commonPrint.log('windows runas: [command masked] resultCode:$result'); return result > 32; } Future _killProcess(int port) async { final result = await Process.run('netstat', ['-ano']); final lines = result.stdout.toString().trim().split('\n'); for (final line in lines) { if (!line.contains(':$port') || !line.contains('LISTENING')) continue; final parts = line.trim().split(RegExp(r'\s+')); final pid = int.tryParse(parts.last); if (pid != null) { await Process.run('taskkill', ['/PID', pid.toString(), '/F']); } } } Future checkService() async { final result = await Process.run('sc', ['query', appHelperService]); if (result.exitCode != 0) return WindowsHelperServiceStatus.none; final output = result.stdout.toString(); if (!output.contains('RUNNING')) return WindowsHelperServiceStatus.presence; final isReachable = await request.quickPingHelper(); return isReachable ? WindowsHelperServiceStatus.running : WindowsHelperServiceStatus.presence; } Future registerService() async { final createdNewKey = await HelperAuthManager.ensureAuthKey(); final authKey = HelperAuthManager.getAuthKey(); final quickCheck = await Process.run('sc', ['query', appHelperService]); if (quickCheck.exitCode == 0 && quickCheck.stdout.toString().contains('RUNNING')) { final isReachable = await request.quickPingHelper(); if (isReachable) { if (createdNewKey && authKey != null) { await _restartServiceWithAuthKey(authKey); } return true; } } final status = await checkService(); if (status == WindowsHelperServiceStatus.running) { if (createdNewKey && authKey != null) { await _restartServiceWithAuthKey(authKey); } return true; } await _killProcess(helperPort); final command = [ '/c', if (status == WindowsHelperServiceStatus.presence) ...[ 'sc', 'delete', appHelperService, '/force', '&&', ], 'sc', 'create', appHelperService, 'binPath= "${appPath.helperPath}"', 'start= auto', '&&', if (authKey != null) ...[ 'sc', 'config', appHelperService, 'Environment= HELPER_AUTH_KEY=$authKey', '&&', ], 'sc', 'start', appHelperService, ].join(' '); final res = runas('cmd.exe', command); for (int i = 0; i < 10; i++) { await Future.delayed(const Duration(milliseconds: 200)); if (await request.quickPingHelper()) return true; if (i > 0 && i % 4 == 0) { final check = await Process.run('sc', ['query', appHelperService]); final out = check.stdout.toString(); if (out.contains('STOPPED') || out.contains('FAILED')) { commonPrint.log('Helper service stopped/failed, skipping wait'); break; } } } return res; } Future _restartServiceWithAuthKey(String authKey) async { try { await Process.run('sc', ['stop', appHelperService]); await Future.delayed(Duration(milliseconds: 500)); await Process.run('sc', [ 'config', appHelperService, 'Environment= HELPER_AUTH_KEY=$authKey', ]); await Process.run('sc', ['start', appHelperService]); await Future.delayed(Duration(milliseconds: 500)); } catch (e) { commonPrint.log('Failed to restart service with auth key: $e'); } } Future registerTask( String appName, { bool requireNetwork = true, }) async { final executablePath = Platform.resolvedExecutable; final workingDirectory = dirname(executablePath); final taskXml = ''' 开机自动启动代理服务 \\$appName InteractiveToken HighestAvailable true IgnoreNew false false false true $requireNetwork false false true true false false false PT0S 6 "$executablePath" $workingDirectory '''; final taskPath = join(await appPath.tempPath, 'task.xml'); await File(taskPath).create(recursive: true); await File( taskPath, ).writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true); final commandLine = [ '/Create', '/TN', appName, '/XML', '%s', '/F', ].join(' '); return runas('schtasks', commandLine.replaceFirst('%s', taskPath)); } Future unregisterTask(String appName) async { final commandLine = ['/Delete', '/TN', appName, '/F'].join(' '); return runas('schtasks', commandLine); } } final windows = system.isWindows ? Windows() : null; class MacOS { static MacOS? _instance; List? originDns; MacOS._internal(); factory MacOS() { _instance ??= MacOS._internal(); return _instance!; } Future get defaultServiceName async { final result = await Process.run('route', ['-n', 'get', 'default']); final output = result.stdout.toString(); final deviceLine = output .split('\n') .firstWhere((s) => s.contains('interface:'), orElse: () => ''); final parts = deviceLine.trim().split(' '); if (parts.length != 2) return null; final device = parts[1]; final serviceResult = await Process.run('networksetup', [ '-listnetworkserviceorder', ]); final serviceOutput = serviceResult.stdout.toString(); final currentService = serviceOutput .split('\n\n') .firstWhere((s) => s.contains('Device: $device'), orElse: () => ''); if (currentService.isEmpty) return null; final serviceNameLine = currentService .split('\n') .firstWhere( (line) => RegExp(r'^\(\d+\).*').hasMatch(line), orElse: () => '', ); final nameParts = serviceNameLine.trim().split(' '); if (nameParts.length < 2) return null; return nameParts[1]; } Future?> get systemDns async { final deviceServiceName = await defaultServiceName; if (deviceServiceName == null) return null; final result = await Process.run('networksetup', [ '-getdnsservers', deviceServiceName, ]); final output = result.stdout.toString().trim(); originDns = output.startsWith("There aren't any DNS Servers set on") ? [] : output.split('\n'); return originDns; } Future updateDns(bool restore) async { final serviceName = await defaultServiceName; if (serviceName == null) return; List? nextDns; if (restore) { nextDns = originDns; } else { final currentDns = await systemDns; if (currentDns == null) return; const needAddDns = '223.5.5.5'; if (currentDns.contains(needAddDns)) return; nextDns = List.from(currentDns)..add(needAddDns); } if (nextDns == null) return; await Process.run('networksetup', [ '-setdnsservers', serviceName, if (nextDns.isNotEmpty) ...nextDns, if (nextDns.isEmpty) 'Empty', ]); } } final macOS = system.isMacOS ? MacOS() : null; ================================================ FILE: lib/common/task.dart ================================================ import 'dart:convert'; import 'package:flutter/foundation.dart'; /// Encode data to YAML string in a separate isolate to avoid blocking UI /// Note: This uses JSON encoding which is a valid subset of YAML Future encodeYamlTask(T data) async { return await compute(_encodeYaml, data); } /// Internal function to encode YAML (runs in isolate) /// Uses JSON encoding as it's a valid YAML subset and more readable for config preview Future _encodeYaml(T content) async { // Use pretty-printed JSON which is valid YAML and more readable const encoder = JsonEncoder.withIndent(' '); return encoder.convert(content); } ================================================ FILE: lib/common/text.dart ================================================ import 'package:bett_box/enum/enum.dart'; import 'package:flutter/material.dart'; import 'color.dart'; extension TextStyleExtension on TextStyle { TextStyle get toLight => copyWith(color: color?.opacity80); TextStyle get toLighter => copyWith(color: color?.opacity60); TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500); TextStyle get toBold => copyWith(fontWeight: FontWeight.bold); TextStyle get toJetBrainsMono => copyWith(fontFamily: FontFamily.jetBrainsMono.value); TextStyle adjustSize(int size) => copyWith(fontSize: fontSize! + size); } ================================================ FILE: lib/common/theme.dart ================================================ import 'package:bett_box/common/common.dart'; import 'package:flutter/material.dart'; class CommonTheme { final BuildContext context; final Map _colorMap; final double textScaleFactor; CommonTheme.of(this.context, this.textScaleFactor) : _colorMap = {}; Color get darkenSecondaryContainer { return _colorMap.updateCacheValue( 'darkenSecondaryContainer', () => context.colorScheme.secondaryContainer.blendDarken( context, factor: 0.1, ), ); } Color get darkenSecondaryContainerLighter { return _colorMap.updateCacheValue( 'darkenSecondaryContainerLighter', () => context.colorScheme.secondaryContainer .blendDarken(context, factor: 0.1) .opacity60, ); } Color get darken2SecondaryContainer { return _colorMap.updateCacheValue( 'darken2SecondaryContainer', () => context.colorScheme.secondaryContainer.blendDarken( context, factor: 0.2, ), ); } Color get darken3PrimaryContainer { return _colorMap.updateCacheValue( 'darken3PrimaryContainer', () => context.colorScheme.primaryContainer.blendDarken( context, factor: 0.3, ), ); } } ================================================ FILE: lib/common/tray.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'common.dart'; class Tray { Timer? _debounceTimer; TrayState? _pendingState; bool _isUpdating = false; static const _debounceDelay = Duration(milliseconds: 300); Future _updateSystemTray({ required Brightness? brightness, required bool isStart, bool force = false, }) async { if (system.isAndroid) { return; } if (force) { await trayManager.destroy(); } await trayManager.setIcon( utils.getTrayIconPath( brightness: brightness ?? WidgetsBinding.instance.platformDispatcher.platformBrightness, isStart: isStart, ), isTemplate: system.isMacOS, ); if (!Platform.isLinux) { await trayManager.setToolTip(appName); } } Future update({ required TrayState trayState, bool focus = false, }) async { if (system.isAndroid) { return; } _debounceTimer?.cancel(); if (_isUpdating) { _pendingState = trayState; return; } if (focus) { await _doUpdate(trayState: trayState, focus: focus); } else { _debounceTimer = Timer(_debounceDelay, () async { await _doUpdate(trayState: trayState, focus: focus); }); } } Future _doUpdate({ required TrayState trayState, bool focus = false, }) async { if (_isUpdating) return; _isUpdating = true; try { if (!Platform.isLinux) { await _updateSystemTray( brightness: trayState.brightness, isStart: trayState.isStart, force: focus, ); } List menuItems = []; final showMenuItem = MenuItem( label: appLocalizations.show, onClick: (_) { window?.show(); }, ); menuItems.add(showMenuItem); final startMenuItem = MenuItem.checkbox( label: trayState.isStart ? appLocalizations.stop : appLocalizations.start, onClick: (_) async { globalState.appController.updateStart(); }, checked: false, ); menuItems.add(startMenuItem); menuItems.add(MenuItem.separator()); for (final mode in Mode.values) { menuItems.add( MenuItem.checkbox( label: Intl.message(mode.name), onClick: (_) { globalState.appController.changeMode(mode); }, checked: mode == trayState.mode, ), ); } menuItems.add(MenuItem.separator()); for (final group in trayState.groups) { List subMenuItems = []; for (final proxy in group.all) { subMenuItems.add( MenuItem.checkbox( label: proxy.name, checked: trayState.selectedMap[group.name] == proxy.name, onClick: (_) { final appController = globalState.appController; appController.updateCurrentSelectedMap(group.name, proxy.name); appController.changeProxy( groupName: group.name, proxyName: proxy.name, ); }, ), ); } menuItems.add( MenuItem.submenu( label: group.name, submenu: Menu(items: subMenuItems), ), ); } if (trayState.groups.isNotEmpty) { menuItems.add(MenuItem.separator()); } if (trayState.isStart) { menuItems.add( MenuItem.checkbox( label: appLocalizations.tun, onClick: (_) { globalState.appController.updateTun(); }, checked: trayState.tunEnable, ), ); menuItems.add( MenuItem.checkbox( label: appLocalizations.systemProxy, onClick: (_) { globalState.appController.updateSystemProxy(); }, checked: trayState.systemProxy, ), ); menuItems.add(MenuItem.separator()); } final autoStartMenuItem = MenuItem.checkbox( label: appLocalizations.autoLaunch, onClick: (_) async { globalState.appController.updateAutoLaunch(); }, checked: trayState.autoLaunch, ); final copyEnvVarMenuItem = MenuItem( label: appLocalizations.copyEnvVar, onClick: (_) async { await _copyEnv(trayState.port); }, ); menuItems.add(autoStartMenuItem); menuItems.add(copyEnvVarMenuItem); if (!system.isAndroid) { final wakelockMenuItem = MenuItem.checkbox( label: appLocalizations.wakelock, onClick: (_) async { await _toggleWakelock(); }, checked: trayState.wakelockEnabled, ); menuItems.add(wakelockMenuItem); } menuItems.add(MenuItem.separator()); final exitMenuItem = MenuItem( label: appLocalizations.exit, onClick: (_) async { await globalState.appController.handleExit(); }, ); menuItems.add(exitMenuItem); final menu = Menu(items: menuItems); await trayManager.setContextMenu(menu); if (Platform.isLinux) { await _updateSystemTray( brightness: trayState.brightness, isStart: trayState.isStart, force: focus, ); } } finally { _isUpdating = false; if (_pendingState != null) { final pending = _pendingState; _pendingState = null; await _doUpdate(trayState: pending!, focus: false); } } } Future _copyEnv(int port) async { final url = 'http://127.0.0.1:$port'; final cmdline = system.isWindows ? 'set \$env:all_proxy=$url' : 'export all_proxy=$url'; await Clipboard.setData(ClipboardData(text: cmdline)); } Future _toggleWakelock() async { try { final enabled = await WakelockPlus.enabled; if (enabled) { await WakelockPlus.disable(); globalState.appController.stopWakelockAutoRecovery(); } else { await WakelockPlus.enable(); globalState.appController.startWakelockAutoRecovery(); } globalState.updateWakelockState(!enabled); await globalState.appController.updateTray(); } catch (e) { commonPrint.log('WakeLock toggle error: $e'); } } } final tray = Tray(); ================================================ FILE: lib/common/ui_manager.dart ================================================ import 'dart:io'; import 'dart:isolate'; import 'package:archive/archive_io.dart'; import 'package:flutter/services.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/state.dart'; import 'package:path/path.dart'; class UiManager { static UiManager? _instance; UiManager._internal(); factory UiManager() { _instance ??= UiManager._internal(); return _instance!; } Future initializeUI() async { try { final uiPath = await appPath.uiPath; final uiDir = Directory(uiPath); final versionFile = File(join(uiPath, '.ui_version')); final currentVersion = globalState.packageInfo.version; if (await uiDir.exists()) { if (await versionFile.exists()) { final existingVersion = await versionFile.readAsString(); if (existingVersion.trim() == currentVersion) { commonPrint.log('UI already up to date (v$currentVersion)'); return; } commonPrint.log('UI version mismatch: $existingVersion -> $currentVersion'); } await clearUI(); } commonPrint.log('Extracting UI from assets...'); await uiDir.create(recursive: true); final zipData = await rootBundle.load('assets/data/zash.zip'); final bytes = Uint8List.fromList(zipData.buffer.asUint8List()); final tempPath = await appPath.tempPath; final tempExtractPath = join( tempPath, 'ui_extract_${DateTime.now().millisecondsSinceEpoch}', ); await Isolate.run(() async { final tempExtractDir = Directory(tempExtractPath); await tempExtractDir.create(recursive: true); try { final archive = ZipDecoder().decodeBytes(bytes); for (final file in archive) { final filename = file.name; final filePath = join(tempExtractPath, filename); if (file.isFile) { final outFile = File(filePath); await outFile.create(recursive: true); await outFile.writeAsBytes(file.content as List); } else { await Directory(filePath).create(recursive: true); } } final extractedFiles = await tempExtractDir.list().toList(); String sourceDir = tempExtractPath; if (extractedFiles.length == 1 && extractedFiles.first is Directory) { sourceDir = extractedFiles.first.path; } await _copyDirectory(Directory(sourceDir), Directory(uiPath)); final vFile = File(join(uiPath, '.ui_version')); await vFile.writeAsString(currentVersion); } finally { if (await tempExtractDir.exists()) { await tempExtractDir.delete(recursive: true); } } }); commonPrint.log('UI extracted successfully to: $uiPath (v$currentVersion)'); } catch (e) { commonPrint.log('Error extracting UI: $e'); rethrow; } } static Future _copyDirectory(Directory source, Directory destination) async { await for (final entity in source.list(recursive: false)) { if (entity is Directory) { final newDirectory = Directory( join(destination.path, basename(entity.path)), ); await newDirectory.create(recursive: true); await _copyDirectory(entity, newDirectory); } else if (entity is File) { final newFile = File(join(destination.path, basename(entity.path))); await entity.copy(newFile.path); } } } Future clearUI() async { try { final uiPath = await appPath.uiPath; final uiDir = Directory(uiPath); if (await uiDir.exists()) { await uiDir.delete(recursive: true); commonPrint.log('UI cleared successfully'); } } catch (e) { commonPrint.log('Error clearing UI: $e'); } } } final uiManager = UiManager(); ================================================ FILE: lib/common/utils.dart ================================================ import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:ui'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:flutter/foundation.dart'; import 'package:bett_box/l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lpinyin/lpinyin.dart'; class Utils { Color? getDelayColor(int? delay) { if (delay == null) return null; if (delay < 0) return Colors.red; if (delay < 600) return Colors.green; return const Color(0xFFC57F0A); } String get id { final timestamp = DateTime.now().microsecondsSinceEpoch; final random = Random(); final randomStr = String.fromCharCodes( List.generate(8, (_) => random.nextInt(26) + 97), ); return '$timestamp$randomStr'; } String getDateStringLast2(int value) { var valueRaw = '0$value'; return valueRaw.substring(valueRaw.length - 2); } String generateRandomString({int minLength = 10, int maxLength = 100}) { const latinChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; final random = Random(); int length = minLength + random.nextInt(maxLength - minLength + 1); String result = ''; for (int i = 0; i < length; i++) { if (random.nextBool()) { result += String.fromCharCode( 0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1), ); } else { result += latinChars[random.nextInt(latinChars.length)]; } } return result; } String get uuidV4 { final Random random = Random(); final bytes = List.generate(16, (_) => random.nextInt(256)); bytes[6] = (bytes[6] & 0x0F) | 0x40; bytes[8] = (bytes[8] & 0x3F) | 0x80; final hex = bytes .map((byte) => byte.toRadixString(16).padLeft(2, '0')) .join(); return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}'; } String getTimeDifference(DateTime dateTime) { var currentDateTime = DateTime.now(); var difference = currentDateTime.difference(dateTime); var inHours = difference.inHours; var inMinutes = difference.inMinutes; var inSeconds = difference.inSeconds; return '${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}'; } String getTimeText(int? timeStamp) { if (timeStamp == null) { return '00:00:00'; } final diff = timeStamp / 1000; final inHours = (diff / 3600).floor(); if (inHours > 99) { return '99:59:59'; } final inMinutes = (diff / 60 % 60).floor(); final inSeconds = (diff % 60).floor(); return '${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}'; } Locale getSystemLocale() { final platformLocale = WidgetsBinding.instance.platformDispatcher.locale; final supportedLocales = AppLocalizations.delegate.supportedLocales; if (platformLocale.languageCode.toLowerCase() == 'zh') { final isTraditional = (platformLocale.countryCode?.toUpperCase() == 'TW') || (platformLocale.countryCode?.toUpperCase() == 'HK') || (platformLocale.countryCode?.toUpperCase() == 'MO') || (platformLocale.scriptCode?.toLowerCase() == 'hant'); return isTraditional ? const Locale('zh', 'TC') : const Locale('zh', 'CN'); } for (final locale in supportedLocales) { if (locale.languageCode == platformLocale.languageCode) { return locale; } } return const Locale('zh', 'CN'); } Locale? getLocaleForString(String? localString) { if (localString == null) return null; var localSplit = localString.split('_'); if (localSplit.length == 1) { return Locale(localSplit[0]); } if (localSplit.length == 2) { return Locale(localSplit[0], localSplit[1]); } if (localSplit.length == 3) { return Locale.fromSubtags( languageCode: localSplit[0], scriptCode: localSplit[1], countryCode: localSplit[2], ); } return null; } int sortByChar(String a, String b) { if (a.isEmpty && b.isEmpty) { return 0; } if (a.isEmpty) { return -1; } if (b.isEmpty) { return 1; } final charA = a[0]; final charB = b[0]; if (charA == charB) { return sortByChar(a.substring(1), b.substring(1)); } else { return charA.compareToLower(charB); } } String getOverwriteLabel(String label) { final reg = RegExp(r'\((\d+)\)$'); final matches = reg.allMatches(label); if (matches.isNotEmpty) { final match = matches.last; final number = int.parse(match[1] ?? '0') + 1; return label.replaceFirst(reg, '($number)', label.length - 3 - 1); } else { return '$label(1)'; } } String getTrayIconPath({ required Brightness brightness, bool isStart = false, }) { if (system.isMacOS) { return 'assets/images/icon_template.png'; } if (system.isLinux) { return 'assets/images/icon.png'; } final suffix = system.isWindows ? 'ico' : 'png'; return switch (brightness) { Brightness.dark => !isStart ? 'assets/images/icon.$suffix' : 'assets/images/icon_white.$suffix', Brightness.light => !isStart ? 'assets/images/icon_light.$suffix' : 'assets/images/icon_black.$suffix', }; } int compareVersions(String version1, String version2) { List v1 = version1.split('+')[0].split('.'); List v2 = version2.split('+')[0].split('.'); int major1 = int.parse(v1[0]); int major2 = int.parse(v2[0]); if (major1 != major2) { return major1.compareTo(major2); } int minor1 = v1.length > 1 ? int.parse(v1[1]) : 0; int minor2 = v2.length > 1 ? int.parse(v2[1]) : 0; if (minor1 != minor2) { return minor1.compareTo(minor2); } int patch1 = v1.length > 2 ? int.parse(v1[2]) : 0; int patch2 = v2.length > 2 ? int.parse(v2[2]) : 0; if (patch1 != patch2) { return patch1.compareTo(patch2); } int build1 = version1.contains('+') ? int.parse(version1.split('+')[1]) : 0; int build2 = version2.contains('+') ? int.parse(version2.split('+')[1]) : 0; return build1.compareTo(build2); } String getPinyin(String value) { return value.isNotEmpty ? PinyinHelper.getFirstWordPinyin(value.substring(0, 1)) : ''; } String? getFileNameForDisposition(String? disposition) { if (disposition == null) return null; final parseValue = HeaderValue.parse(disposition); final parameters = parseValue.parameters; final fileNamePointKey = parameters.keys.firstWhere( (key) => key == 'filename*', orElse: () => '', ); if (fileNamePointKey.isNotEmpty) { final res = parameters[fileNamePointKey]?.split("''") ?? []; if (res.length >= 2) { return Uri.decodeComponent(res[1]); } } final fileNameKey = parameters.keys.firstWhere( (key) => key == 'filename', orElse: () => '', ); if (fileNameKey.isEmpty) return null; return parameters[fileNameKey]; } FlutterView getScreen() { return WidgetsBinding.instance.platformDispatcher.views.first; } List parseReleaseBody(String? body) { if (body == null) return []; const pattern = r'- \s*(.*)'; final regex = RegExp(pattern); return regex .allMatches(body) .map((match) => match.group(1) ?? '') .where((item) => item.isNotEmpty) .toList(); } ViewMode getViewMode(double viewWidth) { if (viewWidth <= maxMobileWidth) return ViewMode.mobile; if (viewWidth <= maxLaptopWidth) return ViewMode.laptop; return ViewMode.desktop; } int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) { final columns = max((viewWidth / 300).ceil(), 2); return switch (proxiesLayout) { ProxiesLayout.tight => columns + 1, ProxiesLayout.standard => columns, ProxiesLayout.loose => columns - 1, }; } int getProfilesColumns(double viewWidth) { return min(max((viewWidth / 320).floor(), 1), 3); } final _indexPrimary = [50, 100, 200, 300, 400, 500, 600, 700, 800, 850, 900]; MaterialColor _createPrimarySwatch(Color color) { final Map swatch = {}; final int a = color.alpha8bit; final int r = color.red8bit; final int g = color.green8bit; final int b = color.blue8bit; for (final int strength in _indexPrimary) { final double ds = 0.5 - strength / 1000; swatch[strength] = Color.fromARGB( a, r + ((ds < 0 ? r : (255 - r)) * ds).round(), g + ((ds < 0 ? g : (255 - g)) * ds).round(), b + ((ds < 0 ? b : (255 - b)) * ds).round(), ); } swatch[50] = swatch[50]!.lighten(18); swatch[100] = swatch[100]!.lighten(16); swatch[200] = swatch[200]!.lighten(14); swatch[300] = swatch[300]!.lighten(10); swatch[400] = swatch[400]!.lighten(6); swatch[700] = swatch[700]!.darken(2); swatch[800] = swatch[800]!.darken(3); swatch[900] = swatch[900]!.darken(4); return MaterialColor(color.value32bit, swatch); } List getMaterialColorShades(Color color) { final swatch = _createPrimarySwatch(color); return [ if (swatch[50] != null) swatch[50]!, if (swatch[100] != null) swatch[100]!, if (swatch[200] != null) swatch[200]!, if (swatch[300] != null) swatch[300]!, if (swatch[400] != null) swatch[400]!, if (swatch[500] != null) swatch[500]!, if (swatch[600] != null) swatch[600]!, if (swatch[700] != null) swatch[700]!, if (swatch[800] != null) swatch[800]!, if (swatch[850] != null) swatch[850]!, if (swatch[900] != null) swatch[900]!, ]; } String getBackupFileName() { return '${appName}_backup_${DateTime.now().show}.zip'; } String get logFile { return '${appName}_${DateTime.now().show}.log'; } Future getLocalIpAddress() async { List interfaces = await NetworkInterface.list(includeLoopback: false) ..sort((a, b) { if (a.isWifi && !b.isWifi) return -1; if (!a.isWifi && b.isWifi) return 1; if (a.includesIPv4 && !b.includesIPv4) return -1; if (!a.includesIPv4 && b.includesIPv4) return 1; return 0; }); for (final interface in interfaces) { final addresses = interface.addresses; if (addresses.isEmpty) { continue; } addresses.sort((a, b) { if (a.isIPv4 && !b.isIPv4) return -1; if (!a.isIPv4 && b.isIPv4) return 1; return 0; }); return addresses.first.address; } return ''; } SingleActivator controlSingleActivator(LogicalKeyboardKey trigger) { final control = system.isMacOS ? false : true; return SingleActivator(trigger, control: control, meta: !control); } FutureOr handleWatch(Function function) async { if (kDebugMode) { final stopwatch = Stopwatch()..start(); final res = await function(); stopwatch.stop(); commonPrint.log('Time:${stopwatch.elapsedMilliseconds} ms'); return res; } return await function(); } String generateSecret() { final random = Random(); // Generate an 8-digit number (10000000 to 99999999) final secret = 10000000 + random.nextInt(90000000); return secret.toString(); } } final utils = Utils(); ================================================ FILE: lib/common/window.dart ================================================ import 'dart:io'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart' as acrylic; import 'package:screen_retriever/screen_retriever.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; class Window { Future init(int version) async { final props = globalState.config.windowProps; final acquire = await singleInstanceLock.acquire(); if (!acquire) { exit(0); } if (system.isWindows) { protocol.register('clash'); protocol.register('clashmeta'); protocol.register('bettbox'); } if ((version > 10 && system.isMacOS)) { await acrylic.Window.initialize(); } await windowManager.ensureInitialized(); WindowOptions windowOptions = WindowOptions( size: Size(props.width, props.height), minimumSize: const Size(380, 400), ); if (!system.isMacOS || version > 10) { await windowManager.setTitleBarStyle(TitleBarStyle.hidden); } if (!system.isMacOS) { final left = props.left ?? 0; final top = props.top ?? 0; final right = left + props.width; final bottom = top + props.height; if (left == 0 && top == 0) { await windowManager.setAlignment(Alignment.center); } else { final displays = await screenRetriever.getAllDisplays(); final isPositionValid = displays.any((display) { final displayBounds = Rect.fromLTWH( display.visiblePosition!.dx, display.visiblePosition!.dy, display.size.width, display.size.height, ); return displayBounds.contains(Offset(left, top)) || displayBounds.contains(Offset(right, bottom)); }); if (isPositionValid) { await windowManager.setPosition(Offset(left, top)); } } } await windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.setPreventClose(true); }); if (props.isLocked) { try { await windowManager.setResizable(false); } catch (e) { commonPrint.log('Failed to apply the locked state: $e'); } } } void updateMacOSBrightness(Brightness brightness) { if (!system.isMacOS) { return; } acrylic.Window.overrideMacOSBrightness(dark: brightness == Brightness.dark); } Future show() async { globalState.handleForeground(); render?.resume(); await windowManager.show(); await windowManager.focus(); await windowManager.setSkipTaskbar(false); await globalState.resumeForegroundUpdates(); await globalState.appController.syncWakelockIfNeeded(); } Future get isVisible async { return await windowManager.isVisible(); } Future get isMinimized async { return await windowManager.isMinimized(); } Future close() async { try { await trayManager.destroy(); commonPrint.log('The tray icon has been destroyed.'); } catch (e) { commonPrint.log('Failed to destroy the tray icon: $e'); } exit(0); } Future hide() async { await windowManager.hide(); await windowManager.setSkipTaskbar(true); await globalState.handleBackground(); } } final window = system.isDesktop ? Window() : null; ================================================ FILE: lib/controller.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'package:archive/archive_io.dart'; import 'package:bett_box/clash/clash.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/plugins/app.dart'; import 'package:bett_box/plugins/service.dart' as vpn_service; import 'package:bett_box/providers/providers.dart'; import 'package:bett_box/state.dart'; import 'package:bett_box/widgets/dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:yaml/yaml.dart'; import 'common/common.dart'; import 'common/flclash_database_extractor.dart'; import 'models/models.dart'; import 'views/profiles/override_profile.dart'; class AppController { int? lastProfileModified; final BuildContext context; final WidgetRef _ref; Timer? _wakelockSyncTimer; Completer? _restartLock; Completer? _exitLock; int _backgroundLoadVersion = 0; int _updateGroupsRetryCount = 0; bool _isUpdatingGroups = false; AppController(this.context, WidgetRef ref) : _ref = ref; void setupClashConfigDebounce() { debouncer.call(FunctionTag.setupClashConfig, () async { await setupClashConfig(); }); } void updateClashConfigDebounce() { debouncer.call(FunctionTag.updateClashConfig, () async { await updateClashConfig(); }); } void updateGroupsDebounce() { debouncer.call(FunctionTag.updateGroups, updateGroups); } void addCheckIpNumDebounce() { debouncer.call(FunctionTag.addCheckIpNum, () { _ref.read(checkIpNumProvider.notifier).add(); }); } void addCheckIp() { _ref.read(checkIpNumProvider.notifier).add(); } void applyProfileDebounce({bool silence = false}) { debouncer.call(FunctionTag.applyProfile, (silence) { applyProfile(silence: silence); }, args: [silence]); } void savePreferencesDebounce() { debouncer.call(FunctionTag.savePreferences, savePreferences); } void changeProxyDebounce(String groupName, String proxyName) { debouncer.call(FunctionTag.changeProxy, ( String groupName, String proxyName, ) async { await changeProxy(groupName: groupName, proxyName: proxyName); await updateGroups(); addCheckIp(); }, args: [groupName, proxyName]); } Future restartCore() async { if (_restartLock != null) { return _restartLock!.future; } _restartLock = Completer(); commonPrint.log('restart core'); try { final wasRunning = _ref.read(runTimeProvider.notifier).isStart; if (wasRunning) { await globalState.handleStop(); } await Future.delayed(const Duration(milliseconds: 500)); await _initCore(); if (wasRunning) { if (system.isDesktop) { await _fastStart(); } else { await globalState.handleStart(); } } } finally { _restartLock?.complete(); _restartLock = null; } } Future updateStatus(bool isStart) async { if (isStart) { await _fastStart(); } else { await globalState.handleStop(); clashCore.resetTraffic(); _ref.read(trafficsProvider.notifier).clear(); _ref.read(totalTrafficProvider.notifier).value = Traffic(); _ref.read(runTimeProvider.notifier).value = null; addCheckIpNumDebounce(); } } Future _fastStart() async { final patchConfig = _ref.read(patchClashConfigProvider); final isDesktop = system.isDesktop; if (isDesktop && patchConfig.tun.enable) { await _quickSetupConfig(enableTun: false); await globalState.handleStart([updateRunTime, updateTraffic]); Future.microtask(() async { final res = await _requestAdmin(true); if (res.needRestart) { await restartCore(); } else if (!res.isError) { await _updateClashConfig(); } _backgroundLoad(); }); _scheduleCheckIpRefresh(); return; } final needReapply = await _checkIfNeedReapply(); if (needReapply) { await _quickSetupConfig(); } await globalState.handleStart([updateRunTime, updateTraffic]); _scheduleCheckIpRefresh(); _backgroundLoad(); } void _scheduleCheckIpRefresh() { Future.delayed(const Duration(seconds: 1), () { addCheckIpNumDebounce(); }); } void _backgroundLoad() { final version = ++_backgroundLoadVersion; Future.microtask(() async { try { final groups = await clashCore.getProxiesGroups(); if (version != _backgroundLoadVersion) return; final providers = await clashCore.getExternalProviders(); if (version != _backgroundLoadVersion) return; _ref.read(groupsProvider.notifier).value = groups; _ref.read(providersProvider.notifier).value = providers; await Future.delayed(const Duration(seconds: 2)); if (version != _backgroundLoadVersion) return; await clashCore.requestGc(); } catch (e) { commonPrint.log('Background load error: $e'); } }); } Future _checkIfNeedReapply() async { final currentLastModified = await _ref .read(currentProfileProvider) ?.profileLastModified; if (currentLastModified != null && lastProfileModified != null && currentLastModified <= lastProfileModified!) { return false; } return true; } Future _setupCoreConfig({bool? enableTun}) async { await _ref.read(currentProfileProvider)?.checkAndUpdate(); final patchConfig = _ref.read(patchClashConfigProvider); final targetTun = enableTun ?? patchConfig.tun.enable; final realTunEnable = await _prepareTun(targetTun); if (realTunEnable == null) return; final realPatchConfig = patchConfig.copyWith.tun(enable: realTunEnable); final params = await globalState.getSetupParams( pathConfig: realPatchConfig, ); final message = await clashCore.setupConfig(params); if (message.isNotEmpty) { await _rollbackConfig(); throw message; } globalState.backupSuccessfulConfig(params); lastProfileModified = await _ref.read( currentProfileProvider.select((state) => state?.profileLastModified), ); } Future _quickSetupConfig({bool? enableTun}) async { await safeRun(() async { await _setupCoreConfig(enableTun: enableTun); }, needLoading: false); } Future updateRunTime() async { final startTime = globalState.startTime; if (startTime == null) { if (_ref.read(runTimeProvider) != null) { _ref.read(runTimeProvider.notifier).value = null; } return; } final startTimeStamp = startTime.millisecondsSinceEpoch; final nowTimeStamp = DateTime.now().millisecondsSinceEpoch; final elapsed = nowTimeStamp - startTimeStamp; final current = _ref.read(runTimeProvider); if (current == null) { _ref.read(runTimeProvider.notifier).value = elapsed; return; } _ref.read(runTimeProvider.notifier).value = elapsed; } Future _shouldUpdateDashboardTick() async { final lifecycleState = WidgetsBinding.instance.lifecycleState; if (lifecycleState != AppLifecycleState.resumed) return false; if (system.isDesktop) { if (await window?.isVisible == false) return false; if (await window?.isMinimized == true) return false; } return true; } Future updateTraffic() async { _ref.read(totalTrafficProvider.notifier).value = await clashCore .getTotalTraffic(); if (!await _shouldUpdateDashboardTick()) { return; } final traffic = await clashCore.getTraffic(); _ref.read(trafficsProvider.notifier).addTraffic(traffic); } Future addProfile(Profile profile) async { _ref.read(profilesProvider.notifier).setProfile(profile); if (_ref.read(currentProfileIdProvider) != null) return; _ref.read(currentProfileIdProvider.notifier).value = profile.id; } Future deleteProfile(String id) async { _ref.read(profilesProvider.notifier).deleteProfileById(id); await clearEffect(id); if (globalState.config.currentProfileId == id) { final profiles = globalState.config.profiles; final currentProfileId = _ref.read(currentProfileIdProvider.notifier); if (profiles.isNotEmpty) { final updateId = profiles.first.id; currentProfileId.value = updateId; } else { currentProfileId.value = null; updateStatus(false); } } } Future updateProviders() async { _ref.read(providersProvider.notifier).value = await clashCore .getExternalProviders(); } Future updateLocalIp() async { _ref.read(localIpProvider.notifier).value = null; await Future.delayed(commonDuration); _ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress(); } Future updateProfile(Profile profile) async { final newProfile = await profile.update(); _ref .read(profilesProvider.notifier) .setProfile(newProfile.copyWith(isUpdating: false)); if (profile.id == _ref.read(currentProfileIdProvider)) { applyProfileDebounce(silence: true); } } void setProfile(Profile profile) { _ref.read(profilesProvider.notifier).setProfile(profile); } void setProfileAndAutoApply(Profile profile) { _ref.read(profilesProvider.notifier).setProfile(profile); if (profile.id == _ref.read(currentProfileIdProvider)) { applyProfileDebounce(silence: true); } } void setProfiles(List profiles) { _ref.read(profilesProvider.notifier).value = profiles; } void addLog(Log log) { _ref.read(logsProvider).add(log); } void updateOrAddHotKeyAction(HotKeyAction hotKeyAction) { final hotKeyActions = _ref.read(hotKeyActionsProvider); final index = hotKeyActions.indexWhere( (item) => item.action == hotKeyAction.action, ); final newList = List.of(hotKeyActions); if (index == -1) { newList.add(hotKeyAction); } else { newList[index] = hotKeyAction; } _ref.read(hotKeyActionsProvider.notifier).value = newList; } List getCurrentGroups() { return _ref.read(currentGroupsStateProvider.select((state) => state.value)); } String getRealTestUrl(String? url) { return _ref.read(getRealTestUrlProvider(url)); } int getProxiesColumns() { return _ref.read(getProxiesColumnsProvider); } dynamic addSortNum() { return _ref.read(sortNumProvider.notifier).add(); } String? getCurrentGroupName() { final currentGroupName = _ref.read( currentProfileProvider.select((state) => state?.currentGroupName), ); return currentGroupName; } ProxyCardState getProxyCardState(String proxyName) { return _ref.read(getProxyCardStateProvider(proxyName)); } String? getSelectedProxyName(String groupName) { return _ref.read(getSelectedProxyNameProvider(groupName)); } void updateCurrentGroupName(String groupName) { final profile = _ref.read(currentProfileProvider); if (profile == null || profile.currentGroupName == groupName) { return; } setProfile(profile.copyWith(currentGroupName: groupName)); } Future updateClashConfig() async { await safeRun(() async { await _updateClashConfig(); }, needLoading: true); } Future _prepareTun(bool targetTun) async { final res = await _requestAdmin(targetTun); if (res.needRestart) { await restartCore(); } else if (res.isError) { return null; } return _ref.read(realTunEnableProvider); } Future _updateClashConfig() async { final updateParams = _ref.read(updateParamsProvider); final realTunEnable = await _prepareTun(updateParams.tun.enable); if (realTunEnable == null) return; final message = await clashCore.updateConfig( updateParams.copyWith.tun(enable: realTunEnable), ); if (message.isNotEmpty) throw message; } Future> _requestAdmin(bool enableTun) async { if (system.isWindows && kDebugMode) { return Result.success(false); } final realTunEnable = _ref.read(realTunEnableProvider); if (enableTun != realTunEnable && realTunEnable == false) { final code = await system.authorizeCore(); switch (code) { case AuthorizeCode.success: return Result.success(true, needRestart: true); case AuthorizeCode.none: break; case AuthorizeCode.error: if (system.isWindows) { globalState.showNotifier(appLocalizations.tunEnableRequireAdmin); } enableTun = false; break; } } _ref.read(realTunEnableProvider.notifier).value = enableTun; return Result.success(enableTun); } Future setupClashConfig() async { await safeRun(() async { await _setupCoreConfig(); }, needLoading: true); } Future _applyProfile() async { await clashCore.requestGc(); await setupClashConfig(); await updateGroups(); await updateProviders(); } Future applyProfile({bool silence = false}) async { if (silence) { await _applyProfile(); } else { await safeRun(() async { await _applyProfile(); }, needLoading: true); } addCheckIpNumDebounce(); } void handleChangeProfile() { _ref.read(delayDataSourceProvider.notifier).value = {}; applyProfile(); _ref.read(logsProvider.notifier).value = FixedList(maxLength); _ref.read(requestsProvider.notifier).value = FixedList(maxLength); globalState.computeHeightMapCache = {}; } void updateBrightness() { _ref.read(systemBrightnessProvider.notifier).value = WidgetsBinding.instance.platformDispatcher.platformBrightness; } Future autoUpdateProfiles() async { for (final profile in _ref.read(profilesProvider)) { if (!profile.autoUpdate) continue; final isNotNeedUpdate = profile.lastUpdateDate ?.add(profile.autoUpdateDuration) .isBeforeNow; if (isNotNeedUpdate == false || profile.type == ProfileType.file) { continue; } try { await updateProfile(profile); } catch (e) { commonPrint.log(e.toString()); } } } Future checkAndUpdateMissedProfiles() async { final now = DateTime.now(); final profilesToUpdate = []; for (final profile in _ref.read(profilesProvider)) { if (!profile.autoUpdate) continue; if (profile.type == ProfileType.file) continue; if (profile.isUpdating) continue; final lastUpdate = profile.lastUpdateDate; if (lastUpdate == null) continue; final expectedNextUpdate = lastUpdate.add(profile.autoUpdateDuration); final isOverdue = now.difference(expectedNextUpdate) > const Duration(minutes: 1); if (isOverdue) { profilesToUpdate.add(profile); } } if (profilesToUpdate.isEmpty) return; for (final profile in profilesToUpdate) { try { commonPrint.log('[MissedUpdate] Updating profile: ${profile.label ?? profile.id}'); await updateProfile(profile); } catch (e) { commonPrint.log('[MissedUpdate] Failed to update ${profile.id}: $e'); } if (profilesToUpdate.length > 1) { await Future.delayed(const Duration(seconds: 2)); } } } Future updateGroups() async { if (_isUpdatingGroups) { commonPrint.log('updateGroups already in progress, skipping'); return; } _isUpdatingGroups = true; try { final currentGroups = _ref.read(groupsProvider); final isInitialDesktopLoad = system.isDesktop && currentGroups.isEmpty; final maxAttempts = isInitialDesktopLoad ? 6 : 4; final newGroups = await retry( task: clashCore.getProxiesGroups, retryIf: (res) => res.isEmpty, maxAttempts: maxAttempts, ); _ref.read(groupsProvider.notifier).value = newGroups; _updateGroupsRetryCount = 0; return; } catch (e) { final currentGroups = _ref.read(groupsProvider); final isInitialDesktopLoad = system.isDesktop && currentGroups.isEmpty; final maxRetryRounds = isInitialDesktopLoad ? 8 : 4; final retryDelay = isInitialDesktopLoad ? const Duration(seconds: 1) : const Duration(seconds: 2); if (currentGroups.isNotEmpty) { commonPrint.log('updateGroups error, keeping existing groups: $e'); return; } if (_updateGroupsRetryCount >= maxRetryRounds) { commonPrint.log( 'updateGroups max retries ($maxRetryRounds) reached, giving up', ); _updateGroupsRetryCount = 0; return; } _updateGroupsRetryCount++; commonPrint.log( 'updateGroups initial load failed ($_updateGroupsRetryCount/$maxRetryRounds), scheduling retry in ${retryDelay.inSeconds}s: $e', ); Future.delayed(retryDelay, () { updateGroups(); }); } finally { _isUpdatingGroups = false; } } Future updateProfiles() async { for (final profile in _ref.read(profilesProvider)) { if (profile.type == ProfileType.file) { continue; } await updateProfile(profile); } } Future savePreferences() async { await preferences.saveConfig(globalState.config); } Future changeProxy({ required String groupName, required String proxyName, }) async { await clashCore.changeProxy( ChangeProxyParams(groupName: groupName, proxyName: proxyName), ); if (_ref.read(appSettingProvider).closeConnections) { clashCore.closeConnections(); } addCheckIp(); } Future handleBackOrExit() async { if (_ref.read(backBlockProvider)) { return; } if (system.isDesktop) { await savePreferences(); } await system.back(); } void backBlock() { _ref.read(backBlockProvider.notifier).value = true; } void unBackBlock() { _ref.read(backBlockProvider.notifier).value = false; } Future handleExit() async { if (_exitLock != null) { return _exitLock!.future; } final exitLock = Completer(); _exitLock = exitLock; globalState.isExiting = true; try { stopWakelockAutoRecovery(); await globalState.handleBackground(); await savePreferences(); if (macOS != null) { await macOS!.updateDns(true); } if (proxy != null) { await proxy!.stopProxy(); } await clashCore.shutdown(); if (clashService != null) { await clashService!.destroy(); } } catch (e) { commonPrint.log('handleExit error: $e'); } finally { if (!exitLock.isCompleted) { exitLock.complete(); } system.exit(); } } Future handleClear() async { await preferences.clearPreferences(); commonPrint.log('clear preferences'); globalState.config = Config(themeProps: defaultThemeProps); } Future autoCheckUpdate() async { final prefs = await preferences.sharedPreferencesCompleter.future; final lastCheckTime = prefs?.getInt('last_check_update_time') ?? 0; final now = DateTime.now().millisecondsSinceEpoch; final isAutoCheck = _ref.read(appSettingProvider).autoCheckUpdate; final forceCheck = (now - lastCheckTime) > const Duration(days: 28).inMilliseconds; if (!isAutoCheck && !forceCheck) return; final res = await request.checkForUpdate(); if (res != null) { checkUpdateResultHandle(data: res); } await prefs?.setInt('last_check_update_time', now); } Future checkUpdateResultHandle({ Map? data, bool handleError = false, }) async { if (globalState.isPre && !handleError) { return; } if (data != null) { final tagName = data['tag_name']; final body = data['body']; final submits = utils.parseReleaseBody(body); final textTheme = context.textTheme; final res = await globalState.showMessage( title: appLocalizations.discoverNewVersion, message: TextSpan( text: '$tagName \n', style: textTheme.headlineSmall, children: [ TextSpan(text: '\n', style: textTheme.bodyMedium), for (final submit in submits) TextSpan(text: '- $submit \n', style: textTheme.bodyMedium), ], ), confirmText: appLocalizations.goDownload, ); if (res != true) { return; } launchUrl( Uri.parse('https://github.com/$repository/releases/latest'), mode: LaunchMode.externalApplication, ); } else if (handleError) { globalState.showMessage( title: appLocalizations.checkUpdate, message: TextSpan(text: appLocalizations.checkUpdateError), ); } } Future _handlePreference() async { if (await preferences.isInit) { return; } final res = await globalState.showMessage( title: appLocalizations.tip, message: TextSpan(text: appLocalizations.cacheCorrupt), ); if (res == true) { final file = File(await appPath.sharedPreferencesPath); final isExists = await file.exists(); if (isExists) { await file.delete(); } } await handleExit(); } Future _initCore() async { final isInit = await clashCore.isInit; if (!isInit) { await clashCore.init(); await clashCore.setState(globalState.getCoreState()); } } void startWakelockAutoRecovery() { _wakelockSyncTimer?.cancel(); _wakelockSyncTimer = Timer.periodic(const Duration(seconds: 168), ( _, ) async { try { final userEnabled = _ref.read(wakelockStateProvider); if (!userEnabled) { stopWakelockAutoRecovery(); return; } await syncWakelockIfNeeded(); } catch (_) {} }); } void stopWakelockAutoRecovery() { _wakelockSyncTimer?.cancel(); _wakelockSyncTimer = null; } Future syncWakelockIfNeeded() async { final userEnabled = _ref.read(wakelockStateProvider); if (!userEnabled) { stopWakelockAutoRecovery(); return; } final actualState = await WakelockPlus.enabled; if (actualState) { return; } await WakelockPlus.enable(); } Future _initHighRefreshRateDefault() async { try { final androidVersion = await system.version; final currentSetting = _ref.read(appSettingProvider); final bool shouldEnableHighRefreshRate = androidVersion >= 31; // Android 12+ if (currentSetting.enableHighRefreshRate != shouldEnableHighRefreshRate) { _ref .read(appSettingProvider.notifier) .updateState( (state) => state.copyWith( enableHighRefreshRate: shouldEnableHighRefreshRate, ), ); } } catch (e) { commonPrint.log('Failed to initialize high refresh rate default: $e'); } } Future init() async { FlutterError.onError = (details) { if (kDebugMode) { commonPrint.log(details.stack.toString()); } }; vpn_service.service?.addNativeEventCallback((method, arguments) async { if (method == 'vpnStartFailed') { globalState.showNotifier('Failed, Please try again later'); await updateStatus(false); } }); if (system.isAndroid) { await _initHighRefreshRateDefault(); } try { final wakelockEnabled = await WakelockPlus.enabled; _ref.read(wakelockStateProvider.notifier).state = wakelockEnabled; if (wakelockEnabled) { startWakelockAutoRecovery(); } } catch (e) { commonPrint.log('Failed to check wake lock status: $e'); } await updateTray(true); await _initCore(); try { await _initStatus(); } catch (e) { commonPrint.log('_initStatus failed, falling back to basic startup: $e'); try { await applyProfile(silence: true); } catch (e2) { commonPrint.log('Fallback applyProfile also failed: $e2'); } } autoLaunch?.updateStatus(_ref.read(appSettingProvider).autoLaunch); autoUpdateProfiles(); autoCheckUpdate(); final isWindowVisible = await window?.isVisible ?? false; if (isWindowVisible) { window?.show(); } else { if (!_ref.read(appSettingProvider).silentLaunch) { window?.show(); } else { window?.hide(); } } await syncDesktopRuntimeState(preferCurrentState: true); await updateTray(true); await _handlePreference(); await _handlerDisclaimer(); _ref.read(initProvider.notifier).value = true; } Future _initStatus() async { if (system.isAndroid) { await globalState.updateStartTime(); if (globalState.isStart && _ref.read(runTimeProvider) == null) { _ref.read(runTimeProvider.notifier).value = 0; } } else if (system.isDesktop) { await syncDesktopRuntimeState(); } final needRecovery = await _detectAbnormalExit(); if (needRecovery) { commonPrint.log('Abnormal exit detected'); if (system.isAndroid) { try { await applyProfile(silence: true); } catch (e) { commonPrint.log('Recovery failed: $e'); } } } final shouldStart = globalState.isStart || _ref.read(appSettingProvider).autoRun; if (shouldStart) { try { await updateStatus(true); } catch (e) { commonPrint.log('Auto start failed: $e'); await applyProfile(); addCheckIpNumDebounce(); } } else { await applyProfile(); addCheckIpNumDebounce(); } } Future syncDesktopRuntimeState({ bool preferCurrentState = false, }) async { if (!system.isDesktop) return; if (!preferCurrentState || !globalState.isStart) { await globalState.updateStartTime(); } if (globalState.isStart) { if (_ref.read(runTimeProvider) == null) { _ref.read(runTimeProvider.notifier).value = 0; } await globalState.startUpdateTasks([updateTraffic]); return; } if (_ref.read(runTimeProvider) != null) { _ref.read(runTimeProvider.notifier).value = null; } globalState.stopUpdateTasks(); } Future _detectAbnormalExit() async { final prefs = await preferences.sharedPreferencesCompleter.future; if (system.isAndroid) { final isVpnRunningFlag = prefs?.getBool('is_vpn_running') ?? false; return !globalState.isStart && isVpnRunningFlag; } if (system.isDesktop) { final wasTunRunning = prefs?.getBool('is_tun_running') ?? false; return !globalState.isStart && wasTunRunning; } return false; } void setDelay(Delay delay) { _ref.read(delayDataSourceProvider.notifier).setDelay(delay); } void toPage(PageLabel pageLabel) { final context = globalState.navigatorKey.currentState?.context; if (context != null && context.mounted) { Navigator.of(context, rootNavigator: true).popUntil((route) => route.isFirst); } _ref.read(currentPageLabelProvider.notifier).value = pageLabel; } void toProfiles() { toPage(PageLabel.profiles); } void initLink() { linkManager.initAppLinksListen((url) async { final res = await globalState.showMessage( title: '${appLocalizations.add}${appLocalizations.profile}', message: TextSpan( children: [ TextSpan(text: appLocalizations.doYouWantToPass), TextSpan( text: ' $url ', style: TextStyle( color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline, decorationColor: Theme.of(context).colorScheme.primary, ), ), TextSpan( text: '${appLocalizations.create}${appLocalizations.profile}', ), ], ), ); if (res != true) { return; } addProfileFormURL(url); }); } Future showDisclaimer() async { return await globalState.showCommonDialog( dismissible: false, child: CommonDialog( title: appLocalizations.disclaimer, actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text(appLocalizations.exit), ), TextButton( onPressed: () { _ref .read(appSettingProvider.notifier) .updateState( (state) => state.copyWith(disclaimerAccepted: true), ); Navigator.of(context).pop(true); }, child: Text(appLocalizations.agree), ), ], child: SelectableText(appLocalizations.disclaimerDesc), ), ) ?? false; } Future _handlerDisclaimer() async { if (_ref.read(appSettingProvider).disclaimerAccepted) { return; } final isDisclaimerAccepted = await showDisclaimer(); if (!isDisclaimerAccepted) { await handleExit(); } return; } Future addProfileFormURL(String url) async { if (globalState.navigatorKey.currentState?.canPop() ?? false) { globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); } toProfiles(); final profile = await safeRun( () async { return await Profile.normal(url: url).update(); }, needLoading: true, title: '${appLocalizations.add}${appLocalizations.profile}', ); if (profile != null) { await addProfile(profile); } } Future addProfileFormFile() async { final platformFile = await safeRun(picker.pickerFile); final bytes = platformFile?.bytes; if (bytes == null) { return; } if (!context.mounted) return; globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); toProfiles(); final profile = await safeRun( () async { await Future.delayed(const Duration(milliseconds: 500)); return await Profile.normal(label: platformFile?.name).saveFile(bytes); }, needLoading: true, title: '${appLocalizations.add}${appLocalizations.profile}', ); if (profile != null) { await addProfile(profile); } } Future addProfileFormQrCode() async { final url = await safeRun(picker.pickerConfigQRCode); if (url == null) return; addProfileFormURL(url); } void updateViewSize(Size size) { WidgetsBinding.instance.addPostFrameCallback((_) { _ref.read(viewSizeProvider.notifier).value = size; }); } void setProvider(ExternalProvider? provider) { _ref.read(providersProvider.notifier).setProvider(provider); } List _sortOfName(List proxies) { return List.of(proxies)..sort( (a, b) => utils.sortByChar(utils.getPinyin(a.name), utils.getPinyin(b.name)), ); } int _delayValue(int? delay) => (delay == null || delay == -1) ? 1 << 30 : delay; List _sortOfDelay({required List proxies, String? testUrl}) { return List.of(proxies)..sort((a, b) { final aDelay = _ref.read( getDelayProvider(proxyName: a.name, testUrl: testUrl), ); final bDelay = _ref.read( getDelayProvider(proxyName: b.name, testUrl: testUrl), ); return _delayValue(aDelay).compareTo(_delayValue(bDelay)); }); } List getSortProxies({ required List proxies, required ProxiesSortType sortType, String? testUrl, }) { return switch (sortType) { ProxiesSortType.none => proxies, ProxiesSortType.delay => _sortOfDelay(proxies: proxies, testUrl: testUrl), ProxiesSortType.name => _sortOfName(proxies), }; } Future clearEffect(String profileId) async { final profilePath = await appPath.getProfilePath(profileId); final providersDirPath = await appPath.getProvidersDirPath(profileId); await Isolate.run(() async { final profileFile = File(profilePath); final isExists = await profileFile.exists(); if (isExists) { await profileFile.delete(recursive: true); } final providersFileDir = Directory(providersDirPath); final providersFileIsExists = await providersFileDir.exists(); if (providersFileIsExists) { await providersFileDir.delete(recursive: true); } }); } void updateTun() { _ref .read(patchClashConfigProvider.notifier) .updateState((state) => state.copyWith.tun(enable: !state.tun.enable)); } void updateSystemProxy() { _ref .read(networkSettingProvider.notifier) .updateState( (state) => state.copyWith(systemProxy: !state.systemProxy), ); } Future> getPackages({bool forceRefresh = false}) async { final cached = _ref.read(packagesProvider); if (!forceRefresh && cached.isNotEmpty) return cached; final packages = await app.getPackages(forceRefresh: forceRefresh); _ref.read(packagesProvider.notifier).value = packages; return packages; } void updateStart() { updateStatus(!_ref.read(runTimeProvider.notifier).isStart); } void updateCurrentSelectedMap(String groupName, String proxyName) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile != null && currentProfile.selectedMap[groupName] != proxyName) { final SelectedMap selectedMap = Map.from(currentProfile.selectedMap) ..[groupName] = proxyName; _ref .read(profilesProvider.notifier) .setProfile(currentProfile.copyWith(selectedMap: selectedMap)); } } void updateCurrentUnfoldSet(Set value) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile == null) { return; } _ref .read(profilesProvider.notifier) .setProfile(currentProfile.copyWith(unfoldSet: value)); } void changeMode(Mode mode) { _ref .read(patchClashConfigProvider.notifier) .updateState((state) => state.copyWith(mode: mode)); if (mode == Mode.global) { updateCurrentGroupName(GroupName.GLOBAL.name); } updateGroupsDebounce(); addCheckIpNumDebounce(); } void updateAutoLaunch() { _ref .read(appSettingProvider.notifier) .updateState((state) => state.copyWith(autoLaunch: !state.autoLaunch)); } Future updateVisible() async { final visible = await window?.isVisible; if (visible != null && !visible) { window?.show(); } else { window?.hide(); } } void updateMode() { _ref.read(patchClashConfigProvider.notifier).updateState((state) { final index = Mode.values.indexWhere((item) => item == state.mode); if (index == -1) { return null; } final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1; return state.copyWith(mode: Mode.values[nextIndex]); }); } Future handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async { final res = await globalState.showCommonDialog( child: AddRuleDialog( rule: rule, snippet: ref.read( profileOverrideStateProvider.select((state) => state.snippet!), ), ), ); if (res == null) { return; } ref.read(profileOverrideStateProvider.notifier).updateState((state) { final model = state.copyWith.overrideData!( rule: state.overrideData!.rule.updateRules((rules) { final index = rules.indexWhere((item) => item.id == res.id); if (index == -1) { return List.from([res, ...rules]); } return List.from(rules)..[index] = res; }), ); return model; }); } Future exportLogs() async { final logsRaw = _ref.read(logsProvider).list.map((item) => item.toString()); final data = await Isolate.run>(() async { final logsRawString = logsRaw.join('\n'); return utf8.encode(logsRawString); }); return await picker.saveFile(utils.logFile, Uint8List.fromList(data)) != null; } Future> backupData() async { final homeDirPath = await appPath.homeDirPath; final profilesPath = await appPath.profilesPath; final configJson = globalState.config.toJson(); // Get valid profile IDs final validProfileIds = globalState.config.profiles .map((p) => p.id) .toSet(); final currentProfileId = globalState.config.currentProfileId; commonPrint.log( 'Starting backup: ${validProfileIds.length} profiles, current: $currentProfileId', ); return Isolate.run>(() async { // Use ZipFileEncoder like FLClash - more reliable than ZipEncoder + Archive final tempDir = Directory.systemTemp; final tempZipPath = join( tempDir.path, 'bettbox_backup_${DateTime.now().millisecondsSinceEpoch}.zip', ); final encoder = ZipFileEncoder(); encoder.create(tempZipPath); // Add marker file final markerData = json.encode({ 'app': 'Bettbox', 'version': '1.0', 'timestamp': DateTime.now().millisecondsSinceEpoch, }); final markerBytes = utf8.encode(markerData); final tempMarkerFile = File( join( tempDir.path, 'bettbox_marker_${DateTime.now().millisecondsSinceEpoch}.tmp', ), ); await tempMarkerFile.writeAsBytes(markerBytes); await encoder.addFile(tempMarkerFile, '.bettbox_marker'); await tempMarkerFile.delete(); // Add config file final configStr = json.encode(configJson); final tempConfigFile = File( join( tempDir.path, 'bettbox_config_${DateTime.now().millisecondsSinceEpoch}.tmp', ), ); await tempConfigFile.writeAsString(configStr); await encoder.addFile(tempConfigFile, 'config.json'); await tempConfigFile.delete(); // Add profiles dir (valid subscriptions only) final profilesDir = Directory(profilesPath); if (await profilesDir.exists()) { final files = await profilesDir .list(recursive: false) .toList(); // First level only for (final file in files) { if (file is File) { // Check if valid subscription config final fileName = basename(file.path); final profileId = fileName.replaceAll(RegExp(r'\.(yaml|yml)$'), ''); if (validProfileIds.contains(profileId)) { // Normalize path: use Unix-style / separator final relativePath = relative( file.path, from: homeDirPath, ).replaceAll('\\', '/'); await encoder.addFile(file, relativePath); } } } // Add current active subscription Providers if (currentProfileId != null && validProfileIds.contains(currentProfileId)) { final providersDir = Directory( join(profilesPath, 'providers', currentProfileId), ); if (await providersDir.exists()) { final providerFiles = await providersDir .list(recursive: true) .toList(); for (final providerFile in providerFiles) { if (providerFile is File) { final relativePath = relative( providerFile.path, from: homeDirPath, ).replaceAll('\\', '/'); await encoder.addFile(providerFile, relativePath); } } } } } encoder.close(); // Read the zip file and return bytes final zipFile = File(tempZipPath); final bytes = await zipFile.readAsBytes(); await zipFile.delete(); return bytes; }); } Future updateTray([bool focus = false]) async { final trayState = _ref.read(trayStateProvider); await tray.update(trayState: trayState, focus: focus); } Future _processRecoveryArchive( Future Function() getArchive, RecoveryOption recoveryOption, ) async { try { final archive = await getArchive(); commonPrint.log('Archive decoded: ${archive.files.length} files'); await _recoveryFromArchive(archive, recoveryOption); } catch (e) { commonPrint.log('Recovery failed: $e'); throw 'Backup file is corrupted or invalid: $e'; } } /// Restore data from bytes Future recoveryData( List data, RecoveryOption recoveryOption, ) async { commonPrint.log('Starting recovery from bytes: ${data.length} bytes'); await _processRecoveryArchive(() => Isolate.run(() { final zipDecoder = ZipDecoder(); return zipDecoder.decodeBytes(data); }), recoveryOption); } /// Restore data from file path Future recoveryDataFromFile( String path, RecoveryOption recoveryOption, ) async { commonPrint.log('Starting recovery from file: $path'); await _processRecoveryArchive(() => Isolate.run(() { try { final input = InputFileStream(path); final zipDecoder = ZipDecoder(); final result = zipDecoder.decodeStream(input); input.close(); if (result.files.isNotEmpty) { return result; } } catch (e) { commonPrint.log('Stream decoding failed: $e'); } final bytes = File(path).readAsBytesSync(); final zipDecoder = ZipDecoder(); return zipDecoder.decodeBytes(bytes); }), recoveryOption); } /// Unified recovery entry: check marker and dispatch to recovery logic Future _recoveryFromArchive( Archive archive, RecoveryOption recoveryOption, ) async { if (archive.files.isEmpty) { throw 'Backup file is empty or corrupted'; } final homeDirPath = await appPath.homeDirPath; // Check for Bettbox marker final hasBettboxMarker = archive.files.any( (file) => file.name == '.bettbox_marker', ); if (hasBettboxMarker) { // Bettbox backup await _recoveryBettboxBackup(archive, recoveryOption, homeDirPath); } else { // Legacy backup await _recoveryLegacyBackup(archive, recoveryOption, homeDirPath); } } /// Restore Bettbox Future _recoveryBettboxBackup( Archive archive, RecoveryOption recoveryOption, String homeDirPath, ) async { // Separate config and profile files final configs = archive.files .where( (item) => item.name.endsWith('.json') && item.name != '.bettbox_marker', ) .toList(); final profiles = archive.files.where( (item) => !item.name.endsWith('.json') && item.name != '.bettbox_marker', ); // Find config.json final configIndex = configs.indexWhere( (config) => config.name == 'config.json', ); if (configIndex == -1) throw 'invalid backup file'; // Parse config final configFile = configs[configIndex]; final configContent = configFile.content; if (configContent.isEmpty) { throw 'Config file is empty or corrupted'; } var tempConfig = Config.compatibleFromJson( json.decode(utf8.decode(configContent)), ); // Restore profile files to disk for (final profile in profiles) { final filePath = join(homeDirPath, profile.name); final file = File(filePath); await file.create(recursive: true); await file.writeAsBytes(profile.content); } // Apply recovery logic _recovery(tempConfig, recoveryOption); } /// Restore legacy Future _recoveryLegacyBackup( Archive archive, RecoveryOption recoveryOption, String homeDirPath, ) async { // Separate config and profile files final configs = archive.files .where((item) => item.name.endsWith('.json')) .toList(); final profileFiles = archive.files .where( (item) => !item.name.endsWith('.json') && !item.name.endsWith('.sqlite'), ) .toList(); // Find config.json final configIndex = configs.indexWhere( (config) => config.name == 'config.json', ); if (configIndex == -1) throw 'invalid backup file'; // Parse backup config final configFile = configs[configIndex]; final configContent = configFile.content; if (configContent.isEmpty) { throw 'Config file is empty or corrupted'; } final backupConfig = Config.compatibleFromJson( json.decode(utf8.decode(configContent)), ); // Restore profile files to disk for (final profile in profileFiles) { final filePath = join(homeDirPath, profile.name); final file = File(filePath); await file.create(recursive: true); await file.writeAsBytes(profile.content); } // Extract profiles from backup List profiles = []; bool extractedFromDatabase = false; // 1. Try SQLite database first (FlClash backup) final dbFile = archive.files.firstWhereOrNull( (file) => file.name.endsWith('database.sqlite'), ); if (dbFile != null && dbFile.content.isNotEmpty) { try { // Save database temporarily final tempDbPath = join(await appPath.tempPath, 'temp_flclash.db'); final tempDb = File(tempDbPath); await tempDb.writeAsBytes(dbFile.content); // Extract profiles from database profiles = await FlClashDatabaseExtractor.extractProfiles(tempDbPath); extractedFromDatabase = true; // Clean up temp file if (await tempDb.exists()) { await tempDb.delete(); } commonPrint.log( 'Extracted ${profiles.length} profiles from FlClash database', ); } catch (e) { commonPrint.log( 'Failed to extract from database, fallback to file names: $e', ); profiles = []; extractedFromDatabase = false; } } // 2. Fallback if database extraction failed if (profiles.isEmpty) { // Get from config.json if (backupConfig.profiles.isNotEmpty) { profiles = backupConfig.profiles; } else { // Extract ID from profile file names (FlClash mode) for (final profileFile in profileFiles) { final fileName = profileFile.name.split('/').last; if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) { final id = fileName.replaceAll(RegExp(r'\.(yaml|yml)$'), ''); // Try to extract friendly label from YAML final label = await _extractLabelFromYaml(profileFile) ?? id; // Create basic Profile object profiles.add( Profile( id: id, label: label, autoUpdateDuration: defaultUpdateDuration, url: '', // Mark empty, user needs to add ), ); } } } } // Create limited recovery config (subscriptions only) Config limitedConfig = globalState.config.copyWith(profiles: profiles); // Android: also restore app list if (system.isAndroid) { // FlClash uses accessControlProps instead of accessControl final vpnProps = backupConfig.vpnProps; AccessControl? accessControl; // Try to get from vpnProps.accessControl try { accessControl = vpnProps.accessControl; } catch (_) { // Fallback: try accessControlProps from raw JSON try { final configJson = json.decode(utf8.decode(configFile.content)); final vpnPropsJson = configJson['vpnProps']; if (vpnPropsJson != null && vpnPropsJson is Map) { final accessControlPropsJson = vpnPropsJson['accessControlProps']; if (accessControlPropsJson != null) { accessControl = AccessControl.fromJson( accessControlPropsJson as Map, ); } } } catch (_) {} } if (accessControl != null) { limitedConfig = limitedConfig.copyWith.vpnProps( accessControl: accessControl, ); } } // Apply limited recovery _recoveryLimited(limitedConfig, recoveryOption); // Show recovery result message _showRecoveryResultMessage(profiles, extractedFromDatabase); } /// Extract label Future _extractLabelFromYaml(ArchiveFile profileFile) async { try { final yamlContent = utf8.decode(profileFile.content); // Try to extract from comments final lines = yamlContent.split('\n'); for (final line in lines) { if (line.trim().startsWith('#')) { final comment = line.trim().substring(1).trim(); if (comment.isNotEmpty && comment.length < 50 && !comment.startsWith('!')) { return comment; } } } // Try to extract from first proxy name final yamlMap = loadYaml(yamlContent); if (yamlMap is Map && yamlMap['proxies'] is List) { final proxies = yamlMap['proxies'] as List; if (proxies.isNotEmpty && proxies[0] is Map) { final firstProxy = proxies[0] as Map; final name = firstProxy['name']; if (name != null && name.toString().isNotEmpty) { return 'Sub - $name'; } } } } catch (e) { commonPrint.log('Failed to extract label from YAML: $e'); } return null; } /// Show results void _showRecoveryResultMessage( List profiles, bool extractedFromDatabase, ) { if (profiles.isEmpty) return; final hasEmptyUrl = profiles.any((p) => p.url.isEmpty); String message; if (extractedFromDatabase) { // Successfully extracted from database message = 'Restored ${profiles.length} subscriptions with URLs.'; } else if (hasEmptyUrl) { // Partial recovery, missing URLs message = 'Restored ${profiles.length} subscriptions.\n\n' 'Warning: URLs not included. Edit subscriptions to add URLs for auto-update.'; } else { // Complete recovery message = 'Restored ${profiles.length} subscriptions.'; } globalState.showMessage( title: appLocalizations.recoverySuccess, message: TextSpan(text: message), ); } void _restoreProfiles(List profiles) { final recoveryStrategy = _ref.read( appSettingProvider.select((state) => state.recoveryStrategy), ); if (recoveryStrategy == RecoveryStrategy.override) { _ref.read(profilesProvider.notifier).value = profiles; } else { for (final profile in profiles) { _ref.read(profilesProvider.notifier).setProfile(profile); } } } void _ensureCurrentProfile(List profiles) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile == null && profiles.isNotEmpty) { _ref.read(currentProfileIdProvider.notifier).value = profiles.first.id; } } /// Partial restore void _recoveryLimited(Config config, RecoveryOption recoveryOption) { final profiles = config.profiles; // Restore subscriptions _restoreProfiles(profiles); // Android: restore app list if (system.isAndroid) { _ref .read(vpnSettingProvider.notifier) .updateState( (state) => state.copyWith(accessControl: config.vpnProps.accessControl), ); } // Ensure current profile exists _ensureCurrentProfile(profiles); } /// Full restore void _recovery(Config config, RecoveryOption recoveryOption) { final profiles = config.profiles; // Restore subscriptions _restoreProfiles(profiles); final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles; if (!onlyProfiles) { // Restore settings // 1. Clash config if (system.isDesktop) { // Desktop: preserve current TUN state, avoid mobile backup override final currentTunEnable = _ref.read(patchClashConfigProvider).tun.enable; _ref.read(patchClashConfigProvider.notifier).value = config .patchClashConfig .copyWith .tun(enable: currentTunEnable); } else { // Mobile: restore directly _ref.read(patchClashConfigProvider.notifier).value = config.patchClashConfig; } // 2. App settings final currentAppSetting = _ref.read(appSettingProvider); final backupAppSetting = config.appSetting; // Merge dashboardWidgets: preserve platform-specific widgets final currentWidgets = currentAppSetting.dashboardWidgets; final backupWidgets = backupAppSetting.dashboardWidgets; final mergedWidgets = _mergeDashboardWidgets( currentWidgets, backupWidgets, ); _ref.read(appSettingProvider.notifier).value = backupAppSetting.copyWith( dashboardWidgets: mergedWidgets, ); // 3. Restore current profile ID _ref.read(currentProfileIdProvider.notifier).value = config.currentProfileId; // 4. Restore WebDAV settings _ref.read(appDAVSettingProvider.notifier).value = config.dav; // 5. Restore theme settings _ref.read(themeSettingProvider.notifier).value = config.themeProps; // 6. Restore window settings (desktop only) if (system.isDesktop) { _ref.read(windowSettingProvider.notifier).value = config.windowProps; } // 7. VPN settings if (system.isAndroid) { // Android: restore VPN settings _ref.read(vpnSettingProvider.notifier).value = config.vpnProps; } else if (system.isDesktop) { // Desktop: restore network settings, preserve TUN state final currentVpnProps = _ref.read(vpnSettingProvider); _ref.read(networkSettingProvider.notifier).value = config.networkProps; // Only restore non-platform-specific VPN settings _ref.read(vpnSettingProvider.notifier).value = config.vpnProps.copyWith( enable: currentVpnProps.enable, // Preserve current TUN state ); } // 8. Restore proxy style _ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle; // 9. Restore DNS override settings _ref.read(overrideDnsProvider.notifier).value = config.overrideDns; // 10. Restore hotkey settings (desktop only) if (system.isDesktop) { _ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions; } // 11. Restore script settings _ref.read(scriptStateProvider.notifier).value = config.scriptProps; } // Ensure current profile exists _ensureCurrentProfile(profiles); } /// Merge widgets List _mergeDashboardWidgets( List currentWidgets, List backupWidgets, ) { // Platform widgets final Set androidOnlyWidgets = { // Android-specific widgets (if any) }; final Set desktopOnlyWidgets = { DashboardWidget.tunButton, // TUN button (desktop-specific) DashboardWidget .systemProxyButton, // System proxy button (more common on desktop) }; // Determine platform-specific widgets final platformSpecificWidgets = system.isAndroid ? androidOnlyWidgets : desktopOnlyWidgets; // Build position map for platform-specific widgets final platformWidgetPositions = {}; for (var i = 0; i < currentWidgets.length; i++) { final widget = currentWidgets[i]; if (platformSpecificWidgets.contains(widget)) { platformWidgetPositions[widget] = i; } } // Get non-platform-specific widgets from backup final backupCommonWidgets = backupWidgets .where((widget) => !platformSpecificWidgets.contains(widget)) .toList(); // Merge strategy: insert platform-specific widgets at original positions final mergedWidgets = [...backupCommonWidgets]; // Insert platform-specific widgets by position (smallest first) final sortedEntries = platformWidgetPositions.entries.toList() ..sort((a, b) => a.value.compareTo(b.value)); for (final entry in sortedEntries) { final widget = entry.key; final originalPosition = entry.value; // Insert position cannot exceed list length final insertPosition = originalPosition.clamp(0, mergedWidgets.length); mergedWidgets.insert(insertPosition, widget); } // Use default widgets if merged is empty return mergedWidgets.isNotEmpty ? mergedWidgets : defaultDashboardWidgets; } /// Rollback Future _rollbackConfig() async { final lastConfig = globalState.getLastSuccessfulConfig(); if (lastConfig == null) { commonPrint.log('No backup config available for rollback'); return; } try { commonPrint.log('Rolling back to last successful config'); await clashCore.setupConfig(lastConfig); commonPrint.log('Config rollback successful'); } catch (e) { commonPrint.log('Config rollback failed: $e'); } } Future safeRun( FutureOr Function() futureFunction, { String? title, bool needLoading = false, bool silence = true, }) async { final realSilence = needLoading == true ? true : silence; try { if (needLoading) { _ref.read(loadingProvider.notifier).value = true; } final res = await futureFunction(); return res; } catch (e) { commonPrint.log('$e'); if (realSilence) { globalState.showNotifier(e.toString()); } else { globalState.showMessage( title: title ?? appLocalizations.tip, message: TextSpan(text: e.toString()), ); } return null; } finally { _ref.read(loadingProvider.notifier).value = false; } } } ================================================ FILE: lib/enum/enum.dart ================================================ // ignore_for_file: constant_identifier_names import 'dart:io'; import 'package:bett_box/common/system.dart'; import 'package:bett_box/views/dashboard/widgets/widgets.dart'; import 'package:bett_box/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; enum SupportPlatform { Windows, MacOS, Linux, Android; static SupportPlatform get currentPlatform { if (system.isWindows) { return SupportPlatform.Windows; } else if (system.isMacOS) { return SupportPlatform.MacOS; } else if (Platform.isLinux) { return SupportPlatform.Linux; } else if (system.isAndroid) { return SupportPlatform.Android; } throw 'invalid platform'; } } const desktopPlatforms = [ SupportPlatform.Linux, SupportPlatform.MacOS, SupportPlatform.Windows, ]; enum GroupType { Selector, URLTest, Fallback, LoadBalance; static GroupType parseProfileType(String type) { return switch (type) { 'url-test' => URLTest, 'select' => Selector, 'fallback' => Fallback, 'load-balance' => LoadBalance, String() => throw UnimplementedError(), }; } } enum GroupName { GLOBAL, Proxy, Auto, Fallback } extension GroupTypeExtension on GroupType { static List get valueList => GroupType.values.map((e) => e.toString().split('.').last).toList(); bool get isComputedSelected { return [GroupType.URLTest, GroupType.Fallback].contains(this); } static GroupType? getGroupType(String value) { final index = GroupTypeExtension.valueList.indexOf(value); if (index == -1) return null; return GroupType.values[index]; } String get value => GroupTypeExtension.valueList[index]; } enum UsedProxy { GLOBAL, DIRECT, REJECT } extension UsedProxyExtension on UsedProxy { static List get valueList => UsedProxy.values.map((e) => e.toString().split('.').last).toList(); String get value => UsedProxyExtension.valueList[index]; } enum Mode { rule, global, direct } enum IpClickBehavior { privacyProtection, manualRefresh, switchDomestic } enum ViewMode { mobile, laptop, desktop } enum LogLevel { debug, info, warning, error, silent } extension LogLevelExt on LogLevel { Color? get color { return switch (this) { LogLevel.silent => Colors.grey.shade700, LogLevel.debug => Colors.grey.shade400, LogLevel.info => null, LogLevel.warning => const Color.fromARGB(230, 255, 166, 0), LogLevel.error => Colors.redAccent, }; } } enum TransportProtocol { udp, tcp } enum TrafficUnit { B, KB, MB, GB, TB } enum NavigationItemMode { mobile, desktop, more } enum Network { tcp, udp } enum ProxiesSortType { none, delay, name } enum TunStack { gvisor, system, mixed } enum AccessControlMode { acceptSelected, rejectSelected } enum AccessSortType { none, name, time } enum ProfileType { file, url } enum ResultType { @JsonValue(0) success, @JsonValue(-1) error, } enum AppMessageType { log, delay, request, loaded } enum InvokeMessageType { protect, process } enum FindProcessMode { always, off } enum RecoveryOption { all, onlyProfiles } enum ChipType { action, delete } enum CommonCardType { plain, filled } // // extension CommonCardTypeExt on CommonCardType { // CommonCardType get variant => CommonCardType.plain; // } enum ProxiesType { tab, list } enum ProxiesLayout { loose, standard, tight } enum ProxyCardType { expand, shrink, min } enum DnsMode { normal, @JsonValue('fake-ip') fakeIp, @JsonValue('redir-host') redirHost, hosts, } enum CacheAlgorithm { arc, lru } enum FilterMode { blacklist, whitelist, rule } enum ExternalControllerStatus { @JsonValue('') close(''), @JsonValue('127.0.0.1:9090') open('127.0.0.1:9090'); final String value; const ExternalControllerStatus(this.value); } enum KeyboardModifier { alt([PhysicalKeyboardKey.altLeft, PhysicalKeyboardKey.altRight]), capsLock([PhysicalKeyboardKey.capsLock]), control([PhysicalKeyboardKey.controlLeft, PhysicalKeyboardKey.controlRight]), fn([PhysicalKeyboardKey.fn]), meta([PhysicalKeyboardKey.metaLeft, PhysicalKeyboardKey.metaRight]), shift([PhysicalKeyboardKey.shiftLeft, PhysicalKeyboardKey.shiftRight]); final List physicalKeys; const KeyboardModifier(this.physicalKeys); } extension KeyboardModifierExt on KeyboardModifier { HotKeyModifier toHotKeyModifier() { return switch (this) { KeyboardModifier.alt => HotKeyModifier.alt, KeyboardModifier.capsLock => HotKeyModifier.capsLock, KeyboardModifier.control => HotKeyModifier.control, KeyboardModifier.fn => HotKeyModifier.fn, KeyboardModifier.meta => HotKeyModifier.meta, KeyboardModifier.shift => HotKeyModifier.shift, }; } } enum HotAction { start, view, mode, proxy, tun } enum ProxiesIconStyle { standard, none, icon } enum FontFamily { twEmoji('Twemoji'), jetBrainsMono('JetBrainsMono'), icon('Icons'); final String value; const FontFamily(this.value); } enum RouteMode { bypassPrivate, config } enum ActionMethod { message, initClash, getIsInit, forceGc, shutdown, validateConfig, updateConfig, getConfig, getProxies, changeProxy, getTraffic, getTotalTraffic, resetTraffic, asyncTestDelay, getConnections, closeConnections, resetConnections, closeConnection, getExternalProviders, getExternalProvider, updateGeoData, updateExternalProvider, sideLoadExternalProvider, startLog, stopLog, startListener, stopListener, getCountryCode, getMemory, crash, setupConfig, flushFakeIP, flushDnsCache, ///Android, setState, startTun, stopTun, getRunTime, updateDns, getAndroidVpnOptions, getCurrentProfileName, } enum AuthorizeCode { none, success, error } enum WindowsHelperServiceStatus { none, presence, running } enum FunctionTag { updateClashConfig, setupClashConfig, updateStatus, updateGroups, addCheckIpNum, applyProfile, savePreferences, changeProxy, checkIp, handleWill, updateDelay, vpnTip, autoLaunch, renderPause, updatePageIndex, pageChange, proxiesTabChange, logs, requests, autoScrollToEnd, } enum DashboardWidget { networkSpeed(GridItem(crossAxisCellCount: 8, child: NetworkSpeed())), networkSpeedSmall( GridItem(crossAxisCellCount: 4, child: NetworkSpeedSmall()), ), outboundModeV2(GridItem(crossAxisCellCount: 8, child: OutboundModeV2())), outboundMode(GridItem(crossAxisCellCount: 4, child: OutboundMode())), trafficUsage(GridItem(crossAxisCellCount: 4, child: TrafficUsage())), networkDetection(GridItem(crossAxisCellCount: 4, child: NetworkDetection())), tunButton( GridItem(crossAxisCellCount: 4, child: TUNButton()), platforms: desktopPlatforms, ), vpnButton( GridItem(crossAxisCellCount: 4, child: VpnButton()), platforms: [SupportPlatform.Android], ), systemProxyButton( GridItem(crossAxisCellCount: 4, child: SystemProxyButton()), platforms: desktopPlatforms, ), intranetIp(GridItem(crossAxisCellCount: 4, child: IntranetIP())), memoryInfo(GridItem(crossAxisCellCount: 4, child: MemoryInfo())), connectionsCount(GridItem(crossAxisCellCount: 4, child: ConnectionsCount())), ipv6Switch(GridItem(crossAxisCellCount: 4, child: Ipv6Switch())), wakelockSwitch( GridItem(crossAxisCellCount: 4, child: WakelockSwitch()), platforms: desktopPlatforms, ), dnsOverride(GridItem(crossAxisCellCount: 4, child: DnsOverride())), snifferOverride(GridItem(crossAxisCellCount: 4, child: SnifferOverride())), ntpOverride(GridItem(crossAxisCellCount: 4, child: NtpOverride())), providersInfo(GridItem(crossAxisCellCount: 4, child: ProvidersInfo())), fcmStatus(GridItem(crossAxisCellCount: 4, child: FcmStatus())), onlinePanel(GridItem(crossAxisCellCount: 4, child: OnlinePanel())), startButton( GridItem(crossAxisCellCount: 4, isDeletable: false, child: StartButton()), ); final GridItem widget; final List platforms; const DashboardWidget(this.widget, {this.platforms = SupportPlatform.values}); static DashboardWidget getDashboardWidget(GridItem gridItem) { final dashboardWidgets = DashboardWidget.values; final index = dashboardWidgets.indexWhere( (item) => item.widget == gridItem, ); return dashboardWidgets[index]; } } enum GeodataLoader { standard, memconservative } enum PageLabel { dashboard, proxies, profiles, tools, logs, requests, resources, script, connections, } enum RuleAction { DOMAIN('DOMAIN'), DOMAIN_SUFFIX('DOMAIN-SUFFIX'), DOMAIN_KEYWORD('DOMAIN-KEYWORD'), DOMAIN_REGEX('DOMAIN-REGEX'), GEOSITE('GEOSITE'), IP_CIDR('IP-CIDR'), IP_CIDR6('IP-CIDR6'), IP_SUFFIX('IP-SUFFIX'), IP_ASN('IP-ASN'), GEOIP('GEOIP'), SRC_GEOIP('SRC-GEOIP'), SRC_IP_ASN('SRC-IP-ASN'), SRC_IP_CIDR('SRC-IP-CIDR'), SRC_IP_SUFFIX('SRC-IP-SUFFIX'), DST_PORT('DST-PORT'), SRC_PORT('SRC-PORT'), IN_PORT('IN-PORT'), IN_TYPE('IN-TYPE'), IN_USER('IN-USER'), IN_NAME('IN-NAME'), PROCESS_PATH('PROCESS-PATH'), PROCESS_PATH_REGEX('PROCESS-PATH-REGEX'), PROCESS_NAME('PROCESS-NAME'), PROCESS_NAME_REGEX('PROCESS-NAME-REGEX'), UID('UID'), NETWORK('NETWORK'), DSCP('DSCP'), RULE_SET('RULE-SET'), AND('AND'), OR('OR'), NOT('NOT'), SUB_RULE('SUB-RULE'), MATCH('MATCH'); final String value; const RuleAction(this.value); } extension RuleActionExt on RuleAction { bool get hasParams => [ RuleAction.GEOIP, RuleAction.IP_ASN, RuleAction.SRC_IP_ASN, RuleAction.IP_CIDR, RuleAction.IP_CIDR6, RuleAction.IP_SUFFIX, RuleAction.RULE_SET, ].contains(this); } enum OverrideRuleType { override, added } enum RuleTarget { DIRECT, REJECT } enum RecoveryStrategy { compatible, override } enum CacheTag { logs, rules, requests, proxiesList } enum Language { yaml, javaScript } enum ImportOption { code, url, file } enum ScrollPositionCacheKeys { tools, profiles, proxiesList, proxiesTabList } enum DelayAnimationType { none, rotatingCircle, pulse, spinningLines, threeInOut, threeBounce, circle, fadingCircle, fadingFour, wave, doubleBounce, } ================================================ FILE: lib/l10n/intl/messages_all.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that looks up messages for specific locales by // delegating to the appropriate library. // Ignore issues from commonly used lints in this file. // ignore_for_file:implementation_imports, file_names, unnecessary_new // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering // ignore_for_file:argument_type_not_assignable, invalid_assignment // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases // ignore_for_file:comment_references import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/src/intl_helpers.dart'; import 'messages_en.dart' as messages_en; import 'messages_ru.dart' as messages_ru; import 'messages_zh_CN.dart' as messages_zh_cn; import 'messages_zh_TC.dart' as messages_zh_tc; typedef Future LibraryLoader(); Map _deferredLibraries = { 'en': () => new SynchronousFuture(null), 'ru': () => new SynchronousFuture(null), 'zh_CN': () => new SynchronousFuture(null), 'zh_TC': () => new SynchronousFuture(null), }; MessageLookupByLibrary? _findExact(String localeName) { switch (localeName) { case 'en': return messages_en.messages; case 'ru': return messages_ru.messages; case 'zh_CN': return messages_zh_cn.messages; case 'zh_TC': return messages_zh_tc.messages; default: return null; } } /// User programs should call this before using [localeName] for messages. Future initializeMessages(String localeName) { var availableLocale = Intl.verifiedLocale( localeName, (locale) => _deferredLibraries[locale] != null, onFailure: (_) => null, ); if (availableLocale == null) { return new SynchronousFuture(false); } var lib = _deferredLibraries[availableLocale]; lib == null ? new SynchronousFuture(false) : lib(); initializeInternalMessageLookup(() => new CompositeMessageLookup()); messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); return new SynchronousFuture(true); } bool _messagesExistFor(String locale) { try { return _findExact(locale) != null; } catch (e) { return false; } } MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { var actualLocale = Intl.verifiedLocale( locale, _messagesExistFor, onFailure: (_) => null, ); if (actualLocale == null) return null; return _findExact(actualLocale); } ================================================ FILE: lib/l10n/intl/messages_en.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a en locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; static String m0(label) => "Delete selected ${label}?"; static String m1(label) => "Delete current ${label}?"; static String m2(label) => "${label} Details"; static String m3(label) => "${label} cannot be empty"; static String m4(label) => "${label} already exists"; static String m5(label) => "No ${label}"; static String m6(label) => "${label} must be a number"; static String m7(label) => "${label} must be between 1024 and 49151"; static String m8(count) => "${count} items selected"; static String m9(label) => "${label} must be a URL"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("About"), "accessControl": MessageLookupByLibrary.simpleMessage("Access Control"), "accessControlAllowDesc": MessageLookupByLibrary.simpleMessage( "Only route selected apps through VPN", ), "accessControlDesc": MessageLookupByLibrary.simpleMessage( "Configure per-app proxy access", ), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage( "Exclude selected apps from VPN", ), "account": MessageLookupByLibrary.simpleMessage("Account"), "action": MessageLookupByLibrary.simpleMessage("Action"), "action_mode": MessageLookupByLibrary.simpleMessage("Switch Mode"), "action_proxy": MessageLookupByLibrary.simpleMessage("System Proxy"), "action_start": MessageLookupByLibrary.simpleMessage("Start/Stop"), "action_tun": MessageLookupByLibrary.simpleMessage("TUN"), "action_view": MessageLookupByLibrary.simpleMessage("Show/Hide"), "add": MessageLookupByLibrary.simpleMessage("Add"), "addProfile": MessageLookupByLibrary.simpleMessage("Add Profile"), "addRule": MessageLookupByLibrary.simpleMessage("Add Rule"), "addTunnel": MessageLookupByLibrary.simpleMessage("Add Forwarding"), "addedOriginRules": MessageLookupByLibrary.simpleMessage( "Append to Original Rules", ), "address": MessageLookupByLibrary.simpleMessage("Address"), "addressHelp": MessageLookupByLibrary.simpleMessage( "WebDAV server address", ), "addressTip": MessageLookupByLibrary.simpleMessage( "Please enter a valid WebDAV address", ), "adminAutoLaunch": MessageLookupByLibrary.simpleMessage( "Admin Auto-Launch", ), "adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage( "Auto-start with admin privileges", ), "advancedSettings": MessageLookupByLibrary.simpleMessage( "Advanced Settings", ), "ago": MessageLookupByLibrary.simpleMessage(" Ago"), "agree": MessageLookupByLibrary.simpleMessage("Agree"), "allApps": MessageLookupByLibrary.simpleMessage("All Apps"), "allowBypass": MessageLookupByLibrary.simpleMessage("Allow Bypassing VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage( "Allow specific apps to bypass VPN", ), "allowLan": MessageLookupByLibrary.simpleMessage("Allow LAN"), "allowLanDesc": MessageLookupByLibrary.simpleMessage( "Allow LAN access to proxy", ), "alreadyInWhitelist": MessageLookupByLibrary.simpleMessage( "Already in whitelist", ), "app": MessageLookupByLibrary.simpleMessage("App"), "appAccessControl": MessageLookupByLibrary.simpleMessage( "App Access Control", ), "appDesc": MessageLookupByLibrary.simpleMessage("App-related settings"), "application": MessageLookupByLibrary.simpleMessage("Application"), "applicationDesc": MessageLookupByLibrary.simpleMessage( "Modify application settings", ), "auto": MessageLookupByLibrary.simpleMessage("Auto"), "autoCheckUpdate": MessageLookupByLibrary.simpleMessage( "Auto Check Updates", ), "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage( "Check updates on app launch", ), "autoCloseConnections": MessageLookupByLibrary.simpleMessage( "Auto-Close Connections", ), "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage( "Close connections when switching nodes", ), "autoLaunch": MessageLookupByLibrary.simpleMessage("Auto Launch"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage( "Launch on system startup", ), "autoRun": MessageLookupByLibrary.simpleMessage("Auto Run"), "autoRunDesc": MessageLookupByLibrary.simpleMessage( "Connect on app launch", ), "autoSetSystemDns": MessageLookupByLibrary.simpleMessage( "Auto Set System DNS", ), "autoUpdate": MessageLookupByLibrary.simpleMessage("Auto Update"), "autoUpdateInterval": MessageLookupByLibrary.simpleMessage( "Auto update interval (min)", ), "backup": MessageLookupByLibrary.simpleMessage("Backup"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage( "Backup & Restore", ), "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage( "Sync data via WebDAV or local files", ), "backupSuccess": MessageLookupByLibrary.simpleMessage("Backup Successful"), "basicConfig": MessageLookupByLibrary.simpleMessage("Core Configuration"), "basicConfigDesc": MessageLookupByLibrary.simpleMessage( "Global core settings", ), "batteryOptimization": MessageLookupByLibrary.simpleMessage( "Battery Optimization", ), "batteryOptimizationDesc": MessageLookupByLibrary.simpleMessage( "Request battery optimization whitelist", ), "bind": MessageLookupByLibrary.simpleMessage("Bind"), "blacklist": MessageLookupByLibrary.simpleMessage("Blacklist"), "blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist Mode"), "bypassDomain": MessageLookupByLibrary.simpleMessage("Bypass Domain"), "bypassDomainDesc": MessageLookupByLibrary.simpleMessage( "Active only when System Proxy is on", ), "bypassPrivateRoute": MessageLookupByLibrary.simpleMessage( "Bypass Private Network", ), "bypassPrivateRouteDesc": MessageLookupByLibrary.simpleMessage( "Automatically bypass private network IP addresses", ), "cacheAlgorithm": MessageLookupByLibrary.simpleMessage("Cache Algorithm"), "cacheCorrupt": MessageLookupByLibrary.simpleMessage( "Cache corrupted. Clear it?", ), "cameraPermissionDenied": MessageLookupByLibrary.simpleMessage( "Camera Permission Denied", ), "cameraPermissionDesc": MessageLookupByLibrary.simpleMessage( "Camera permission is required to scan QR codes. Please grant it in settings.", ), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage( "Show System Apps", ), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("Deselect All"), "checkError": MessageLookupByLibrary.simpleMessage("Check Failed"), "checkOrAddProfile": MessageLookupByLibrary.simpleMessage( "Please add a profile first", ), "checkUpdate": MessageLookupByLibrary.simpleMessage("Check for Updates"), "checkUpdateError": MessageLookupByLibrary.simpleMessage( "Already on the latest version", ), "checking": MessageLookupByLibrary.simpleMessage("Checking..."), "circle": MessageLookupByLibrary.simpleMessage("Circle"), "clearCacheDesc": MessageLookupByLibrary.simpleMessage( "Clear FakeIP and DNS cache?", ), "clearCacheTitle": MessageLookupByLibrary.simpleMessage("Clear Cache"), "clearData": MessageLookupByLibrary.simpleMessage("Clear Data"), "clipboard": MessageLookupByLibrary.simpleMessage("Clipboard"), "clipboardDesc": MessageLookupByLibrary.simpleMessage( "Get profile link from clipboard", ), "clipboardExport": MessageLookupByLibrary.simpleMessage( "Export to Clipboard", ), "clipboardImport": MessageLookupByLibrary.simpleMessage( "Import from Clipboard", ), "color": MessageLookupByLibrary.simpleMessage("Color"), "colorSchemes": MessageLookupByLibrary.simpleMessage("Color Schemes"), "columns": MessageLookupByLibrary.simpleMessage("Columns"), "compatible": MessageLookupByLibrary.simpleMessage("Compatible Mode"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( "Reduces some features for full Clash compatibility", ), "concurrencyLimit": MessageLookupByLibrary.simpleMessage( "Concurrency Limit", ), "concurrencyLimitDesc": MessageLookupByLibrary.simpleMessage( "Maximum concurrent delay tests", ), "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), "connection": MessageLookupByLibrary.simpleMessage("Active Connections"), "connections": MessageLookupByLibrary.simpleMessage("Connections"), "connectionsDesc": MessageLookupByLibrary.simpleMessage( "View active connections", ), "connectivity": MessageLookupByLibrary.simpleMessage("Connectivity:"), "contactMe": MessageLookupByLibrary.simpleMessage("Contact Me"), "content": MessageLookupByLibrary.simpleMessage("Content"), "contentScheme": MessageLookupByLibrary.simpleMessage("Content"), "controlSecret": MessageLookupByLibrary.simpleMessage("Control Secret"), "controlSecretDesc": MessageLookupByLibrary.simpleMessage( "RESTful API access password", ), "copy": MessageLookupByLibrary.simpleMessage("Copy"), "copyEnvVar": MessageLookupByLibrary.simpleMessage( "Copy Environment Variable", ), "copyLink": MessageLookupByLibrary.simpleMessage("Copy Link"), "copySuccess": MessageLookupByLibrary.simpleMessage("Copy Successful"), "core": MessageLookupByLibrary.simpleMessage("Core"), "coreConnected": MessageLookupByLibrary.simpleMessage("Connected"), "coreInfo": MessageLookupByLibrary.simpleMessage("Core Info"), "coreSuspended": MessageLookupByLibrary.simpleMessage("Suspended"), "country": MessageLookupByLibrary.simpleMessage("Country"), "crashTest": MessageLookupByLibrary.simpleMessage("Crash Test"), "create": MessageLookupByLibrary.simpleMessage("Create"), "creationTime": MessageLookupByLibrary.simpleMessage("Creation Time"), "custom": MessageLookupByLibrary.simpleMessage("Custom"), "customUrl": MessageLookupByLibrary.simpleMessage("Custom URL"), "cut": MessageLookupByLibrary.simpleMessage("Cut"), "dark": MessageLookupByLibrary.simpleMessage("Dark"), "dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"), "days": MessageLookupByLibrary.simpleMessage("Days"), "defaultNameserver": MessageLookupByLibrary.simpleMessage( "Default Nameserver", ), "defaultNameserverDesc": MessageLookupByLibrary.simpleMessage( "Used to resolve DNS servers", ), "defaultSort": MessageLookupByLibrary.simpleMessage("Default Sort"), "defaultText": MessageLookupByLibrary.simpleMessage("Default"), "delay": MessageLookupByLibrary.simpleMessage("Delay"), "delayAnimation": MessageLookupByLibrary.simpleMessage("Delay Animation"), "delayAnimationDesc": MessageLookupByLibrary.simpleMessage( "Customize animation during delay testing", ), "delaySort": MessageLookupByLibrary.simpleMessage("Sort by Delay"), "delete": MessageLookupByLibrary.simpleMessage("Delete"), "deleteMultipTip": m0, "deleteTip": m1, "deleteTunnel": MessageLookupByLibrary.simpleMessage("Delete Forwarding"), "desc": MessageLookupByLibrary.simpleMessage( "Bettbox is based on the powerful and flexible Mihomo (Clash.Meta) proxy kernel, dedicated to a superior user experience. Forked from FlClash: Better Experience, Out of the box", ), "destination": MessageLookupByLibrary.simpleMessage("Destination"), "destinationGeoIP": MessageLookupByLibrary.simpleMessage( "Destination GeoIP", ), "destinationIPASN": MessageLookupByLibrary.simpleMessage( "Destination IP ASN", ), "details": m2, "detectionTip": MessageLookupByLibrary.simpleMessage( "Third-party API result (for reference only)", ), "developerMode": MessageLookupByLibrary.simpleMessage("Developer Mode"), "developerModeEnableTip": MessageLookupByLibrary.simpleMessage( "Developer mode is enabled.", ), "dialerIp4pConvert": MessageLookupByLibrary.simpleMessage( "Enable Dialer IP4P Conversion", ), "dialerIp4pConvertDesc": MessageLookupByLibrary.simpleMessage( "Enable dialer IP4P address conversion feature", ), "direct": MessageLookupByLibrary.simpleMessage("Direct"), "directNameserver": MessageLookupByLibrary.simpleMessage( "Direct Nameserver", ), "directNameserverDesc": MessageLookupByLibrary.simpleMessage( "Used to resolve direct domains", ), "directNameserverFollowPolicy": MessageLookupByLibrary.simpleMessage( "Direct DNS Follows Policy", ), "disableQuic": MessageLookupByLibrary.simpleMessage("Disable QUIC"), "disableQuicDesc": MessageLookupByLibrary.simpleMessage( "Disable QUIC to resolve specific network issues", ), "disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage( "This free open-source software is for non-commercial learning and personal use only. Proxy services are independent of this software. By agreeing, you acknowledge this; otherwise, please exit.", ), "discoverNewVersion": MessageLookupByLibrary.simpleMessage( "New Version Available", ), "discovery": MessageLookupByLibrary.simpleMessage("New Version Found"), "dnsDesc": MessageLookupByLibrary.simpleMessage("DNS-related settings"), "dnsHijack": MessageLookupByLibrary.simpleMessage("DNS Hijack"), "dnsHijackDesc": MessageLookupByLibrary.simpleMessage( "Redirect DNS queries to internal DNS module", ), "dnsMode": MessageLookupByLibrary.simpleMessage("DNS Mode"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage( "Do you want to pass", ), "domain": MessageLookupByLibrary.simpleMessage("Domain"), "doubleBounce": MessageLookupByLibrary.simpleMessage("Double Bounce"), "download": MessageLookupByLibrary.simpleMessage("Download"), "dozeSuspend": MessageLookupByLibrary.simpleMessage("Doze Support"), "dozeSuspendDesc": MessageLookupByLibrary.simpleMessage( "Sync with system Doze mode", ), "edit": MessageLookupByLibrary.simpleMessage("Edit"), "editTunnel": MessageLookupByLibrary.simpleMessage("Edit Forwarding"), "emptyTip": m3, "en": MessageLookupByLibrary.simpleMessage("English"), "enableCrashReport": MessageLookupByLibrary.simpleMessage( "Crash Analytics", ), "enableCrashReportDesc": MessageLookupByLibrary.simpleMessage( "Upload crash logs when needed", ), "enableOverride": MessageLookupByLibrary.simpleMessage("Enable Override"), "endpointIndependentNat": MessageLookupByLibrary.simpleMessage( "NAT Enhancement", ), "endpointIndependentNatDesc": MessageLookupByLibrary.simpleMessage( "Enable endpoint-independent NAT", ), "entries": MessageLookupByLibrary.simpleMessage(" entries"), "exclude": MessageLookupByLibrary.simpleMessage("Hide from Recents"), "excludeChina": MessageLookupByLibrary.simpleMessage("Exclude China"), "excludeChinaDesc": MessageLookupByLibrary.simpleMessage( "Allow China QUIC traffic instead of blocking all", ), "excludeDesc": MessageLookupByLibrary.simpleMessage( "Hide app from recent tasks list", ), "existsTip": m4, "exit": MessageLookupByLibrary.simpleMessage("Exit"), "expand": MessageLookupByLibrary.simpleMessage("Standard"), "experimental": MessageLookupByLibrary.simpleMessage("Experimental"), "experimentalDesc": MessageLookupByLibrary.simpleMessage( "Use with caution", ), "expirationTime": MessageLookupByLibrary.simpleMessage("Expiration Time"), "exportFile": MessageLookupByLibrary.simpleMessage("Export File"), "exportLogs": MessageLookupByLibrary.simpleMessage("Export Logs"), "exportSuccess": MessageLookupByLibrary.simpleMessage("Export Successful"), "expressiveScheme": MessageLookupByLibrary.simpleMessage("Expressive"), "externalController": MessageLookupByLibrary.simpleMessage( "External Controller", ), "externalControllerDesc": MessageLookupByLibrary.simpleMessage( "Control core via online port", ), "externalLink": MessageLookupByLibrary.simpleMessage("External Link"), "externalResources": MessageLookupByLibrary.simpleMessage( "External Resources", ), "fadingCircle": MessageLookupByLibrary.simpleMessage("Fading Circle"), "fadingFour": MessageLookupByLibrary.simpleMessage("Fading Four"), "fakeIpFilterMode": MessageLookupByLibrary.simpleMessage( "FakeIP Filter Mode", ), "fakeIpFilterModeDesc": MessageLookupByLibrary.simpleMessage( "Specify FakeIP filter mode", ), "fakeipFilter": MessageLookupByLibrary.simpleMessage("FakeIP Filter"), "fakeipRange": MessageLookupByLibrary.simpleMessage("FakeIP Range"), "fakeipRangeV6": MessageLookupByLibrary.simpleMessage("FakeIPv6 Range"), "fakeipTtl": MessageLookupByLibrary.simpleMessage("FakeIP TTL"), "fallback": MessageLookupByLibrary.simpleMessage("Fallback"), "fallbackDesc": MessageLookupByLibrary.simpleMessage( "Usually offshore DNS", ), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback Filter"), "fcmOptimization": MessageLookupByLibrary.simpleMessage("FCM Optimization"), "fcmOptimizationDesc": MessageLookupByLibrary.simpleMessage( "Enhance FCM connection stability", ), "fcmTip": MessageLookupByLibrary.simpleMessage( "FCM support depends on your device; results are for reference. Disable \'Allow Bypass VPN\' in network settings for accurate results.", ), "fidelityScheme": MessageLookupByLibrary.simpleMessage("Fidelity"), "file": MessageLookupByLibrary.simpleMessage("File"), "fileDesc": MessageLookupByLibrary.simpleMessage("Upload profile file"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage( "File modified. Save changes?", ), "filterSystemApp": MessageLookupByLibrary.simpleMessage( "Filter System Apps", ), "findProcessMode": MessageLookupByLibrary.simpleMessage("Find Process"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage( "Enable process matching", ), "fontFamily": MessageLookupByLibrary.simpleMessage("Font"), "forceDnsMapping": MessageLookupByLibrary.simpleMessage( "Force DNS Mapping", ), "forceDnsMappingDesc": MessageLookupByLibrary.simpleMessage( "Force mapping DNS results to connections", ), "forceDomain": MessageLookupByLibrary.simpleMessage("Force Sniff Domain"), "forceGCDesc": MessageLookupByLibrary.simpleMessage( "Force kernel garbage collection? Experimental, use with caution.", ), "forceGCTitle": MessageLookupByLibrary.simpleMessage( "Force Garbage Collection", ), "formatError": MessageLookupByLibrary.simpleMessage( "Please check the format", ), "fourColumns": MessageLookupByLibrary.simpleMessage("4 Columns"), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("Fruit Salad"), "general": MessageLookupByLibrary.simpleMessage("General"), "generalDesc": MessageLookupByLibrary.simpleMessage( "Modify general settings", ), "generateSecret": MessageLookupByLibrary.simpleMessage("Generate"), "geoData": MessageLookupByLibrary.simpleMessage("GeoData"), "geodataLoader": MessageLookupByLibrary.simpleMessage("GEO Low Memory"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage( "Use GEO low memory loader", ), "geoipCode": MessageLookupByLibrary.simpleMessage("GeoIP Code"), "getOriginRules": MessageLookupByLibrary.simpleMessage("Original Rules"), "global": MessageLookupByLibrary.simpleMessage("Global"), "go": MessageLookupByLibrary.simpleMessage("Go"), "goDownload": MessageLookupByLibrary.simpleMessage("Download Now"), "harmonyFont": MessageLookupByLibrary.simpleMessage("HarmonyOS Font"), "harmonyFontDesc": MessageLookupByLibrary.simpleMessage( "Use optimized HarmonyOS Sans font", ), "hasCacheChange": MessageLookupByLibrary.simpleMessage( "Cache modifications?", ), "healthCheckTimeout": MessageLookupByLibrary.simpleMessage("Timeout"), "healthCheckTimeoutDesc": MessageLookupByLibrary.simpleMessage( "Node health check timeout", ), "highRefreshRate": MessageLookupByLibrary.simpleMessage( "High Refresh Rate", ), "highRefreshRateDesc": MessageLookupByLibrary.simpleMessage( "Enable highest refresh rate support", ), "host": MessageLookupByLibrary.simpleMessage("Host"), "hostsDesc": MessageLookupByLibrary.simpleMessage( "Append hosts to current config", ), "hotkeyConflict": MessageLookupByLibrary.simpleMessage("Hotkey Conflict"), "hotkeyManagement": MessageLookupByLibrary.simpleMessage( "Hotkey Management", ), "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage( "Control app via keyboard", ), "hours": MessageLookupByLibrary.simpleMessage("Hours"), "httpPortSniffer": MessageLookupByLibrary.simpleMessage( "HTTP Port Sniffing", ), "icmpForwarding": MessageLookupByLibrary.simpleMessage("ICMP Forwarding"), "icmpForwardingDesc": MessageLookupByLibrary.simpleMessage( "Enable ICMP Ping", ), "icon": MessageLookupByLibrary.simpleMessage("Icon"), "iconConfiguration": MessageLookupByLibrary.simpleMessage( "Icon Configuration", ), "iconStyle": MessageLookupByLibrary.simpleMessage("Icon Style"), "import": MessageLookupByLibrary.simpleMessage("Import"), "importFailed": MessageLookupByLibrary.simpleMessage("Import failed"), "importFile": MessageLookupByLibrary.simpleMessage("Import from File"), "importFromCode": MessageLookupByLibrary.simpleMessage("Import from Code"), "importFromURL": MessageLookupByLibrary.simpleMessage("Import from URL"), "importUrl": MessageLookupByLibrary.simpleMessage("Import from URL"), "infiniteTime": MessageLookupByLibrary.simpleMessage("Never Expires"), "init": MessageLookupByLibrary.simpleMessage("Init"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage( "Enter a valid hotkey", ), "intelligentSelected": MessageLookupByLibrary.simpleMessage("Smart Select"), "internet": MessageLookupByLibrary.simpleMessage("Internet"), "interval": MessageLookupByLibrary.simpleMessage("Interval"), "intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"), "invalidIpFormat": MessageLookupByLibrary.simpleMessage( "Invalid IP or CIDR format", ), "ipClickBehavior": MessageLookupByLibrary.simpleMessage("Toggle Display"), "ipPrivacyProtection": MessageLookupByLibrary.simpleMessage( "Hide IP Display", ), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/CIDR"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( "Enable IPv6 traffic routing", ), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage( "Allow IPv6 inbound", ), "ja": MessageLookupByLibrary.simpleMessage("Japanese"), "just": MessageLookupByLibrary.simpleMessage("Just now"), "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage( "TCP keep-alive interval", ), "key": MessageLookupByLibrary.simpleMessage("Key"), "language": MessageLookupByLibrary.simpleMessage("Language"), "layout": MessageLookupByLibrary.simpleMessage("Layout"), "light": MessageLookupByLibrary.simpleMessage("Light"), "lightIcon": MessageLookupByLibrary.simpleMessage("Light Icon"), "lightIconDesc": MessageLookupByLibrary.simpleMessage( "Manually switch light desktop app icon", ), "list": MessageLookupByLibrary.simpleMessage("List"), "listen": MessageLookupByLibrary.simpleMessage("Listen"), "local": MessageLookupByLibrary.simpleMessage("Local"), "localBackupDesc": MessageLookupByLibrary.simpleMessage( "Backup data locally", ), "localRecoveryDesc": MessageLookupByLibrary.simpleMessage( "Restore data from file", ), "log": MessageLookupByLibrary.simpleMessage("Log"), "logLevel": MessageLookupByLibrary.simpleMessage("Log Level"), "logcat": MessageLookupByLibrary.simpleMessage("Log Capture"), "logcatDesc": MessageLookupByLibrary.simpleMessage( "Show log capture entry", ), "logs": MessageLookupByLibrary.simpleMessage("Logs"), "logsDesc": MessageLookupByLibrary.simpleMessage("View captured logs"), "logsTest": MessageLookupByLibrary.simpleMessage("Logs Test"), "loopback": MessageLookupByLibrary.simpleMessage("Loopback Unlock"), "loopbackDesc": MessageLookupByLibrary.simpleMessage( "UWP loopback unlocking tool", ), "loose": MessageLookupByLibrary.simpleMessage("Loose"), "manualRefreshIp": MessageLookupByLibrary.simpleMessage("Refresh IP"), "memoryInfo": MessageLookupByLibrary.simpleMessage("Memory Info"), "messageTest": MessageLookupByLibrary.simpleMessage("Message Test"), "messageTestTip": MessageLookupByLibrary.simpleMessage( "This is a message.", ), "min": MessageLookupByLibrary.simpleMessage("Min"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("Minimize on Exit"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "Override default exit behavior", ), "minutes": MessageLookupByLibrary.simpleMessage("Minutes"), "mixedPort": MessageLookupByLibrary.simpleMessage("Mixed Port"), "mode": MessageLookupByLibrary.simpleMessage("Mode"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("Monochrome"), "months": MessageLookupByLibrary.simpleMessage("Months"), "more": MessageLookupByLibrary.simpleMessage("More"), "name": MessageLookupByLibrary.simpleMessage("Name"), "nameSort": MessageLookupByLibrary.simpleMessage("Sort by Name"), "nameserver": MessageLookupByLibrary.simpleMessage("Nameserver"), "nameserverDesc": MessageLookupByLibrary.simpleMessage( "Used to resolve domains", ), "nameserverPolicy": MessageLookupByLibrary.simpleMessage( "Nameserver Policy", ), "nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage( "Specify domain-specific nameservers", ), "navBarHapticFeedback": MessageLookupByLibrary.simpleMessage( "Haptic Feedback", ), "navBarHapticFeedbackDesc": MessageLookupByLibrary.simpleMessage( "Vibrate on navigation tab switch", ), "network": MessageLookupByLibrary.simpleMessage("Network"), "networkDesc": MessageLookupByLibrary.simpleMessage( "Modify network-related settings", ), "networkDetection": MessageLookupByLibrary.simpleMessage( "Network Detection", ), "networkFix": MessageLookupByLibrary.simpleMessage("Network Fix"), "networkFixDesc": MessageLookupByLibrary.simpleMessage( "Fix Windows network globe icon issue", ), "networkMatch": MessageLookupByLibrary.simpleMessage("Network Match"), "networkMatchHint": MessageLookupByLibrary.simpleMessage( "Max 2 IPs/CIDRs, comma-separated", ), "networkSpeed": MessageLookupByLibrary.simpleMessage("Network Speed"), "networkType": MessageLookupByLibrary.simpleMessage("Network Type"), "neutralScheme": MessageLookupByLibrary.simpleMessage("Neutral"), "noAnimation": MessageLookupByLibrary.simpleMessage("Default"), "noData": MessageLookupByLibrary.simpleMessage("No Data"), "noHotKey": MessageLookupByLibrary.simpleMessage("No Hotkeys"), "noIcon": MessageLookupByLibrary.simpleMessage("No Icon"), "noInfo": MessageLookupByLibrary.simpleMessage("No Info"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"), "noNetwork": MessageLookupByLibrary.simpleMessage("No Network"), "noNetworkApp": MessageLookupByLibrary.simpleMessage("No Network App"), "noProxy": MessageLookupByLibrary.simpleMessage("No Proxy"), "noProxyDesc": MessageLookupByLibrary.simpleMessage( "Please create or add a valid profile", ), "noResolve": MessageLookupByLibrary.simpleMessage("No Resolve"), "noStatusAvailable": MessageLookupByLibrary.simpleMessage("No Status"), "nodeExclusion": MessageLookupByLibrary.simpleMessage("Node Exclusion"), "nodeExclusionDesc": MessageLookupByLibrary.simpleMessage( "Exclude all matched nodes", ), "nodeExclusionPlaceholder": MessageLookupByLibrary.simpleMessage( "HK|Hong Kong|🇭🇰", ), "none": MessageLookupByLibrary.simpleMessage("None"), "notRecommended": MessageLookupByLibrary.simpleMessage("Not Recommended"), "notSelectedTip": MessageLookupByLibrary.simpleMessage( "Current proxy group cannot be selected.", ), "ntp": MessageLookupByLibrary.simpleMessage("NTP"), "ntpDesc": MessageLookupByLibrary.simpleMessage("Use NTP time service"), "ntpInterval": MessageLookupByLibrary.simpleMessage("Update Interval"), "ntpPort": MessageLookupByLibrary.simpleMessage("Port"), "ntpServer": MessageLookupByLibrary.simpleMessage("Server"), "ntpStatus": MessageLookupByLibrary.simpleMessage("Status"), "ntpStatusDesc": MessageLookupByLibrary.simpleMessage( "Enable NTP time service", ), "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "No profile. Please add one.", ), "nullTip": m5, "numberTip": m6, "oneColumn": MessageLookupByLibrary.simpleMessage("1 Column"), "onlinePanel": MessageLookupByLibrary.simpleMessage("Online Panel"), "onlyIcon": MessageLookupByLibrary.simpleMessage("Icon Only"), "onlyOtherApps": MessageLookupByLibrary.simpleMessage( "Third-Party Apps Only", ), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage( "Proxy Traffic Only", ), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( "Only record proxy traffic", ), "openDashboard": MessageLookupByLibrary.simpleMessage("Open Zashboard"), "openSettings": MessageLookupByLibrary.simpleMessage("Open Settings"), "options": MessageLookupByLibrary.simpleMessage("Options"), "other": MessageLookupByLibrary.simpleMessage("Other"), "otherContributors": MessageLookupByLibrary.simpleMessage( "Other Contributors", ), "otherSettings": MessageLookupByLibrary.simpleMessage("Enhanced Tools"), "otherSettingsDesc": MessageLookupByLibrary.simpleMessage( "Modify enhanced tool settings", ), "outboundMode": MessageLookupByLibrary.simpleMessage("Outbound Mode"), "override": MessageLookupByLibrary.simpleMessage("Override"), "overrideDesc": MessageLookupByLibrary.simpleMessage( "Override proxy configurations", ), "overrideDestination": MessageLookupByLibrary.simpleMessage( "Override Destination", ), "overrideDestinationDesc": MessageLookupByLibrary.simpleMessage( "Override destination with sniffed result", ), "overrideDns": MessageLookupByLibrary.simpleMessage("Override DNS"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage( "Override profile\'s DNS settings", ), "overrideExperimental": MessageLookupByLibrary.simpleMessage( "Override Experimental", ), "overrideExperimentalDesc": MessageLookupByLibrary.simpleMessage( "Override profile\'s Experimental settings", ), "overrideInvalidTip": MessageLookupByLibrary.simpleMessage( "Inactive in script mode", ), "overrideNtp": MessageLookupByLibrary.simpleMessage("Override NTP"), "overrideNtpDesc": MessageLookupByLibrary.simpleMessage( "Override profile\'s NTP settings", ), "overrideOriginRules": MessageLookupByLibrary.simpleMessage( "Override Original Rules", ), "overrideSniffer": MessageLookupByLibrary.simpleMessage("Override Sniffer"), "overrideSnifferDesc": MessageLookupByLibrary.simpleMessage( "Override profile\'s Sniffer settings", ), "overrideTestUrl": MessageLookupByLibrary.simpleMessage("Override Config"), "overrideTunnel": MessageLookupByLibrary.simpleMessage("Override Tunnel"), "overrideTunnelDesc": MessageLookupByLibrary.simpleMessage( "Override profile\'s Tunnel settings", ), "packageListPermissionDenied": MessageLookupByLibrary.simpleMessage( "Permission denied. Cannot access app list.", ), "packageListPermissionRequired": MessageLookupByLibrary.simpleMessage( "Permission to access installed apps is required. Grant now?", ), "palette": MessageLookupByLibrary.simpleMessage("Palette"), "parsePureIp": MessageLookupByLibrary.simpleMessage("Parse Pure IP"), "parsePureIpDesc": MessageLookupByLibrary.simpleMessage( "Parse pure IP connections", ), "password": MessageLookupByLibrary.simpleMessage("Password"), "paste": MessageLookupByLibrary.simpleMessage("Paste"), "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage( "Please bind WebDAV", ), "pleaseCloseSystemProxyFirst": MessageLookupByLibrary.simpleMessage( "Please close System Proxy first", ), "pleaseCloseTunFirst": MessageLookupByLibrary.simpleMessage( "Please close TUN first", ), "pleaseEnterScriptName": MessageLookupByLibrary.simpleMessage( "Please enter a script name", ), "pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage( "Please enter the admin password", ), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage( "Please upload a file", ), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "Please upload a valid QR code", ), "port": MessageLookupByLibrary.simpleMessage("Port"), "portConflictTip": MessageLookupByLibrary.simpleMessage( "Please enter a different port", ), "portTip": m7, "powerSwitch": MessageLookupByLibrary.simpleMessage("Power Switch"), "preferH3Desc": MessageLookupByLibrary.simpleMessage( "Prioritize DoH HTTP/3", ), "pressKeyboard": MessageLookupByLibrary.simpleMessage("Press a key"), "preview": MessageLookupByLibrary.simpleMessage("Preview"), "profile": MessageLookupByLibrary.simpleMessage("Profile"), "profileAutoUpdateIntervalInvalidValidationDesc": MessageLookupByLibrary.simpleMessage("Please enter a valid interval"), "profileAutoUpdateIntervalNullValidationDesc": MessageLookupByLibrary.simpleMessage("Please enter update interval"), "profileHasUpdate": MessageLookupByLibrary.simpleMessage( "Profile modified. Disable auto-update?", ), "profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage( "Please enter a profile name", ), "profileParseErrorDesc": MessageLookupByLibrary.simpleMessage( "Profile parse error", ), "profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage( "Please enter a valid URL", ), "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage( "Please enter a profile URL", ), "profiles": MessageLookupByLibrary.simpleMessage("Profiles"), "profilesSort": MessageLookupByLibrary.simpleMessage("Profile Sorting"), "progress": MessageLookupByLibrary.simpleMessage("Progress"), "project": MessageLookupByLibrary.simpleMessage("Project"), "providers": MessageLookupByLibrary.simpleMessage("Providers"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("Proxy Settings"), "proxyChains": MessageLookupByLibrary.simpleMessage("Proxy Chains"), "proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy Group"), "proxyNameserver": MessageLookupByLibrary.simpleMessage("Proxy Nameserver"), "proxyNameserverDesc": MessageLookupByLibrary.simpleMessage( "Used to resolve proxy nodes", ), "proxyPort": MessageLookupByLibrary.simpleMessage("Proxy Port"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage( "Set the Clash listening port", ), "proxyProviders": MessageLookupByLibrary.simpleMessage("Proxy Providers"), "pulse": MessageLookupByLibrary.simpleMessage("Pulse"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("Pure Black Mode"), "qrcode": MessageLookupByLibrary.simpleMessage("QR Code"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage( "Scan QR code to get profile", ), "quicGoDisableEcn": MessageLookupByLibrary.simpleMessage( "Disable QUIC ECN", ), "quicGoDisableEcnDesc": MessageLookupByLibrary.simpleMessage( "Disable QUIC Explicit Congestion Notification", ), "quicGoDisableGso": MessageLookupByLibrary.simpleMessage( "Disable QUIC GSO", ), "quicGoDisableGsoDesc": MessageLookupByLibrary.simpleMessage( "Disable QUIC Generic Segmentation Offload", ), "quicPortSniffer": MessageLookupByLibrary.simpleMessage( "QUIC Port Sniffing", ), "quickResponse": MessageLookupByLibrary.simpleMessage("Quick Response"), "quickResponseDesc": MessageLookupByLibrary.simpleMessage( "Disconnect on network change (WiFi/Mobile)", ), "rainbowScheme": MessageLookupByLibrary.simpleMessage("Rainbow"), "recovery": MessageLookupByLibrary.simpleMessage("Restore"), "recoveryAll": MessageLookupByLibrary.simpleMessage("Restore All Data"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage( "Restore Profiles Only", ), "recoveryStrategy": MessageLookupByLibrary.simpleMessage( "Recovery Strategy", ), "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage( "Compatible", ), "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage( "Override", ), "recoverySuccess": MessageLookupByLibrary.simpleMessage( "Restore Successful", ), "redirPort": MessageLookupByLibrary.simpleMessage("Redir Port"), "redo": MessageLookupByLibrary.simpleMessage("Redo"), "refreshAppList": MessageLookupByLibrary.simpleMessage("Refresh App List"), "refreshAppListConfirm": MessageLookupByLibrary.simpleMessage( "Refresh the app list?", ), "regExp": MessageLookupByLibrary.simpleMessage("RegExp"), "remote": MessageLookupByLibrary.simpleMessage("Remote"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage( "Backup data to WebDAV", ), "remoteDestination": MessageLookupByLibrary.simpleMessage( "Remote Destination", ), "remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage( "Restore data from WebDAV", ), "remove": MessageLookupByLibrary.simpleMessage("Remove"), "rename": MessageLookupByLibrary.simpleMessage("Rename"), "request": MessageLookupByLibrary.simpleMessage("Request"), "requests": MessageLookupByLibrary.simpleMessage("Requests"), "requestsDesc": MessageLookupByLibrary.simpleMessage( "View recent request logs", ), "reset": MessageLookupByLibrary.simpleMessage("Reset"), "resetTip": MessageLookupByLibrary.simpleMessage( "Are you sure you want to reset?", ), "resources": MessageLookupByLibrary.simpleMessage("Resources"), "resourcesDesc": MessageLookupByLibrary.simpleMessage( "External resource info", ), "respectRules": MessageLookupByLibrary.simpleMessage("Respect Rules"), "respectRulesDesc": MessageLookupByLibrary.simpleMessage( "DNS connections follow Rules", ), "restart": MessageLookupByLibrary.simpleMessage("Restart"), "restartCoreDesc": MessageLookupByLibrary.simpleMessage( "Manually restart the core?", ), "restartCoreTitle": MessageLookupByLibrary.simpleMessage("Restart Core"), "restartTip": MessageLookupByLibrary.simpleMessage( "Restart TUN for changes to take effect", ), "retry": MessageLookupByLibrary.simpleMessage("Retry"), "rotatingCircle": MessageLookupByLibrary.simpleMessage("Rotating Circle"), "ru": MessageLookupByLibrary.simpleMessage("Russian"), "rule": MessageLookupByLibrary.simpleMessage("Rule"), "ruleName": MessageLookupByLibrary.simpleMessage("Rule Name"), "ruleProviders": MessageLookupByLibrary.simpleMessage("Rule Providers"), "ruleTarget": MessageLookupByLibrary.simpleMessage("Rule Target"), "runTime": MessageLookupByLibrary.simpleMessage("Uptime"), "runtimeConfig": MessageLookupByLibrary.simpleMessage("Runtime Config"), "save": MessageLookupByLibrary.simpleMessage("Save"), "saveChanges": MessageLookupByLibrary.simpleMessage("Save changes?"), "saveTip": MessageLookupByLibrary.simpleMessage( "Are you sure you want to save?", ), "script": MessageLookupByLibrary.simpleMessage("Script"), "scriptDesc": MessageLookupByLibrary.simpleMessage( "Global override script config", ), "search": MessageLookupByLibrary.simpleMessage("Search"), "seconds": MessageLookupByLibrary.simpleMessage("Seconds"), "secretCopied": MessageLookupByLibrary.simpleMessage( "Secret copied to clipboard", ), "selectAll": MessageLookupByLibrary.simpleMessage("Select All"), "selected": MessageLookupByLibrary.simpleMessage("Selected"), "selectedCountTitle": m8, "serviceReady": MessageLookupByLibrary.simpleMessage("Service Ready"), "serviceRunning": MessageLookupByLibrary.simpleMessage("Service Running"), "settings": MessageLookupByLibrary.simpleMessage("Settings"), "show": MessageLookupByLibrary.simpleMessage("Show"), "shrink": MessageLookupByLibrary.simpleMessage("Compact"), "silentLaunch": MessageLookupByLibrary.simpleMessage("Silent Launch"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage( "Start in the background", ), "size": MessageLookupByLibrary.simpleMessage("Size"), "skipDomain": MessageLookupByLibrary.simpleMessage("Skip Domain"), "skipDstAddress": MessageLookupByLibrary.simpleMessage( "Skip Destination IP", ), "skipSrcAddress": MessageLookupByLibrary.simpleMessage("Skip Source IP"), "smartAutoStop": MessageLookupByLibrary.simpleMessage("Smart Auto-Stop"), "smartAutoStopDesc": MessageLookupByLibrary.simpleMessage( "Stop VPN on specific networks", ), "smartAutoStopServiceRunning": MessageLookupByLibrary.simpleMessage( "Smart Auto-Stop running", ), "smartDelayLaunch": MessageLookupByLibrary.simpleMessage("Smart Delay"), "smartDelayLaunchDesc": MessageLookupByLibrary.simpleMessage( "Launch after network connected", ), "sniffer": MessageLookupByLibrary.simpleMessage("Sniffer"), "snifferAddressHint": MessageLookupByLibrary.simpleMessage( "One address per line", ), "snifferDesc": MessageLookupByLibrary.simpleMessage( "Modify domain sniffer config", ), "snifferDomainHint": MessageLookupByLibrary.simpleMessage( "One domain per line", ), "snifferPorts": MessageLookupByLibrary.simpleMessage("Ports"), "snifferPortsHint": MessageLookupByLibrary.simpleMessage( "e.g.: 80, 8080-8880", ), "snifferStatus": MessageLookupByLibrary.simpleMessage("Status"), "snifferStatusDesc": MessageLookupByLibrary.simpleMessage( "Enable Sniffer service", ), "socksPort": MessageLookupByLibrary.simpleMessage("Socks Port"), "sort": MessageLookupByLibrary.simpleMessage("Sort"), "source": MessageLookupByLibrary.simpleMessage("Source"), "sourceIp": MessageLookupByLibrary.simpleMessage("Source IP"), "specialProxy": MessageLookupByLibrary.simpleMessage("Special Proxy"), "specialRules": MessageLookupByLibrary.simpleMessage("Special Rules"), "spinningLines": MessageLookupByLibrary.simpleMessage("Spinning Lines"), "stackMode": MessageLookupByLibrary.simpleMessage("Stack Mode"), "standard": MessageLookupByLibrary.simpleMessage("Standard"), "start": MessageLookupByLibrary.simpleMessage("Start"), "startTest": MessageLookupByLibrary.simpleMessage("Start Test"), "startVpn": MessageLookupByLibrary.simpleMessage("Starting..."), "status": MessageLookupByLibrary.simpleMessage("Status"), "statusDesc": MessageLookupByLibrary.simpleMessage( "Uses system DNS when disabled", ), "stop": MessageLookupByLibrary.simpleMessage("Stop"), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping..."), "storeFix": MessageLookupByLibrary.simpleMessage("Store Fix"), "storeFixDesc": MessageLookupByLibrary.simpleMessage( "Fix Play Store download issues", ), "strictRoute": MessageLookupByLibrary.simpleMessage("Strict Route"), "strictRouteDesc": MessageLookupByLibrary.simpleMessage( "Use TUN strict routing mode", ), "style": MessageLookupByLibrary.simpleMessage("Style"), "subRule": MessageLookupByLibrary.simpleMessage("Sub Rule"), "submit": MessageLookupByLibrary.simpleMessage("Submit"), "success": MessageLookupByLibrary.simpleMessage("Success"), "switchLabel": MessageLookupByLibrary.simpleMessage("Switch"), "switchToDomesticIp": MessageLookupByLibrary.simpleMessage( "Get Domestic IP", ), "sync": MessageLookupByLibrary.simpleMessage("Sync"), "syncAll": MessageLookupByLibrary.simpleMessage("Sync All"), "syncFailed": MessageLookupByLibrary.simpleMessage("Sync Failed"), "system": MessageLookupByLibrary.simpleMessage("System"), "systemApp": MessageLookupByLibrary.simpleMessage("System App"), "systemFont": MessageLookupByLibrary.simpleMessage("System Font"), "systemProxy": MessageLookupByLibrary.simpleMessage("System Proxy"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("Set system proxy"), "tab": MessageLookupByLibrary.simpleMessage("Tab"), "tabAnimation": MessageLookupByLibrary.simpleMessage("Tab Animation"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( "Effective only in mobile view", ), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP Concurrent"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage( "Allow concurrent TCP connections", ), "testUrl": MessageLookupByLibrary.simpleMessage("Test URL"), "textScale": MessageLookupByLibrary.simpleMessage("Text Scaling"), "theme": MessageLookupByLibrary.simpleMessage("Theme"), "themeColor": MessageLookupByLibrary.simpleMessage("Theme Color"), "themeDesc": MessageLookupByLibrary.simpleMessage( "Set theme color and icon", ), "themeMode": MessageLookupByLibrary.simpleMessage("Theme Mode"), "threeBounce": MessageLookupByLibrary.simpleMessage("Three Bounce"), "threeColumns": MessageLookupByLibrary.simpleMessage("3 Columns"), "threeInOut": MessageLookupByLibrary.simpleMessage("Three In Out"), "tight": MessageLookupByLibrary.simpleMessage("Compact"), "time": MessageLookupByLibrary.simpleMessage("Time"), "tip": MessageLookupByLibrary.simpleMessage("Tip"), "tlsPortSniffer": MessageLookupByLibrary.simpleMessage("TLS Port Sniffing"), "toggle": MessageLookupByLibrary.simpleMessage("Toggle"), "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("Tonal Spot"), "tooManyRules": MessageLookupByLibrary.simpleMessage("Max 2 rules allowed"), "tools": MessageLookupByLibrary.simpleMessage("Tools"), "tproxyPort": MessageLookupByLibrary.simpleMessage("Tproxy Port"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic Usage"), "tryManualRefresh": MessageLookupByLibrary.simpleMessage( "Please try manual refresh", ), "tun": MessageLookupByLibrary.simpleMessage("TUN"), "tunDesc": MessageLookupByLibrary.simpleMessage( "Take over global device traffic", ), "tunEnableRequireAdmin": MessageLookupByLibrary.simpleMessage( "TUN requires admin privileges. Please run as Administrator.", ), "tunnel": MessageLookupByLibrary.simpleMessage("Tunnel"), "tunnelAddress": MessageLookupByLibrary.simpleMessage("Listen Address"), "tunnelAddressHint": MessageLookupByLibrary.simpleMessage( "e.g.: 127.0.0.1:6553", ), "tunnelDesc": MessageLookupByLibrary.simpleMessage( "Use traffic forwarding tunnel", ), "tunnelList": MessageLookupByLibrary.simpleMessage("Forwarding List"), "tunnelNetwork": MessageLookupByLibrary.simpleMessage("Network Protocol"), "tunnelNetworkHint": MessageLookupByLibrary.simpleMessage("e.g.: tcp, udp"), "tunnelProxy": MessageLookupByLibrary.simpleMessage("Proxy Name"), "tunnelProxyHint": MessageLookupByLibrary.simpleMessage( "e.g.: proxy (optional)", ), "tunnelTarget": MessageLookupByLibrary.simpleMessage("Target Address"), "tunnelTargetHint": MessageLookupByLibrary.simpleMessage( "e.g.: 114.114.114.114:53", ), "twoColumns": MessageLookupByLibrary.simpleMessage("2 Columns"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "Unable to update current profile", ), "undo": MessageLookupByLibrary.simpleMessage("Undo"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("Unified Delay"), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage( "Exclude handshake delays from testing", ), "unknown": MessageLookupByLibrary.simpleMessage("Unknown"), "unnamed": MessageLookupByLibrary.simpleMessage("Unnamed"), "update": MessageLookupByLibrary.simpleMessage("Update"), "upload": MessageLookupByLibrary.simpleMessage("Upload"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("Get profile via URL"), "urlTip": m9, "useHosts": MessageLookupByLibrary.simpleMessage("Use Hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("Use System Hosts"), "value": MessageLookupByLibrary.simpleMessage("Value"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("Vibrant"), "view": MessageLookupByLibrary.simpleMessage("View"), "vpnDesc": MessageLookupByLibrary.simpleMessage("VPN-related settings"), "vpnEnableDesc": MessageLookupByLibrary.simpleMessage( "Route all system traffic via VpnService", ), "vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage( "Attach HTTP proxy to VpnService", ), "vpnTip": MessageLookupByLibrary.simpleMessage( "Restart VPN to apply changes", ), "wakelock": MessageLookupByLibrary.simpleMessage("Wakelock"), "wakelockDescription": MessageLookupByLibrary.simpleMessage( "Keeps the screen on and app active in the background without requiring special CPU wakelock permissions.", ), "wave": MessageLookupByLibrary.simpleMessage("Wave"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage( "WebDAV Configuration", ), "whitelist": MessageLookupByLibrary.simpleMessage("Whitelist"), "whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist Mode"), "writeToSystem": MessageLookupByLibrary.simpleMessage("Write to System"), "writeToSystemDesc": MessageLookupByLibrary.simpleMessage( "Requires administrator privileges", ), "years": MessageLookupByLibrary.simpleMessage("Years"), "zh_CN": MessageLookupByLibrary.simpleMessage("Simplified Chinese"), "zh_TC": MessageLookupByLibrary.simpleMessage("Traditional Chinese"), }; } ================================================ FILE: lib/l10n/intl/messages_ru.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a ru locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ru'; static String m0(label) => "Удалить выбранные ${label}?"; static String m1(label) => "Удалить текущий ${label}?"; static String m2(label) => "Подробности ${label}"; static String m3(label) => "${label} не может быть пустым"; static String m4(label) => "${label} уже существует"; static String m5(label) => "${label} отсутствует"; static String m6(label) => "${label} должен быть числом"; static String m7(label) => "${label} должен быть от 1024 до 49151"; static String m8(count) => "Выбрано: ${count}"; static String m9(label) => "${label} должен быть URL"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("О программе"), "accessControl": MessageLookupByLibrary.simpleMessage("Контроль доступа"), "accessControlAllowDesc": MessageLookupByLibrary.simpleMessage( "Только выбранные приложения используют VPN", ), "accessControlDesc": MessageLookupByLibrary.simpleMessage( "Настройка доступа приложений к прокси", ), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage( "Выбранные приложения исключены из VPN", ), "account": MessageLookupByLibrary.simpleMessage("Аккаунт"), "action": MessageLookupByLibrary.simpleMessage("Действие"), "action_mode": MessageLookupByLibrary.simpleMessage("Сменить режим"), "action_proxy": MessageLookupByLibrary.simpleMessage("Системный прокси"), "action_start": MessageLookupByLibrary.simpleMessage("Запуск/Остановка"), "action_tun": MessageLookupByLibrary.simpleMessage("Виртуальный адаптер"), "action_view": MessageLookupByLibrary.simpleMessage("Показать/Скрыть"), "add": MessageLookupByLibrary.simpleMessage("Добавить"), "addProfile": MessageLookupByLibrary.simpleMessage("Добавить профиль"), "addRule": MessageLookupByLibrary.simpleMessage("Добавить правило"), "addTunnel": MessageLookupByLibrary.simpleMessage( "Добавить перенаправление", ), "addedOriginRules": MessageLookupByLibrary.simpleMessage( "Добавить к исходным", ), "address": MessageLookupByLibrary.simpleMessage("Адрес"), "addressHelp": MessageLookupByLibrary.simpleMessage("Адрес сервера WebDAV"), "addressTip": MessageLookupByLibrary.simpleMessage( "Введите корректный адрес WebDAV", ), "adminAutoLaunch": MessageLookupByLibrary.simpleMessage( "Автозапуск от администратора", ), "adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage( "Автозапуск с правами администратора", ), "advancedSettings": MessageLookupByLibrary.simpleMessage( "Расширенные настройки", ), "ago": MessageLookupByLibrary.simpleMessage("назад"), "agree": MessageLookupByLibrary.simpleMessage("Согласен"), "allApps": MessageLookupByLibrary.simpleMessage("Все приложения"), "allowBypass": MessageLookupByLibrary.simpleMessage("Разрешить обход VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage( "Некоторые приложения смогут обходить VPN", ), "allowLan": MessageLookupByLibrary.simpleMessage("LAN доступ"), "allowLanDesc": MessageLookupByLibrary.simpleMessage( "Разрешить доступ из локальной сети", ), "alreadyInWhitelist": MessageLookupByLibrary.simpleMessage( "Уже в белом списке", ), "app": MessageLookupByLibrary.simpleMessage("Приложение"), "appAccessControl": MessageLookupByLibrary.simpleMessage( "Контроль доступа приложений", ), "appDesc": MessageLookupByLibrary.simpleMessage("Настройки приложения"), "application": MessageLookupByLibrary.simpleMessage("Приложение"), "applicationDesc": MessageLookupByLibrary.simpleMessage( "Настройки приложения", ), "auto": MessageLookupByLibrary.simpleMessage("Авто"), "autoCheckUpdate": MessageLookupByLibrary.simpleMessage("Автообновление"), "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage( "Проверка обновлений при запуске", ), "autoCloseConnections": MessageLookupByLibrary.simpleMessage( "Автозакрытие соединений", ), "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage( "Закрывать соединения при смене узла", ), "autoLaunch": MessageLookupByLibrary.simpleMessage("Автозапуск"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage( "Запуск при старте системы", ), "autoRun": MessageLookupByLibrary.simpleMessage("Автоподключение"), "autoRunDesc": MessageLookupByLibrary.simpleMessage( "Подключаться при запуске приложения", ), "autoSetSystemDns": MessageLookupByLibrary.simpleMessage( "Автоматически настроить системный DNS", ), "autoUpdate": MessageLookupByLibrary.simpleMessage("Автообновление"), "autoUpdateInterval": MessageLookupByLibrary.simpleMessage( "Интервал автообновления (минуты)", ), "backup": MessageLookupByLibrary.simpleMessage("Создать копию"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage( "Резервное копирование", ), "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage( "Синхронизация данных через WebDAV или локально", ), "backupSuccess": MessageLookupByLibrary.simpleMessage( "Резервное копирование успешно", ), "basicConfig": MessageLookupByLibrary.simpleMessage( "Базовая конфигурация ядра", ), "basicConfigDesc": MessageLookupByLibrary.simpleMessage( "Глобальное изменение конфигурации ядра", ), "batteryOptimization": MessageLookupByLibrary.simpleMessage( "Оптимизация батареи", ), "batteryOptimizationDesc": MessageLookupByLibrary.simpleMessage( "Запросить добавление в белый список энергосбережения", ), "bind": MessageLookupByLibrary.simpleMessage("Привязать"), "blacklist": MessageLookupByLibrary.simpleMessage("Чёрный список"), "blacklistMode": MessageLookupByLibrary.simpleMessage( "Режим чёрного списка", ), "bypassDomain": MessageLookupByLibrary.simpleMessage("Исключить домены"), "bypassDomainDesc": MessageLookupByLibrary.simpleMessage( "Работает только при включённом системном прокси", ), "bypassPrivateRoute": MessageLookupByLibrary.simpleMessage( "Обход частной сети", ), "bypassPrivateRouteDesc": MessageLookupByLibrary.simpleMessage( "Автоматически обходить IP-адреса частной сети", ), "cacheAlgorithm": MessageLookupByLibrary.simpleMessage("Алгоритм кэша"), "cacheCorrupt": MessageLookupByLibrary.simpleMessage( "Кэш повреждён. Очистить?", ), "cameraPermissionDenied": MessageLookupByLibrary.simpleMessage( "Доступ к камере запрещён", ), "cameraPermissionDesc": MessageLookupByLibrary.simpleMessage( "Для сканирования QR-кода требуется доступ к камере. Пожалуйста, предоставьте разрешение в настройках.", ), "cancel": MessageLookupByLibrary.simpleMessage("Отмена"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage( "Показать системные приложения", ), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("Отменить выбор"), "checkError": MessageLookupByLibrary.simpleMessage("Ошибка проверки"), "checkOrAddProfile": MessageLookupByLibrary.simpleMessage( "Добавьте профиль", ), "checkUpdate": MessageLookupByLibrary.simpleMessage("Проверить обновление"), "checkUpdateError": MessageLookupByLibrary.simpleMessage( "Установлена последняя версия", ), "checking": MessageLookupByLibrary.simpleMessage("Проверка..."), "circle": MessageLookupByLibrary.simpleMessage("Круг"), "clearCacheDesc": MessageLookupByLibrary.simpleMessage( "Очистить кэш FakeIP и DNS?", ), "clearCacheTitle": MessageLookupByLibrary.simpleMessage("Очистить кэш"), "clearData": MessageLookupByLibrary.simpleMessage("Очистить данные"), "clipboard": MessageLookupByLibrary.simpleMessage("Буфер обмена"), "clipboardDesc": MessageLookupByLibrary.simpleMessage( "Автоматически получать ссылки из буфера обмена", ), "clipboardExport": MessageLookupByLibrary.simpleMessage("Экспорт в буфер"), "clipboardImport": MessageLookupByLibrary.simpleMessage("Импорт из буфера"), "color": MessageLookupByLibrary.simpleMessage("Цвет"), "colorSchemes": MessageLookupByLibrary.simpleMessage("Цветовые схемы"), "columns": MessageLookupByLibrary.simpleMessage("Колонки"), "compatible": MessageLookupByLibrary.simpleMessage("Режим совместимости"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( "Включает полную поддержку Clash с потерей некоторых функций", ), "concurrencyLimit": MessageLookupByLibrary.simpleMessage( "Лимит параллелизма", ), "concurrencyLimitDesc": MessageLookupByLibrary.simpleMessage( "Максимальное количество параллельных тестов задержки", ), "confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"), "connection": MessageLookupByLibrary.simpleMessage("Активные соединения"), "connections": MessageLookupByLibrary.simpleMessage("Соединения"), "connectionsDesc": MessageLookupByLibrary.simpleMessage( "Просмотр текущих соединений", ), "connectivity": MessageLookupByLibrary.simpleMessage("Подключение:"), "contactMe": MessageLookupByLibrary.simpleMessage("Связаться со мной"), "content": MessageLookupByLibrary.simpleMessage("Содержимое"), "contentScheme": MessageLookupByLibrary.simpleMessage("Контентная тема"), "controlSecret": MessageLookupByLibrary.simpleMessage("Пароль управления"), "controlSecretDesc": MessageLookupByLibrary.simpleMessage( "Пароль для доступа к RESTful API", ), "copy": MessageLookupByLibrary.simpleMessage("Копировать"), "copyEnvVar": MessageLookupByLibrary.simpleMessage( "Копировать переменные окружения", ), "copyLink": MessageLookupByLibrary.simpleMessage("Копировать ссылку"), "copySuccess": MessageLookupByLibrary.simpleMessage("Скопировано"), "core": MessageLookupByLibrary.simpleMessage("Ядро"), "coreConnected": MessageLookupByLibrary.simpleMessage("Подключено"), "coreInfo": MessageLookupByLibrary.simpleMessage("Информация о ядре"), "coreSuspended": MessageLookupByLibrary.simpleMessage("Приостановлено"), "country": MessageLookupByLibrary.simpleMessage("Регион"), "crashTest": MessageLookupByLibrary.simpleMessage("Тест сбоя"), "create": MessageLookupByLibrary.simpleMessage("Создать"), "creationTime": MessageLookupByLibrary.simpleMessage("Время создания"), "custom": MessageLookupByLibrary.simpleMessage("Пользовательский"), "customUrl": MessageLookupByLibrary.simpleMessage("Пользовательский URL"), "cut": MessageLookupByLibrary.simpleMessage("Вырезать"), "dark": MessageLookupByLibrary.simpleMessage("Тёмная"), "dashboard": MessageLookupByLibrary.simpleMessage("Главная"), "days": MessageLookupByLibrary.simpleMessage("дней"), "defaultNameserver": MessageLookupByLibrary.simpleMessage( "DNS по умолчанию", ), "defaultNameserverDesc": MessageLookupByLibrary.simpleMessage( "Используется для разрешения DNS-серверов", ), "defaultSort": MessageLookupByLibrary.simpleMessage("По умолчанию"), "defaultText": MessageLookupByLibrary.simpleMessage("По умолчанию"), "delay": MessageLookupByLibrary.simpleMessage("Задержка"), "delayAnimation": MessageLookupByLibrary.simpleMessage("Анимация задержки"), "delayAnimationDesc": MessageLookupByLibrary.simpleMessage( "Настройка анимации при тестировании", ), "delaySort": MessageLookupByLibrary.simpleMessage("По задержке"), "delete": MessageLookupByLibrary.simpleMessage("Удалить"), "deleteMultipTip": m0, "deleteTip": m1, "deleteTunnel": MessageLookupByLibrary.simpleMessage( "Удалить перенаправление", ), "desc": MessageLookupByLibrary.simpleMessage( "Bettbox основан на мощном и гибком прокси-ядре Mihomo (Clash.Meta) и стремится к созданию лучшего пользовательского опыта. Форк от FlClash: Улучшенный опыт, готов к работе «из коробки»", ), "destination": MessageLookupByLibrary.simpleMessage("Адрес назначения"), "destinationGeoIP": MessageLookupByLibrary.simpleMessage( "Геолокация назначения", ), "destinationIPASN": MessageLookupByLibrary.simpleMessage( "IP ASN назначения", ), "details": m2, "detectionTip": MessageLookupByLibrary.simpleMessage( "Зависит от сторонних API, только для справки", ), "developerMode": MessageLookupByLibrary.simpleMessage("Режим разработчика"), "developerModeEnableTip": MessageLookupByLibrary.simpleMessage( "Режим разработчика включён.", ), "dialerIp4pConvert": MessageLookupByLibrary.simpleMessage( "Включить преобразование IP4P", ), "dialerIp4pConvertDesc": MessageLookupByLibrary.simpleMessage( "Включить преобразование IP4P в диалере", ), "direct": MessageLookupByLibrary.simpleMessage("Напрямую"), "directNameserver": MessageLookupByLibrary.simpleMessage("DNS для прямых"), "directNameserverDesc": MessageLookupByLibrary.simpleMessage( "Используется для разрешения прямых доменов", ), "directNameserverFollowPolicy": MessageLookupByLibrary.simpleMessage( "Прямой DNS следует правилам", ), "disableQuic": MessageLookupByLibrary.simpleMessage("Отключить QUIC"), "disableQuicDesc": MessageLookupByLibrary.simpleMessage( "Отключить QUIC для решения сетевых проблем", ), "disclaimer": MessageLookupByLibrary.simpleMessage( "Отказ от ответственности", ), "disclaimerDesc": MessageLookupByLibrary.simpleMessage( "Это бесплатное ПО с открытым исходным кодом, предназначенное только для обучения и личного тестирования. Действия прокси-провайдеров не связаны с этим ПО. Соглашаясь, вы подтверждаете, что полностью осведомлены об этом. Если не согласны, пожалуйста, выйдите!", ), "discoverNewVersion": MessageLookupByLibrary.simpleMessage( "Доступна новая версия", ), "discovery": MessageLookupByLibrary.simpleMessage("Доступно обновление"), "dnsDesc": MessageLookupByLibrary.simpleMessage("Настройки DNS"), "dnsHijack": MessageLookupByLibrary.simpleMessage("Перехват DNS"), "dnsHijackDesc": MessageLookupByLibrary.simpleMessage( "Перенаправить разбор в модуль DNS", ), "dnsMode": MessageLookupByLibrary.simpleMessage("Режим DNS"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("Пропустить"), "domain": MessageLookupByLibrary.simpleMessage("Домен"), "doubleBounce": MessageLookupByLibrary.simpleMessage("Двойной отскок"), "download": MessageLookupByLibrary.simpleMessage("Загрузка"), "dozeSuspend": MessageLookupByLibrary.simpleMessage("Поддержка Doze"), "dozeSuspendDesc": MessageLookupByLibrary.simpleMessage( "Синхронизация с режимом сна Android", ), "edit": MessageLookupByLibrary.simpleMessage("Редактировать"), "editTunnel": MessageLookupByLibrary.simpleMessage( "Изменить перенаправление", ), "emptyTip": m3, "en": MessageLookupByLibrary.simpleMessage("Английский"), "enableCrashReport": MessageLookupByLibrary.simpleMessage("Анализ сбоев"), "enableCrashReportDesc": MessageLookupByLibrary.simpleMessage( "Отправка отчётов о сбоях при необходимости", ), "enableOverride": MessageLookupByLibrary.simpleMessage( "Включить переопределение", ), "endpointIndependentNat": MessageLookupByLibrary.simpleMessage( "Улучшенный NAT", ), "endpointIndependentNatDesc": MessageLookupByLibrary.simpleMessage( "Включить NAT независимый от конечной точки", ), "entries": MessageLookupByLibrary.simpleMessage("записей"), "exclude": MessageLookupByLibrary.simpleMessage("Скрыть из недавних"), "excludeChina": MessageLookupByLibrary.simpleMessage("Исключить Китай"), "excludeChinaDesc": MessageLookupByLibrary.simpleMessage( "Разрешить QUIC-трафик Китая вместо полной блокировки", ), "excludeDesc": MessageLookupByLibrary.simpleMessage( "Скрыть приложение из недавних задач", ), "existsTip": m4, "exit": MessageLookupByLibrary.simpleMessage("Выход"), "expand": MessageLookupByLibrary.simpleMessage("Стандартный"), "experimental": MessageLookupByLibrary.simpleMessage("Экспериментальное"), "experimentalDesc": MessageLookupByLibrary.simpleMessage( "Экспериментальные настройки, используйте с осторожностью", ), "expirationTime": MessageLookupByLibrary.simpleMessage("Срок действия"), "exportFile": MessageLookupByLibrary.simpleMessage("Экспорт файла"), "exportLogs": MessageLookupByLibrary.simpleMessage("Экспорт логов"), "exportSuccess": MessageLookupByLibrary.simpleMessage("Экспорт успешен"), "expressiveScheme": MessageLookupByLibrary.simpleMessage("Экспрессивный"), "externalController": MessageLookupByLibrary.simpleMessage( "Внешнее управление", ), "externalControllerDesc": MessageLookupByLibrary.simpleMessage( "Управление ядром через REST API", ), "externalLink": MessageLookupByLibrary.simpleMessage("Внешняя ссылка"), "externalResources": MessageLookupByLibrary.simpleMessage( "Внешние ресурсы", ), "fadingCircle": MessageLookupByLibrary.simpleMessage("Затухающий круг"), "fadingFour": MessageLookupByLibrary.simpleMessage("Затухающие точки"), "fakeIpFilterMode": MessageLookupByLibrary.simpleMessage( "Режим фильтрации FakeIP", ), "fakeIpFilterModeDesc": MessageLookupByLibrary.simpleMessage( "Указать режим фильтрации FakeIP", ), "fakeipFilter": MessageLookupByLibrary.simpleMessage("Фильтр FakeIP"), "fakeipRange": MessageLookupByLibrary.simpleMessage("Диапазон FakeIP"), "fakeipRangeV6": MessageLookupByLibrary.simpleMessage("Диапазон FakeIPv6"), "fakeipTtl": MessageLookupByLibrary.simpleMessage("Время жизни FakeIP"), "fallback": MessageLookupByLibrary.simpleMessage("Fallback"), "fallbackDesc": MessageLookupByLibrary.simpleMessage( "Обычно используются зарубежные DNS", ), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Фильтр fallback"), "fcmOptimization": MessageLookupByLibrary.simpleMessage("Оптимизация FCM"), "fcmOptimizationDesc": MessageLookupByLibrary.simpleMessage( "Повышает стабильность FCM при прямом подключении", ), "fcmTip": MessageLookupByLibrary.simpleMessage( "FCM зависит от устройства. Для точных результатов отключите \'Разрешить обход VPN\'", ), "fidelityScheme": MessageLookupByLibrary.simpleMessage("Высокая точность"), "file": MessageLookupByLibrary.simpleMessage("Файл"), "fileDesc": MessageLookupByLibrary.simpleMessage( "Загрузить файл конфигурации", ), "fileIsUpdate": MessageLookupByLibrary.simpleMessage( "Файл изменён. Сохранить изменения?", ), "filterSystemApp": MessageLookupByLibrary.simpleMessage( "Скрыть системные приложения", ), "findProcessMode": MessageLookupByLibrary.simpleMessage("Поиск процесса"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage( "Включить поиск процесса", ), "fontFamily": MessageLookupByLibrary.simpleMessage("Шрифт"), "forceDnsMapping": MessageLookupByLibrary.simpleMessage( "Принудительное DNS-отображение", ), "forceDnsMappingDesc": MessageLookupByLibrary.simpleMessage( "Принудительно отображать результаты DNS на соединение", ), "forceDomain": MessageLookupByLibrary.simpleMessage( "Принудительный сниффинг доменов", ), "forceGCDesc": MessageLookupByLibrary.simpleMessage( "Выполнить сборку мусора ядра? Экспериментально, используйте с осторожностью", ), "forceGCTitle": MessageLookupByLibrary.simpleMessage("Принудительный GC"), "formatError": MessageLookupByLibrary.simpleMessage("Проверьте формат"), "fourColumns": MessageLookupByLibrary.simpleMessage("4 колонки"), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("Фруктовый салат"), "general": MessageLookupByLibrary.simpleMessage("Общие"), "generalDesc": MessageLookupByLibrary.simpleMessage( "Изменить общие настройки", ), "generateSecret": MessageLookupByLibrary.simpleMessage("Сгенерировать"), "geoData": MessageLookupByLibrary.simpleMessage("Геоданные"), "geodataLoader": MessageLookupByLibrary.simpleMessage( "Экономия памяти GEO", ), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage( "Использовать загрузчик GEO с низким потреблением памяти", ), "geoipCode": MessageLookupByLibrary.simpleMessage("Код GeoIP"), "getOriginRules": MessageLookupByLibrary.simpleMessage( "Получить исходные правила", ), "global": MessageLookupByLibrary.simpleMessage("Глобально"), "go": MessageLookupByLibrary.simpleMessage("Перейти"), "goDownload": MessageLookupByLibrary.simpleMessage("Перейти к загрузке"), "harmonyFont": MessageLookupByLibrary.simpleMessage("Шрифт Harmony"), "harmonyFontDesc": MessageLookupByLibrary.simpleMessage( "Использовать оптимизированный HarmonyOS Sans", ), "hasCacheChange": MessageLookupByLibrary.simpleMessage( "Сохранить изменения кэша?", ), "healthCheckTimeout": MessageLookupByLibrary.simpleMessage( "Таймаут проверки", ), "healthCheckTimeoutDesc": MessageLookupByLibrary.simpleMessage( "Таймаут проверки работоспособности узлов", ), "highRefreshRate": MessageLookupByLibrary.simpleMessage( "Высокая частота обновления", ), "highRefreshRateDesc": MessageLookupByLibrary.simpleMessage( "Включить поддержку максимальной частоты обновления устройства", ), "host": MessageLookupByLibrary.simpleMessage("Хост"), "hostsDesc": MessageLookupByLibrary.simpleMessage( "Добавить hosts к текущей конфигурации", ), "hotkeyConflict": MessageLookupByLibrary.simpleMessage( "Конфликт горячих клавиш", ), "hotkeyManagement": MessageLookupByLibrary.simpleMessage( "Управление горячими клавишами", ), "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage( "Управление приложением с клавиатуры", ), "hours": MessageLookupByLibrary.simpleMessage("часов"), "httpPortSniffer": MessageLookupByLibrary.simpleMessage( "HTTP порты сниффера", ), "icmpForwarding": MessageLookupByLibrary.simpleMessage("ICMP forwarding"), "icmpForwardingDesc": MessageLookupByLibrary.simpleMessage( "Включить поддержку ICMP Ping", ), "icon": MessageLookupByLibrary.simpleMessage("Иконка"), "iconConfiguration": MessageLookupByLibrary.simpleMessage( "Настройка иконки", ), "iconStyle": MessageLookupByLibrary.simpleMessage("Стиль иконок"), "import": MessageLookupByLibrary.simpleMessage("Импорт"), "importFailed": MessageLookupByLibrary.simpleMessage("Ошибка импорта"), "importFile": MessageLookupByLibrary.simpleMessage("Импорт из файла"), "importFromCode": MessageLookupByLibrary.simpleMessage("Импорт из кода"), "importFromURL": MessageLookupByLibrary.simpleMessage("Импорт из URL"), "importUrl": MessageLookupByLibrary.simpleMessage("Импорт по URL"), "infiniteTime": MessageLookupByLibrary.simpleMessage("Бессрочно"), "init": MessageLookupByLibrary.simpleMessage("Инициализация"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage( "Введите корректное сочетание клавиш", ), "intelligentSelected": MessageLookupByLibrary.simpleMessage("Умный выбор"), "internet": MessageLookupByLibrary.simpleMessage("Интернет"), "interval": MessageLookupByLibrary.simpleMessage("Интервал"), "intranetIP": MessageLookupByLibrary.simpleMessage("Локальный IP"), "invalidIpFormat": MessageLookupByLibrary.simpleMessage( "Неверный формат IP или CIDR", ), "ipClickBehavior": MessageLookupByLibrary.simpleMessage( "Переключение отображения", ), "ipPrivacyProtection": MessageLookupByLibrary.simpleMessage("Скрыть IP"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/маска"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("Включить поддержку IPv6"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage( "Разрешить входящие IPv6", ), "ja": MessageLookupByLibrary.simpleMessage("Японский"), "just": MessageLookupByLibrary.simpleMessage("только что"), "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage( "Интервал TCP keep-alive", ), "key": MessageLookupByLibrary.simpleMessage("Ключ"), "language": MessageLookupByLibrary.simpleMessage("Язык"), "layout": MessageLookupByLibrary.simpleMessage("Макет"), "light": MessageLookupByLibrary.simpleMessage("Светлая"), "lightIcon": MessageLookupByLibrary.simpleMessage("Лёгкая иконка"), "lightIconDesc": MessageLookupByLibrary.simpleMessage( "Переключить на светлый стиль рабочего стола вручную", ), "list": MessageLookupByLibrary.simpleMessage("Список"), "listen": MessageLookupByLibrary.simpleMessage("Прослушивание"), "local": MessageLookupByLibrary.simpleMessage("Локально"), "localBackupDesc": MessageLookupByLibrary.simpleMessage( "Локальное резервное копирование", ), "localRecoveryDesc": MessageLookupByLibrary.simpleMessage( "Восстановление из файла", ), "log": MessageLookupByLibrary.simpleMessage("Лог"), "logLevel": MessageLookupByLibrary.simpleMessage("Уровень логов"), "logcat": MessageLookupByLibrary.simpleMessage("Сбор логов"), "logcatDesc": MessageLookupByLibrary.simpleMessage("Показать раздел логов"), "logs": MessageLookupByLibrary.simpleMessage("Логи"), "logsDesc": MessageLookupByLibrary.simpleMessage("Просмотр журналов"), "logsTest": MessageLookupByLibrary.simpleMessage("Тест логов"), "loopback": MessageLookupByLibrary.simpleMessage("Разблокировка UWP"), "loopbackDesc": MessageLookupByLibrary.simpleMessage( "Инструмент для разблокировки UWP loopback", ), "loose": MessageLookupByLibrary.simpleMessage("Свободный"), "manualRefreshIp": MessageLookupByLibrary.simpleMessage("Обновить IP"), "memoryInfo": MessageLookupByLibrary.simpleMessage("Информация о памяти"), "messageTest": MessageLookupByLibrary.simpleMessage("Тест сообщения"), "messageTestTip": MessageLookupByLibrary.simpleMessage( "Это тестовое сообщение.", ), "min": MessageLookupByLibrary.simpleMessage("Минимальный"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage( "Сворачивать при выходе", ), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "Изменить поведение при выходе", ), "minutes": MessageLookupByLibrary.simpleMessage("минут"), "mixedPort": MessageLookupByLibrary.simpleMessage("Смешанный порт"), "mode": MessageLookupByLibrary.simpleMessage("Режим"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("Монохром"), "months": MessageLookupByLibrary.simpleMessage("месяцев"), "more": MessageLookupByLibrary.simpleMessage("Подробнее"), "name": MessageLookupByLibrary.simpleMessage("Имя"), "nameSort": MessageLookupByLibrary.simpleMessage("По имени"), "nameserver": MessageLookupByLibrary.simpleMessage("Серверы имён"), "nameserverDesc": MessageLookupByLibrary.simpleMessage( "Используется для разрешения доменов", ), "nameserverPolicy": MessageLookupByLibrary.simpleMessage("Политика DNS"), "nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage( "Указать политику DNS для конкретных доменов", ), "navBarHapticFeedback": MessageLookupByLibrary.simpleMessage( "Тактильная отдача", ), "navBarHapticFeedbackDesc": MessageLookupByLibrary.simpleMessage( "Вибрация при переключении нижней панели навигации", ), "network": MessageLookupByLibrary.simpleMessage("Сеть"), "networkDesc": MessageLookupByLibrary.simpleMessage("Настройки сети"), "networkDetection": MessageLookupByLibrary.simpleMessage("Проверка сети"), "networkFix": MessageLookupByLibrary.simpleMessage("Исправление сети"), "networkFixDesc": MessageLookupByLibrary.simpleMessage( "Исправляет значок сети Windows", ), "networkMatch": MessageLookupByLibrary.simpleMessage("Сопоставление сети"), "networkMatchHint": MessageLookupByLibrary.simpleMessage( "Введите IP или CIDR, максимум 2, через запятую", ), "networkSpeed": MessageLookupByLibrary.simpleMessage("Скорость сети"), "networkType": MessageLookupByLibrary.simpleMessage("Тип сети"), "neutralScheme": MessageLookupByLibrary.simpleMessage("Нейтральный"), "noAnimation": MessageLookupByLibrary.simpleMessage("По умолчанию"), "noData": MessageLookupByLibrary.simpleMessage("Нет данных"), "noHotKey": MessageLookupByLibrary.simpleMessage("Нет горячих клавиш"), "noIcon": MessageLookupByLibrary.simpleMessage("Без иконок"), "noInfo": MessageLookupByLibrary.simpleMessage("Нет информации"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage( "Нет дополнительной информации", ), "noNetwork": MessageLookupByLibrary.simpleMessage("Нет сети"), "noNetworkApp": MessageLookupByLibrary.simpleMessage("Приложения без сети"), "noProxy": MessageLookupByLibrary.simpleMessage("Нет прокси"), "noProxyDesc": MessageLookupByLibrary.simpleMessage( "Создайте или добавьте профиль", ), "noResolve": MessageLookupByLibrary.simpleMessage("Не разрешать IP"), "noStatusAvailable": MessageLookupByLibrary.simpleMessage( "Статус недоступен", ), "nodeExclusion": MessageLookupByLibrary.simpleMessage("Исключение узлов"), "nodeExclusionDesc": MessageLookupByLibrary.simpleMessage( "Исключить все узлы, соответствующие шаблону", ), "nodeExclusionPlaceholder": MessageLookupByLibrary.simpleMessage( "HK|Гонконг|🇭🇰", ), "none": MessageLookupByLibrary.simpleMessage("Нет"), "notRecommended": MessageLookupByLibrary.simpleMessage("Не рекомендуется"), "notSelectedTip": MessageLookupByLibrary.simpleMessage( "Невозможно выбрать эту группу прокси", ), "ntp": MessageLookupByLibrary.simpleMessage("NTP"), "ntpDesc": MessageLookupByLibrary.simpleMessage( "Использовать службу времени NTP", ), "ntpInterval": MessageLookupByLibrary.simpleMessage("Интервал обновления"), "ntpPort": MessageLookupByLibrary.simpleMessage("Порт NTP"), "ntpServer": MessageLookupByLibrary.simpleMessage("Сервер NTP"), "ntpStatus": MessageLookupByLibrary.simpleMessage("Статус NTP"), "ntpStatusDesc": MessageLookupByLibrary.simpleMessage( "Включить службу времени NTP", ), "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "Нет профиля, добавьте его", ), "nullTip": m5, "numberTip": m6, "oneColumn": MessageLookupByLibrary.simpleMessage("1 колонка"), "onlinePanel": MessageLookupByLibrary.simpleMessage("Онлайн-панель"), "onlyIcon": MessageLookupByLibrary.simpleMessage("Только иконки"), "onlyOtherApps": MessageLookupByLibrary.simpleMessage("Только сторонние"), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage( "Только прокси-трафик", ), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( "Считать только трафик через прокси", ), "openDashboard": MessageLookupByLibrary.simpleMessage("Открыть Zashboard"), "openSettings": MessageLookupByLibrary.simpleMessage("Открыть настройки"), "options": MessageLookupByLibrary.simpleMessage("Опции"), "other": MessageLookupByLibrary.simpleMessage("Другое"), "otherContributors": MessageLookupByLibrary.simpleMessage( "Другие участники", ), "otherSettings": MessageLookupByLibrary.simpleMessage( "Расширенные инструменты", ), "otherSettingsDesc": MessageLookupByLibrary.simpleMessage( "Настройка расширенных функций", ), "outboundMode": MessageLookupByLibrary.simpleMessage("Режим выхода"), "override": MessageLookupByLibrary.simpleMessage("Переопределение"), "overrideDesc": MessageLookupByLibrary.simpleMessage( "Переопределение конфигурации прокси", ), "overrideDestination": MessageLookupByLibrary.simpleMessage( "Переопределить назначение", ), "overrideDestinationDesc": MessageLookupByLibrary.simpleMessage( "Использовать результаты сниффинга для переопределения целевого адреса", ), "overrideDns": MessageLookupByLibrary.simpleMessage("Переопределить DNS"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage( "Включить переопределение настроек DNS в конфигурации", ), "overrideExperimental": MessageLookupByLibrary.simpleMessage( "Переопределить экспериментальное", ), "overrideExperimentalDesc": MessageLookupByLibrary.simpleMessage( "Включить переопределение экспериментальных настроек в конфигурации", ), "overrideInvalidTip": MessageLookupByLibrary.simpleMessage( "Не действует в режиме скрипта", ), "overrideNtp": MessageLookupByLibrary.simpleMessage("Переопределить NTP"), "overrideNtpDesc": MessageLookupByLibrary.simpleMessage( "Включить переопределение настроек NTP в конфигурации", ), "overrideOriginRules": MessageLookupByLibrary.simpleMessage( "Переопределить исходные", ), "overrideSniffer": MessageLookupByLibrary.simpleMessage( "Переопределить Sniffer", ), "overrideSnifferDesc": MessageLookupByLibrary.simpleMessage( "Включить переопределение настроек Sniffer в конфигурации", ), "overrideTestUrl": MessageLookupByLibrary.simpleMessage( "Переопределить URL теста", ), "overrideTunnel": MessageLookupByLibrary.simpleMessage( "Переопределить туннель", ), "overrideTunnelDesc": MessageLookupByLibrary.simpleMessage( "Включить переопределение настроек туннеля в конфигурации", ), "packageListPermissionDenied": MessageLookupByLibrary.simpleMessage( "Разрешение отклонено. Без доступа невозможно получить список приложений.", ), "packageListPermissionRequired": MessageLookupByLibrary.simpleMessage( "Эта функция требует доступа к списку установленных приложений. Предоставить разрешение?", ), "palette": MessageLookupByLibrary.simpleMessage("Палитра"), "parsePureIp": MessageLookupByLibrary.simpleMessage("Разбор чистых IP"), "parsePureIpDesc": MessageLookupByLibrary.simpleMessage( "Разбирать соединения по чистому IP", ), "password": MessageLookupByLibrary.simpleMessage("Пароль"), "paste": MessageLookupByLibrary.simpleMessage("Вставить"), "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage( "Привяжите WebDAV", ), "pleaseCloseSystemProxyFirst": MessageLookupByLibrary.simpleMessage( "Сначала отключите системный прокси", ), "pleaseCloseTunFirst": MessageLookupByLibrary.simpleMessage( "Сначала отключите виртуальный адаптер", ), "pleaseEnterScriptName": MessageLookupByLibrary.simpleMessage( "Введите название скрипта", ), "pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage( "Введите пароль администратора", ), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("Загрузите файл"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "Загрузите корректный QR-код", ), "port": MessageLookupByLibrary.simpleMessage("Порт"), "portConflictTip": MessageLookupByLibrary.simpleMessage( "Введите разные порты", ), "portTip": m7, "powerSwitch": MessageLookupByLibrary.simpleMessage("Переключатель"), "preferH3Desc": MessageLookupByLibrary.simpleMessage( "Приоритет HTTP/3 для DoH", ), "pressKeyboard": MessageLookupByLibrary.simpleMessage("Нажмите клавиши"), "preview": MessageLookupByLibrary.simpleMessage("Предпросмотр"), "profile": MessageLookupByLibrary.simpleMessage("Профиль"), "profileAutoUpdateIntervalInvalidValidationDesc": MessageLookupByLibrary.simpleMessage( "Введите корректный формат интервала", ), "profileAutoUpdateIntervalNullValidationDesc": MessageLookupByLibrary.simpleMessage("Введите интервал автообновления"), "profileHasUpdate": MessageLookupByLibrary.simpleMessage( "Конфигурация изменена. Отключить автообновление?", ), "profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage( "Введите имя профиля", ), "profileParseErrorDesc": MessageLookupByLibrary.simpleMessage( "Ошибка разбора профиля", ), "profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage( "Введите корректный URL профиля", ), "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage( "Введите URL профиля", ), "profiles": MessageLookupByLibrary.simpleMessage("Профили"), "profilesSort": MessageLookupByLibrary.simpleMessage("Сортировка профилей"), "progress": MessageLookupByLibrary.simpleMessage("Прогресс"), "project": MessageLookupByLibrary.simpleMessage("Проект"), "providers": MessageLookupByLibrary.simpleMessage("Провайдеры"), "proxies": MessageLookupByLibrary.simpleMessage("Прокси"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("Настройки прокси"), "proxyChains": MessageLookupByLibrary.simpleMessage("Цепочка прокси"), "proxyGroup": MessageLookupByLibrary.simpleMessage("Группа прокси"), "proxyNameserver": MessageLookupByLibrary.simpleMessage("DNS для прокси"), "proxyNameserverDesc": MessageLookupByLibrary.simpleMessage( "Используется для разрешения доменов прокси", ), "proxyPort": MessageLookupByLibrary.simpleMessage("Порт прокси"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage( "Установить порт прослушивания Clash", ), "proxyProviders": MessageLookupByLibrary.simpleMessage("Провайдеры прокси"), "pulse": MessageLookupByLibrary.simpleMessage("Пульсация"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("Чистый чёрный"), "qrcode": MessageLookupByLibrary.simpleMessage("QR-код"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage( "Сканировать QR для получения профиля", ), "quicGoDisableEcn": MessageLookupByLibrary.simpleMessage( "Отключить ECN QUIC", ), "quicGoDisableEcnDesc": MessageLookupByLibrary.simpleMessage( "Отключить Explicit Congestion Notification для QUIC", ), "quicGoDisableGso": MessageLookupByLibrary.simpleMessage( "Отключить GSO QUIC", ), "quicGoDisableGsoDesc": MessageLookupByLibrary.simpleMessage( "Отключить Generic Segmentation Offload для QUIC", ), "quicPortSniffer": MessageLookupByLibrary.simpleMessage( "QUIC порты сниффера", ), "quickResponse": MessageLookupByLibrary.simpleMessage("Быстрый отклик"), "quickResponseDesc": MessageLookupByLibrary.simpleMessage( "Активно отключать соединения при изменении сети", ), "rainbowScheme": MessageLookupByLibrary.simpleMessage("Радуга"), "recovery": MessageLookupByLibrary.simpleMessage("Восстановить"), "recoveryAll": MessageLookupByLibrary.simpleMessage("Все данные"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("Только профили"), "recoveryStrategy": MessageLookupByLibrary.simpleMessage( "Стратегия восстановления", ), "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage( "Совместимость", ), "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage( "Перезаписать", ), "recoverySuccess": MessageLookupByLibrary.simpleMessage( "Восстановление успешно", ), "redirPort": MessageLookupByLibrary.simpleMessage("Порт перенаправления"), "redo": MessageLookupByLibrary.simpleMessage("Повторить"), "refreshAppList": MessageLookupByLibrary.simpleMessage( "Обновить список приложений", ), "refreshAppListConfirm": MessageLookupByLibrary.simpleMessage( "Обновить список приложений?", ), "regExp": MessageLookupByLibrary.simpleMessage("Регулярное выражение"), "remote": MessageLookupByLibrary.simpleMessage("Удалённо"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage( "Резервное копирование на WebDAV", ), "remoteDestination": MessageLookupByLibrary.simpleMessage( "Удалённое назначение", ), "remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage( "Восстановление с WebDAV", ), "remove": MessageLookupByLibrary.simpleMessage("Удалить"), "rename": MessageLookupByLibrary.simpleMessage("Переименовать"), "request": MessageLookupByLibrary.simpleMessage("Запрос"), "requests": MessageLookupByLibrary.simpleMessage("Запросы"), "requestsDesc": MessageLookupByLibrary.simpleMessage( "Просмотр недавних запросов", ), "reset": MessageLookupByLibrary.simpleMessage("Сброс"), "resetTip": MessageLookupByLibrary.simpleMessage("Сбросить настройки?"), "resources": MessageLookupByLibrary.simpleMessage("Ресурсы"), "resourcesDesc": MessageLookupByLibrary.simpleMessage( "Информация о внешних ресурсах", ), "respectRules": MessageLookupByLibrary.simpleMessage("Следовать правилам"), "respectRulesDesc": MessageLookupByLibrary.simpleMessage( "DNS-соединения следуют правилам", ), "restart": MessageLookupByLibrary.simpleMessage("Перезапуск"), "restartCoreDesc": MessageLookupByLibrary.simpleMessage( "Перезапустить ядро вручную?", ), "restartCoreTitle": MessageLookupByLibrary.simpleMessage("Перезапуск ядра"), "restartTip": MessageLookupByLibrary.simpleMessage( "Изменения вступят в силу после перезапуска TUN", ), "retry": MessageLookupByLibrary.simpleMessage("Повторить"), "rotatingCircle": MessageLookupByLibrary.simpleMessage("Вращающийся круг"), "ru": MessageLookupByLibrary.simpleMessage("Русский"), "rule": MessageLookupByLibrary.simpleMessage("Правила"), "ruleName": MessageLookupByLibrary.simpleMessage("Имя правила"), "ruleProviders": MessageLookupByLibrary.simpleMessage("Провайдеры правил"), "ruleTarget": MessageLookupByLibrary.simpleMessage("Цель правила"), "runTime": MessageLookupByLibrary.simpleMessage("Время работы"), "runtimeConfig": MessageLookupByLibrary.simpleMessage("Конфигурация"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), "saveChanges": MessageLookupByLibrary.simpleMessage("Сохранить изменения?"), "saveTip": MessageLookupByLibrary.simpleMessage("Сохранить изменения?"), "script": MessageLookupByLibrary.simpleMessage("Скрипт"), "scriptDesc": MessageLookupByLibrary.simpleMessage( "Настройка глобального скрипта переопределения", ), "search": MessageLookupByLibrary.simpleMessage("Поиск"), "seconds": MessageLookupByLibrary.simpleMessage("секунд"), "secretCopied": MessageLookupByLibrary.simpleMessage( "Пароль скопирован в буфер обмена", ), "selectAll": MessageLookupByLibrary.simpleMessage("Выбрать все"), "selected": MessageLookupByLibrary.simpleMessage("Выбрано"), "selectedCountTitle": m8, "serviceReady": MessageLookupByLibrary.simpleMessage("Служба готова"), "serviceRunning": MessageLookupByLibrary.simpleMessage("Служба запущена"), "settings": MessageLookupByLibrary.simpleMessage("Настройки"), "show": MessageLookupByLibrary.simpleMessage("Показать"), "shrink": MessageLookupByLibrary.simpleMessage("Компактный"), "silentLaunch": MessageLookupByLibrary.simpleMessage("Тихий запуск"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage( "Запуск в фоне без открытия окна", ), "size": MessageLookupByLibrary.simpleMessage("Размер"), "skipDomain": MessageLookupByLibrary.simpleMessage("Пропустить домены"), "skipDstAddress": MessageLookupByLibrary.simpleMessage( "Пропустить IP назначения", ), "skipSrcAddress": MessageLookupByLibrary.simpleMessage( "Пропустить IP источника", ), "smartAutoStop": MessageLookupByLibrary.simpleMessage("Умная остановка"), "smartAutoStopDesc": MessageLookupByLibrary.simpleMessage( "Останавливать прокси при подключении к заданной сети", ), "smartAutoStopServiceRunning": MessageLookupByLibrary.simpleMessage( "Служба умной остановки работает", ), "smartDelayLaunch": MessageLookupByLibrary.simpleMessage("Умная задержка"), "smartDelayLaunchDesc": MessageLookupByLibrary.simpleMessage( "Запуск после успешного подключения к сети", ), "sniffer": MessageLookupByLibrary.simpleMessage("Sniffer"), "snifferAddressHint": MessageLookupByLibrary.simpleMessage( "Один адрес на строку", ), "snifferDesc": MessageLookupByLibrary.simpleMessage( "Настройка сниффинга доменов", ), "snifferDomainHint": MessageLookupByLibrary.simpleMessage( "Один домен на строку", ), "snifferPorts": MessageLookupByLibrary.simpleMessage("Порты"), "snifferPortsHint": MessageLookupByLibrary.simpleMessage( "Например: 80, 8080-8880", ), "snifferStatus": MessageLookupByLibrary.simpleMessage("Статус сниффера"), "snifferStatusDesc": MessageLookupByLibrary.simpleMessage( "Включить службу сниффинга", ), "socksPort": MessageLookupByLibrary.simpleMessage("Порт Socks"), "sort": MessageLookupByLibrary.simpleMessage("Сортировка"), "source": MessageLookupByLibrary.simpleMessage("Источник"), "sourceIp": MessageLookupByLibrary.simpleMessage("IP источника"), "specialProxy": MessageLookupByLibrary.simpleMessage("Специальный прокси"), "specialRules": MessageLookupByLibrary.simpleMessage("Специальные правила"), "spinningLines": MessageLookupByLibrary.simpleMessage("Вращающиеся линии"), "stackMode": MessageLookupByLibrary.simpleMessage("Режим стека"), "standard": MessageLookupByLibrary.simpleMessage("Стандартный"), "start": MessageLookupByLibrary.simpleMessage("Запуск"), "startTest": MessageLookupByLibrary.simpleMessage("Тест задержки"), "startVpn": MessageLookupByLibrary.simpleMessage("Запуск VPN"), "status": MessageLookupByLibrary.simpleMessage("Статус"), "statusDesc": MessageLookupByLibrary.simpleMessage( "Использовать системный DNS при выключении", ), "stop": MessageLookupByLibrary.simpleMessage("Остановка"), "stopVpn": MessageLookupByLibrary.simpleMessage("Остановка VPN"), "storeFix": MessageLookupByLibrary.simpleMessage("Исправление магазина"), "storeFixDesc": MessageLookupByLibrary.simpleMessage( "Исправляет проблемы загрузки Google Play", ), "strictRoute": MessageLookupByLibrary.simpleMessage( "Строгая маршрутизация", ), "strictRouteDesc": MessageLookupByLibrary.simpleMessage( "Использовать строгий режим маршрутизации TUN", ), "style": MessageLookupByLibrary.simpleMessage("Стиль"), "subRule": MessageLookupByLibrary.simpleMessage("Подправило"), "submit": MessageLookupByLibrary.simpleMessage("Отправить"), "success": MessageLookupByLibrary.simpleMessage("Успех"), "switchLabel": MessageLookupByLibrary.simpleMessage("Переключатель"), "switchToDomesticIp": MessageLookupByLibrary.simpleMessage( "Получить локальный IP", ), "sync": MessageLookupByLibrary.simpleMessage("Синхронизировать"), "syncAll": MessageLookupByLibrary.simpleMessage("Синхронизировать всё"), "syncFailed": MessageLookupByLibrary.simpleMessage("Ошибка синхронизации"), "system": MessageLookupByLibrary.simpleMessage("Система"), "systemApp": MessageLookupByLibrary.simpleMessage("Системные приложения"), "systemFont": MessageLookupByLibrary.simpleMessage("Системный шрифт"), "systemProxy": MessageLookupByLibrary.simpleMessage("Системный прокси"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage( "Настроить системный прокси", ), "tab": MessageLookupByLibrary.simpleMessage("Вкладки"), "tabAnimation": MessageLookupByLibrary.simpleMessage("Анимация вкладок"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( "Только для некоторых мобильных представлений", ), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP параллельно"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage( "Разрешить параллельные TCP-соединения", ), "testUrl": MessageLookupByLibrary.simpleMessage("URL теста"), "textScale": MessageLookupByLibrary.simpleMessage("Масштаб текста"), "theme": MessageLookupByLibrary.simpleMessage("Тема"), "themeColor": MessageLookupByLibrary.simpleMessage("Цвет темы"), "themeDesc": MessageLookupByLibrary.simpleMessage( "Настройка темы и иконок", ), "themeMode": MessageLookupByLibrary.simpleMessage("Режим темы"), "threeBounce": MessageLookupByLibrary.simpleMessage("Прыгающие точки"), "threeColumns": MessageLookupByLibrary.simpleMessage("3 колонки"), "threeInOut": MessageLookupByLibrary.simpleMessage("Три точки"), "tight": MessageLookupByLibrary.simpleMessage("Компактный"), "time": MessageLookupByLibrary.simpleMessage("Время"), "tip": MessageLookupByLibrary.simpleMessage("Подсказка"), "tlsPortSniffer": MessageLookupByLibrary.simpleMessage( "TLS порты сниффера", ), "toggle": MessageLookupByLibrary.simpleMessage("Переключить"), "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("Тональный акцент"), "tooManyRules": MessageLookupByLibrary.simpleMessage("Максимум 2 правила"), "tools": MessageLookupByLibrary.simpleMessage("Дополнительно"), "tproxyPort": MessageLookupByLibrary.simpleMessage("Порт Tproxy"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Трафик"), "tryManualRefresh": MessageLookupByLibrary.simpleMessage( "Попробуйте обновить вручную", ), "tun": MessageLookupByLibrary.simpleMessage("Виртуальный адаптер"), "tunDesc": MessageLookupByLibrary.simpleMessage( "Перехват всего трафика устройства", ), "tunEnableRequireAdmin": MessageLookupByLibrary.simpleMessage( "Для включения виртуального адаптера требуются права администратора. Запустите программу от имени администратора.", ), "tunnel": MessageLookupByLibrary.simpleMessage("Туннель"), "tunnelAddress": MessageLookupByLibrary.simpleMessage( "Адрес прослушивания", ), "tunnelAddressHint": MessageLookupByLibrary.simpleMessage( "Например: 127.0.0.1:6553", ), "tunnelDesc": MessageLookupByLibrary.simpleMessage( "Использовать туннель перенаправления трафика", ), "tunnelList": MessageLookupByLibrary.simpleMessage( "Список перенаправлений", ), "tunnelNetwork": MessageLookupByLibrary.simpleMessage("Сетевой протокол"), "tunnelNetworkHint": MessageLookupByLibrary.simpleMessage( "Например: tcp, udp", ), "tunnelProxy": MessageLookupByLibrary.simpleMessage("Имя прокси"), "tunnelProxyHint": MessageLookupByLibrary.simpleMessage( "Например: proxy (опционально)", ), "tunnelTarget": MessageLookupByLibrary.simpleMessage("Целевой адрес"), "tunnelTargetHint": MessageLookupByLibrary.simpleMessage( "Например: 114.114.114.114:53", ), "twoColumns": MessageLookupByLibrary.simpleMessage("2 колонки"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "Невозможно обновить текущий профиль", ), "undo": MessageLookupByLibrary.simpleMessage("Отменить"), "unifiedDelay": MessageLookupByLibrary.simpleMessage( "Унифицированная задержка", ), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage( "Убрать задержку рукопожатия и разбора", ), "unknown": MessageLookupByLibrary.simpleMessage("Неизвестно"), "unnamed": MessageLookupByLibrary.simpleMessage("Без имени"), "update": MessageLookupByLibrary.simpleMessage("Обновить"), "upload": MessageLookupByLibrary.simpleMessage("Отправка"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("Получить профиль по URL"), "urlTip": m9, "useHosts": MessageLookupByLibrary.simpleMessage("Использовать hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage( "Использовать системные hosts", ), "value": MessageLookupByLibrary.simpleMessage("Значение"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("Яркий"), "view": MessageLookupByLibrary.simpleMessage("Просмотр"), "vpnDesc": MessageLookupByLibrary.simpleMessage("Настройки VPN"), "vpnEnableDesc": MessageLookupByLibrary.simpleMessage( "Автоматическая маршрутизация всего трафика через VpnService", ), "vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage( "Добавить HTTP-прокси к VPN", ), "vpnTip": MessageLookupByLibrary.simpleMessage( "Перезапустите VPN для применения изменений", ), "wakelock": MessageLookupByLibrary.simpleMessage("Блокировка сна"), "wakelockDescription": MessageLookupByLibrary.simpleMessage( "Эта функция не требует специальных разрешений, так как использует только блокировку пробуждения экрана, а не CPU. Приложение остаётся активным в фоне, экран не гаснет автоматически, что полезно в некоторых сценариях.", ), "wave": MessageLookupByLibrary.simpleMessage("Волна"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage( "Настройки WebDAV", ), "whitelist": MessageLookupByLibrary.simpleMessage("Белый список"), "whitelistMode": MessageLookupByLibrary.simpleMessage( "Режим белого списка", ), "writeToSystem": MessageLookupByLibrary.simpleMessage("Записать в систему"), "writeToSystemDesc": MessageLookupByLibrary.simpleMessage( "Требуются права администратора", ), "years": MessageLookupByLibrary.simpleMessage("лет"), "zh_CN": MessageLookupByLibrary.simpleMessage("Китайский (упрощённый)"), "zh_TC": MessageLookupByLibrary.simpleMessage("Китайский (традиционный)"), }; } ================================================ FILE: lib/l10n/intl/messages_zh_CN.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a zh_CN locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh_CN'; static String m0(label) => "确定删除选中的${label}吗?"; static String m1(label) => "确定删除当前${label}吗?"; static String m2(label) => "${label}详情"; static String m3(label) => "${label}不能为空"; static String m4(label) => "${label}当前已存在"; static String m5(label) => "暂无${label}"; static String m6(label) => "${label}必须为数字"; static String m7(label) => "${label} 必须在 1024 到 49151 之间"; static String m8(count) => "已选择 ${count} 项"; static String m9(label) => "${label}必须为URL"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("关于"), "accessControl": MessageLookupByLibrary.simpleMessage("访问控制"), "accessControlAllowDesc": MessageLookupByLibrary.simpleMessage( "只允许选中的应用进入VPN", ), "accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage( "选中的应用将被排除在VPN之外", ), "account": MessageLookupByLibrary.simpleMessage("账号"), "action": MessageLookupByLibrary.simpleMessage("操作"), "action_mode": MessageLookupByLibrary.simpleMessage("切换模式"), "action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"), "action_start": MessageLookupByLibrary.simpleMessage("启动/停止"), "action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), "action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"), "add": MessageLookupByLibrary.simpleMessage("添加"), "addProfile": MessageLookupByLibrary.simpleMessage("添加配置"), "addRule": MessageLookupByLibrary.simpleMessage("添加规则"), "addTunnel": MessageLookupByLibrary.simpleMessage("添加转发"), "addedOriginRules": MessageLookupByLibrary.simpleMessage("附加到原始规则"), "address": MessageLookupByLibrary.simpleMessage("地址"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"), "adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"), "advancedSettings": MessageLookupByLibrary.simpleMessage("进阶设置"), "ago": MessageLookupByLibrary.simpleMessage("前"), "agree": MessageLookupByLibrary.simpleMessage("同意"), "allApps": MessageLookupByLibrary.simpleMessage("所有应用"), "allowBypass": MessageLookupByLibrary.simpleMessage("允许绕过VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), "allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"), "alreadyInWhitelist": MessageLookupByLibrary.simpleMessage("当前应用已在白名单内"), "app": MessageLookupByLibrary.simpleMessage("应用"), "appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"), "appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"), "application": MessageLookupByLibrary.simpleMessage("应用程序"), "applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序设置"), "auto": MessageLookupByLibrary.simpleMessage("自动"), "autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"), "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"), "autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"), "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage( "切换节点后自动关闭连接", ), "autoLaunch": MessageLookupByLibrary.simpleMessage("开机启动"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"), "autoRun": MessageLookupByLibrary.simpleMessage("自动连接"), "autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开后自动连接"), "autoSetSystemDns": MessageLookupByLibrary.simpleMessage("自动设置系统DNS"), "autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"), "autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"), "backup": MessageLookupByLibrary.simpleMessage("备份"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"), "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage( "通过在线或本地文件同步数据", ), "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), "basicConfig": MessageLookupByLibrary.simpleMessage("内核配置"), "basicConfigDesc": MessageLookupByLibrary.simpleMessage("全局修改内核配置"), "batteryOptimization": MessageLookupByLibrary.simpleMessage("电池优化"), "batteryOptimizationDesc": MessageLookupByLibrary.simpleMessage( "请求安卓电池优化白名单权限", ), "bind": MessageLookupByLibrary.simpleMessage("绑定"), "blacklist": MessageLookupByLibrary.simpleMessage("黑名单"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), "bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"), "bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"), "bypassPrivateRoute": MessageLookupByLibrary.simpleMessage("绕过私有网络"), "bypassPrivateRouteDesc": MessageLookupByLibrary.simpleMessage( "自动绕过私有网络IP地址", ), "cacheAlgorithm": MessageLookupByLibrary.simpleMessage("缓存算法"), "cacheCorrupt": MessageLookupByLibrary.simpleMessage("缓存已损坏,是否清空?"), "cameraPermissionDenied": MessageLookupByLibrary.simpleMessage("相机权限被拒绝"), "cameraPermissionDesc": MessageLookupByLibrary.simpleMessage( "扫描二维码需要相机权限,请在设置中授予相机权限。", ), "cancel": MessageLookupByLibrary.simpleMessage("取消"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消过滤系统应用"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"), "checkError": MessageLookupByLibrary.simpleMessage("检测失败"), "checkOrAddProfile": MessageLookupByLibrary.simpleMessage("请先添加配置"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checking": MessageLookupByLibrary.simpleMessage("检测中..."), "circle": MessageLookupByLibrary.simpleMessage("圆环流转"), "clearCacheDesc": MessageLookupByLibrary.simpleMessage( "是否需要清理FakeIP&DNS缓存?", ), "clearCacheTitle": MessageLookupByLibrary.simpleMessage("清理缓存"), "clearData": MessageLookupByLibrary.simpleMessage("清除数据"), "clipboard": MessageLookupByLibrary.simpleMessage("剪切板"), "clipboardDesc": MessageLookupByLibrary.simpleMessage("自动获取剪切板订阅链接"), "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), "color": MessageLookupByLibrary.simpleMessage("颜色"), "colorSchemes": MessageLookupByLibrary.simpleMessage("配色方案"), "columns": MessageLookupByLibrary.simpleMessage("列数"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( "开启将失去部分应用能力,获得全量的Clash的支持", ), "concurrencyLimit": MessageLookupByLibrary.simpleMessage("并发限制"), "concurrencyLimitDesc": MessageLookupByLibrary.simpleMessage("延迟测试的最大并发数量"), "confirm": MessageLookupByLibrary.simpleMessage("确定"), "connection": MessageLookupByLibrary.simpleMessage("活跃连接"), "connections": MessageLookupByLibrary.simpleMessage("连接"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), "contactMe": MessageLookupByLibrary.simpleMessage("联系我"), "content": MessageLookupByLibrary.simpleMessage("内容"), "contentScheme": MessageLookupByLibrary.simpleMessage("内容主题"), "controlSecret": MessageLookupByLibrary.simpleMessage("控制密码"), "controlSecretDesc": MessageLookupByLibrary.simpleMessage( "RESTful API的访问密码", ), "copy": MessageLookupByLibrary.simpleMessage("复制"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"), "copyLink": MessageLookupByLibrary.simpleMessage("复制链接"), "copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"), "core": MessageLookupByLibrary.simpleMessage("内核"), "coreConnected": MessageLookupByLibrary.simpleMessage("已连接"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), "coreSuspended": MessageLookupByLibrary.simpleMessage("已挂起"), "country": MessageLookupByLibrary.simpleMessage("区域"), "crashTest": MessageLookupByLibrary.simpleMessage("崩溃测试"), "create": MessageLookupByLibrary.simpleMessage("创建"), "creationTime": MessageLookupByLibrary.simpleMessage("创建时间"), "custom": MessageLookupByLibrary.simpleMessage("自定义"), "customUrl": MessageLookupByLibrary.simpleMessage("自定义URL"), "cut": MessageLookupByLibrary.simpleMessage("剪切"), "dark": MessageLookupByLibrary.simpleMessage("深色"), "dashboard": MessageLookupByLibrary.simpleMessage("首页"), "days": MessageLookupByLibrary.simpleMessage("天"), "defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"), "defaultNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"), "defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"), "delay": MessageLookupByLibrary.simpleMessage("延迟"), "delayAnimation": MessageLookupByLibrary.simpleMessage("延迟动画"), "delayAnimationDesc": MessageLookupByLibrary.simpleMessage("自定义测试过程中的动画显示"), "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delete": MessageLookupByLibrary.simpleMessage("删除"), "deleteMultipTip": m0, "deleteTip": m1, "deleteTunnel": MessageLookupByLibrary.simpleMessage("删除转发"), "desc": MessageLookupByLibrary.simpleMessage( "Bettbox基于强大灵活的Mihomo(Clash.Meta)代理内核,致力于更好的体验,Forked form FlClash,Better Experience, Out of the box", ), "destination": MessageLookupByLibrary.simpleMessage("目标地址"), "destinationGeoIP": MessageLookupByLibrary.simpleMessage("目标地理定位"), "destinationIPASN": MessageLookupByLibrary.simpleMessage("目标IP ASN"), "details": m2, "detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api,仅供参考"), "developerMode": MessageLookupByLibrary.simpleMessage("开发者模式"), "developerModeEnableTip": MessageLookupByLibrary.simpleMessage("开发者模式已启用。"), "dialerIp4pConvert": MessageLookupByLibrary.simpleMessage("启用拨号IP4P地址转换"), "dialerIp4pConvertDesc": MessageLookupByLibrary.simpleMessage( "启用拨号器的 IP4P 地址转换功能", ), "direct": MessageLookupByLibrary.simpleMessage("直连"), "directNameserver": MessageLookupByLibrary.simpleMessage("直连域名服务器"), "directNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析直连出口的域名"), "directNameserverFollowPolicy": MessageLookupByLibrary.simpleMessage( "直连DNS遵循规则", ), "disableQuic": MessageLookupByLibrary.simpleMessage("禁用QUIC"), "disableQuicDesc": MessageLookupByLibrary.simpleMessage("禁用QUIC以解决特定网络问题"), "disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage( "本软件为开源免费软件,仅供学习交流等非商业性质的个人测试使用,代理服务商的行为均与本软件无关,同意声明代表您已完全知晓并确认了这一点,如不同意,请选择退出!", ), "discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"), "discovery": MessageLookupByLibrary.simpleMessage("发现新版本"), "dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"), "dnsHijack": MessageLookupByLibrary.simpleMessage("DNS劫持"), "dnsHijackDesc": MessageLookupByLibrary.simpleMessage("将解析导入内部DNS模块"), "dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"), "domain": MessageLookupByLibrary.simpleMessage("域名"), "doubleBounce": MessageLookupByLibrary.simpleMessage("双重弹奏"), "download": MessageLookupByLibrary.simpleMessage("下载"), "dozeSuspend": MessageLookupByLibrary.simpleMessage("休眠支持"), "dozeSuspendDesc": MessageLookupByLibrary.simpleMessage("开启后同步系统Doze休眠模式"), "edit": MessageLookupByLibrary.simpleMessage("编辑"), "editTunnel": MessageLookupByLibrary.simpleMessage("编辑转发"), "emptyTip": m3, "en": MessageLookupByLibrary.simpleMessage("英语"), "enableCrashReport": MessageLookupByLibrary.simpleMessage("应用崩溃分析"), "enableCrashReportDesc": MessageLookupByLibrary.simpleMessage( "必要时上传应用崩溃日志", ), "enableOverride": MessageLookupByLibrary.simpleMessage("启用覆写"), "endpointIndependentNat": MessageLookupByLibrary.simpleMessage("NAT增强"), "endpointIndependentNatDesc": MessageLookupByLibrary.simpleMessage( "启用独立于端点的NAT", ), "entries": MessageLookupByLibrary.simpleMessage("个条目"), "exclude": MessageLookupByLibrary.simpleMessage("后台隐藏"), "excludeChina": MessageLookupByLibrary.simpleMessage("排除国内"), "excludeChinaDesc": MessageLookupByLibrary.simpleMessage( "放行中国QUIC流量而非全部禁用", ), "excludeDesc": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏应用"), "existsTip": m4, "exit": MessageLookupByLibrary.simpleMessage("退出"), "expand": MessageLookupByLibrary.simpleMessage("标准"), "experimental": MessageLookupByLibrary.simpleMessage("Experimental"), "experimentalDesc": MessageLookupByLibrary.simpleMessage("实验性配置请谨慎使用"), "expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"), "exportFile": MessageLookupByLibrary.simpleMessage("导出文件"), "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"), "expressiveScheme": MessageLookupByLibrary.simpleMessage("表现力"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制"), "externalControllerDesc": MessageLookupByLibrary.simpleMessage( "通过在线端口控制内核", ), "externalLink": MessageLookupByLibrary.simpleMessage("外部链接"), "externalResources": MessageLookupByLibrary.simpleMessage("外部资源"), "fadingCircle": MessageLookupByLibrary.simpleMessage("环影隐渐"), "fadingFour": MessageLookupByLibrary.simpleMessage("四方烁动"), "fakeIpFilterMode": MessageLookupByLibrary.simpleMessage("FakeIP过滤模式"), "fakeIpFilterModeDesc": MessageLookupByLibrary.simpleMessage( "指定FakeIP过滤模式", ), "fakeipFilter": MessageLookupByLibrary.simpleMessage("FakeIP过滤列表"), "fakeipRange": MessageLookupByLibrary.simpleMessage("FakeIP范围"), "fakeipRangeV6": MessageLookupByLibrary.simpleMessage("FakeIPv6范围"), "fakeipTtl": MessageLookupByLibrary.simpleMessage("FakeIP有效时间"), "fallback": MessageLookupByLibrary.simpleMessage("Fallback"), "fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"), "fcmOptimization": MessageLookupByLibrary.simpleMessage("FCM优化"), "fcmOptimizationDesc": MessageLookupByLibrary.simpleMessage( "增强FCM直连时的网络稳定性", ), "fcmTip": MessageLookupByLibrary.simpleMessage( "FCM连接和支持取决于设备本身,显示结果仅供参考。因系统权限原因,您需要关闭网络中的\"允许绕过VPN\"选项,以获得更加准确的结果", ), "fidelityScheme": MessageLookupByLibrary.simpleMessage("高保真"), "file": MessageLookupByLibrary.simpleMessage("文件"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"), "filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"), "findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后会将可以查找进程"), "fontFamily": MessageLookupByLibrary.simpleMessage("字体"), "forceDnsMapping": MessageLookupByLibrary.simpleMessage("强制 DNS 映射"), "forceDnsMappingDesc": MessageLookupByLibrary.simpleMessage( "强制将 DNS 查询结果映射到连接", ), "forceDomain": MessageLookupByLibrary.simpleMessage("强制嗅探域名"), "forceGCDesc": MessageLookupByLibrary.simpleMessage( "是否进行强制内核垃圾回收?实验性功能,请谨慎使用", ), "forceGCTitle": MessageLookupByLibrary.simpleMessage("强制垃圾回收"), "formatError": MessageLookupByLibrary.simpleMessage("请检查格式是否正确"), "fourColumns": MessageLookupByLibrary.simpleMessage("四列"), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("果缤纷"), "general": MessageLookupByLibrary.simpleMessage("常规"), "generalDesc": MessageLookupByLibrary.simpleMessage("修改通用设置"), "generateSecret": MessageLookupByLibrary.simpleMessage("生成"), "geoData": MessageLookupByLibrary.simpleMessage("地理数据"), "geodataLoader": MessageLookupByLibrary.simpleMessage("GEO节能"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启后使用GEO低内存加载器"), "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"), "getOriginRules": MessageLookupByLibrary.simpleMessage("获取原始规则"), "global": MessageLookupByLibrary.simpleMessage("全局"), "go": MessageLookupByLibrary.simpleMessage("前往"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), "harmonyFont": MessageLookupByLibrary.simpleMessage("鸿蒙字体"), "harmonyFontDesc": MessageLookupByLibrary.simpleMessage( "使用优化的HarmonyOS Sans", ), "hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"), "healthCheckTimeout": MessageLookupByLibrary.simpleMessage("超时时间"), "healthCheckTimeoutDesc": MessageLookupByLibrary.simpleMessage( "节点健康检查超时时间", ), "highRefreshRate": MessageLookupByLibrary.simpleMessage("高刷新率"), "highRefreshRateDesc": MessageLookupByLibrary.simpleMessage("启用设备最高刷新率支持"), "host": MessageLookupByLibrary.simpleMessage("主机"), "hostsDesc": MessageLookupByLibrary.simpleMessage("追加当前配置Hosts"), "hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"), "hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"), "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"), "hours": MessageLookupByLibrary.simpleMessage("小时"), "httpPortSniffer": MessageLookupByLibrary.simpleMessage("HTTP 端口嗅探"), "icmpForwarding": MessageLookupByLibrary.simpleMessage("ICMP转发"), "icmpForwardingDesc": MessageLookupByLibrary.simpleMessage( "开启后将支持ICMP Ping", ), "icon": MessageLookupByLibrary.simpleMessage("图片"), "iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"), "iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"), "import": MessageLookupByLibrary.simpleMessage("导入"), "importFailed": MessageLookupByLibrary.simpleMessage("导入失败"), "importFile": MessageLookupByLibrary.simpleMessage("通过文件导入"), "importFromCode": MessageLookupByLibrary.simpleMessage("通过代码导入"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "importUrl": MessageLookupByLibrary.simpleMessage("通过URL导入"), "infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"), "init": MessageLookupByLibrary.simpleMessage("初始化"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"), "internet": MessageLookupByLibrary.simpleMessage("互联网"), "interval": MessageLookupByLibrary.simpleMessage("间隔"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "invalidIpFormat": MessageLookupByLibrary.simpleMessage("无效的IP或CIDR格式"), "ipClickBehavior": MessageLookupByLibrary.simpleMessage("显示切换"), "ipPrivacyProtection": MessageLookupByLibrary.simpleMessage("隐藏IP显示"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"), "ja": MessageLookupByLibrary.simpleMessage("日语"), "just": MessageLookupByLibrary.simpleMessage("刚刚"), "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), "key": MessageLookupByLibrary.simpleMessage("键"), "language": MessageLookupByLibrary.simpleMessage("语言"), "layout": MessageLookupByLibrary.simpleMessage("布局"), "light": MessageLookupByLibrary.simpleMessage("浅色"), "lightIcon": MessageLookupByLibrary.simpleMessage("丹青留白"), "lightIconDesc": MessageLookupByLibrary.simpleMessage("手动切换浅色系桌面应用图标"), "list": MessageLookupByLibrary.simpleMessage("列表"), "listen": MessageLookupByLibrary.simpleMessage("监听"), "local": MessageLookupByLibrary.simpleMessage("本地"), "localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"), "localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"), "log": MessageLookupByLibrary.simpleMessage("日志"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "logcatDesc": MessageLookupByLibrary.simpleMessage("开启后将会显示日志入口"), "logs": MessageLookupByLibrary.simpleMessage("日志"), "logsDesc": MessageLookupByLibrary.simpleMessage("查看日志捕获记录"), "logsTest": MessageLookupByLibrary.simpleMessage("日志测试"), "loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"), "loose": MessageLookupByLibrary.simpleMessage("宽松"), "manualRefreshIp": MessageLookupByLibrary.simpleMessage("重新获取IP"), "memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"), "messageTest": MessageLookupByLibrary.simpleMessage("消息测试"), "messageTestTip": MessageLookupByLibrary.simpleMessage("这是一条消息。"), "min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出最小化"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), "minutes": MessageLookupByLibrary.simpleMessage("分钟"), "mixedPort": MessageLookupByLibrary.simpleMessage("混合端口"), "mode": MessageLookupByLibrary.simpleMessage("模式"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("单色"), "months": MessageLookupByLibrary.simpleMessage("月"), "more": MessageLookupByLibrary.simpleMessage("查看"), "name": MessageLookupByLibrary.simpleMessage("名称"), "nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"), "nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"), "nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"), "nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"), "nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"), "navBarHapticFeedback": MessageLookupByLibrary.simpleMessage("触感反馈"), "navBarHapticFeedbackDesc": MessageLookupByLibrary.simpleMessage( "底部导航栏切换震动反馈", ), "network": MessageLookupByLibrary.simpleMessage("网络"), "networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"), "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), "networkFix": MessageLookupByLibrary.simpleMessage("网络修复"), "networkFixDesc": MessageLookupByLibrary.simpleMessage( "修复Windows网络检测地球图标问题", ), "networkMatch": MessageLookupByLibrary.simpleMessage("网络匹配"), "networkMatchHint": MessageLookupByLibrary.simpleMessage( "输入IP或CIDR,最多2个,以逗号分隔", ), "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), "networkType": MessageLookupByLibrary.simpleMessage("网络类型"), "neutralScheme": MessageLookupByLibrary.simpleMessage("中性"), "noAnimation": MessageLookupByLibrary.simpleMessage("默认"), "noData": MessageLookupByLibrary.simpleMessage("暂无数据"), "noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"), "noIcon": MessageLookupByLibrary.simpleMessage("无图标"), "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"), "noNetwork": MessageLookupByLibrary.simpleMessage("无网络"), "noNetworkApp": MessageLookupByLibrary.simpleMessage("无网络应用"), "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), "noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置或者添加有效配置文件"), "noResolve": MessageLookupByLibrary.simpleMessage("不解析IP"), "noStatusAvailable": MessageLookupByLibrary.simpleMessage("未获取到状态"), "nodeExclusion": MessageLookupByLibrary.simpleMessage("节点排除"), "nodeExclusionDesc": MessageLookupByLibrary.simpleMessage("排除所有匹配到的节点"), "nodeExclusionPlaceholder": MessageLookupByLibrary.simpleMessage( "HK|香港|🇭🇰", ), "none": MessageLookupByLibrary.simpleMessage("无"), "notRecommended": MessageLookupByLibrary.simpleMessage("不推荐"), "notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"), "ntp": MessageLookupByLibrary.simpleMessage("NTP"), "ntpDesc": MessageLookupByLibrary.simpleMessage("使用NTP时间服务"), "ntpInterval": MessageLookupByLibrary.simpleMessage("更新时间"), "ntpPort": MessageLookupByLibrary.simpleMessage("端口"), "ntpServer": MessageLookupByLibrary.simpleMessage("服务器"), "ntpStatus": MessageLookupByLibrary.simpleMessage("状态"), "ntpStatusDesc": MessageLookupByLibrary.simpleMessage("开启NTP时间服务"), "nullProfileDesc": MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), "nullTip": m5, "numberTip": m6, "oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "onlinePanel": MessageLookupByLibrary.simpleMessage("在线面板"), "onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"), "onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("代理流量统计"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( "开启后将只统计代理流量", ), "openDashboard": MessageLookupByLibrary.simpleMessage("打开 Zashboard"), "openSettings": MessageLookupByLibrary.simpleMessage("打开设置"), "options": MessageLookupByLibrary.simpleMessage("选项"), "other": MessageLookupByLibrary.simpleMessage("其他"), "otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"), "otherSettings": MessageLookupByLibrary.simpleMessage("增强工具"), "otherSettingsDesc": MessageLookupByLibrary.simpleMessage("修改增强工具设置"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "override": MessageLookupByLibrary.simpleMessage("覆写"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), "overrideDestination": MessageLookupByLibrary.simpleMessage("覆盖目标地址"), "overrideDestinationDesc": MessageLookupByLibrary.simpleMessage( "使用嗅探结果覆盖连接目标地址", ), "overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"), "overrideExperimental": MessageLookupByLibrary.simpleMessage( "覆写Experimental", ), "overrideExperimentalDesc": MessageLookupByLibrary.simpleMessage( "开启后将覆盖配置中的实验性配置", ), "overrideInvalidTip": MessageLookupByLibrary.simpleMessage("在脚本模式下不生效"), "overrideNtp": MessageLookupByLibrary.simpleMessage("覆写NTP"), "overrideNtpDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的NTP选项"), "overrideOriginRules": MessageLookupByLibrary.simpleMessage("覆盖原始规则"), "overrideSniffer": MessageLookupByLibrary.simpleMessage("覆写Sniffer"), "overrideSnifferDesc": MessageLookupByLibrary.simpleMessage( "开启后将覆盖配置中的Sniffer选项", ), "overrideTestUrl": MessageLookupByLibrary.simpleMessage("覆盖配置"), "overrideTunnel": MessageLookupByLibrary.simpleMessage("覆写Tunnel"), "overrideTunnelDesc": MessageLookupByLibrary.simpleMessage( "开启后将覆盖配置中的Tunnel选项", ), "packageListPermissionDenied": MessageLookupByLibrary.simpleMessage( "权限被拒绝。没有权限无法访问应用列表。", ), "packageListPermissionRequired": MessageLookupByLibrary.simpleMessage( "此功能需要访问已安装应用列表的权限。是否授予此权限?", ), "palette": MessageLookupByLibrary.simpleMessage("调色板"), "parsePureIp": MessageLookupByLibrary.simpleMessage("解析纯 IP 连接"), "parsePureIpDesc": MessageLookupByLibrary.simpleMessage("解析纯 IP 连接"), "password": MessageLookupByLibrary.simpleMessage("密码"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"), "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"), "pleaseCloseSystemProxyFirst": MessageLookupByLibrary.simpleMessage( "请先关闭系统代理", ), "pleaseCloseTunFirst": MessageLookupByLibrary.simpleMessage("请先关闭虚拟网卡"), "pleaseEnterScriptName": MessageLookupByLibrary.simpleMessage("请输入脚本名称"), "pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage( "请输入管理员密码", ), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "请上传有效的二维码", ), "port": MessageLookupByLibrary.simpleMessage("端口"), "portConflictTip": MessageLookupByLibrary.simpleMessage("请输入不同的端口"), "portTip": m7, "powerSwitch": MessageLookupByLibrary.simpleMessage("启动开关"), "preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"), "pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"), "preview": MessageLookupByLibrary.simpleMessage("预览"), "profile": MessageLookupByLibrary.simpleMessage("配置"), "profileAutoUpdateIntervalInvalidValidationDesc": MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"), "profileAutoUpdateIntervalNullValidationDesc": MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"), "profileHasUpdate": MessageLookupByLibrary.simpleMessage( "配置文件已经修改,是否关闭自动更新 ", ), "profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage( "请输入配置名称", ), "profileParseErrorDesc": MessageLookupByLibrary.simpleMessage("配置文件解析错误"), "profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage( "请输入有效配置URL", ), "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage( "请输入配置URL", ), "profiles": MessageLookupByLibrary.simpleMessage("配置"), "profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"), "progress": MessageLookupByLibrary.simpleMessage("进程"), "project": MessageLookupByLibrary.simpleMessage("项目"), "providers": MessageLookupByLibrary.simpleMessage("提供者"), "proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"), "proxyChains": MessageLookupByLibrary.simpleMessage("代理链"), "proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"), "proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"), "proxyNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"), "pulse": MessageLookupByLibrary.simpleMessage("脉冲律动"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "quicGoDisableEcn": MessageLookupByLibrary.simpleMessage("禁用QUIC显式拥塞通知"), "quicGoDisableEcnDesc": MessageLookupByLibrary.simpleMessage( "禁用 QUIC 的显式拥塞通知功能", ), "quicGoDisableGso": MessageLookupByLibrary.simpleMessage("禁用QUIC通用分段卸载"), "quicGoDisableGsoDesc": MessageLookupByLibrary.simpleMessage( "禁用 QUIC 的通用分段卸载功能", ), "quicPortSniffer": MessageLookupByLibrary.simpleMessage("QUIC 端口嗅探"), "quickResponse": MessageLookupByLibrary.simpleMessage("快速响应"), "quickResponseDesc": MessageLookupByLibrary.simpleMessage("网络发生变化时主动断开连接"), "rainbowScheme": MessageLookupByLibrary.simpleMessage("彩虹"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), "recoveryStrategy": MessageLookupByLibrary.simpleMessage("恢复策略"), "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage("兼容"), "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage("覆盖"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), "redirPort": MessageLookupByLibrary.simpleMessage("Redir端口"), "redo": MessageLookupByLibrary.simpleMessage("重做"), "refreshAppList": MessageLookupByLibrary.simpleMessage("刷新应用列表"), "refreshAppListConfirm": MessageLookupByLibrary.simpleMessage("是否刷新应用列表?"), "regExp": MessageLookupByLibrary.simpleMessage("正则"), "remote": MessageLookupByLibrary.simpleMessage("远程"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), "remoteDestination": MessageLookupByLibrary.simpleMessage("远程目标"), "remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"), "remove": MessageLookupByLibrary.simpleMessage("移除"), "rename": MessageLookupByLibrary.simpleMessage("重命名"), "request": MessageLookupByLibrary.simpleMessage("请求"), "requests": MessageLookupByLibrary.simpleMessage("请求"), "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"), "reset": MessageLookupByLibrary.simpleMessage("重置"), "resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"), "resources": MessageLookupByLibrary.simpleMessage("资源"), "resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"), "respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"), "respectRulesDesc": MessageLookupByLibrary.simpleMessage("DNS连接跟随Rules"), "restart": MessageLookupByLibrary.simpleMessage("重启"), "restartCoreDesc": MessageLookupByLibrary.simpleMessage("是否手动重启内核?"), "restartCoreTitle": MessageLookupByLibrary.simpleMessage("重启内核"), "restartTip": MessageLookupByLibrary.simpleMessage("重启TUN后改变生效"), "retry": MessageLookupByLibrary.simpleMessage("重试"), "rotatingCircle": MessageLookupByLibrary.simpleMessage("单圆自转"), "ru": MessageLookupByLibrary.simpleMessage("俄语"), "rule": MessageLookupByLibrary.simpleMessage("规则"), "ruleName": MessageLookupByLibrary.simpleMessage("规则名称"), "ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"), "ruleTarget": MessageLookupByLibrary.simpleMessage("规则目标"), "runTime": MessageLookupByLibrary.simpleMessage("启动时间"), "runtimeConfig": MessageLookupByLibrary.simpleMessage("运行时配置"), "save": MessageLookupByLibrary.simpleMessage("保存"), "saveChanges": MessageLookupByLibrary.simpleMessage("是否保存更改?"), "saveTip": MessageLookupByLibrary.simpleMessage("确定要保存吗?"), "script": MessageLookupByLibrary.simpleMessage("脚本"), "scriptDesc": MessageLookupByLibrary.simpleMessage("配置全局覆写脚本"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), "secretCopied": MessageLookupByLibrary.simpleMessage("密码已复制到剪贴板"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selected": MessageLookupByLibrary.simpleMessage("已选择"), "selectedCountTitle": m8, "serviceReady": MessageLookupByLibrary.simpleMessage("服务已就绪"), "serviceRunning": MessageLookupByLibrary.simpleMessage("服务正在运行中"), "settings": MessageLookupByLibrary.simpleMessage("设置"), "show": MessageLookupByLibrary.simpleMessage("显示"), "shrink": MessageLookupByLibrary.simpleMessage("紧凑"), "silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("不打开软件直接在后台启动"), "size": MessageLookupByLibrary.simpleMessage("尺寸"), "skipDomain": MessageLookupByLibrary.simpleMessage("跳过域名"), "skipDstAddress": MessageLookupByLibrary.simpleMessage("跳过目标 IP"), "skipSrcAddress": MessageLookupByLibrary.simpleMessage("跳过来源 IP"), "smartAutoStop": MessageLookupByLibrary.simpleMessage("智能启停"), "smartAutoStopDesc": MessageLookupByLibrary.simpleMessage("连接指定网络后停止代理服务"), "smartAutoStopServiceRunning": MessageLookupByLibrary.simpleMessage( "智能启停服务运行中", ), "smartDelayLaunch": MessageLookupByLibrary.simpleMessage("智能延迟"), "smartDelayLaunchDesc": MessageLookupByLibrary.simpleMessage("在网络成功连接以后启动"), "sniffer": MessageLookupByLibrary.simpleMessage("Sniffer"), "snifferAddressHint": MessageLookupByLibrary.simpleMessage("每行一个地址"), "snifferDesc": MessageLookupByLibrary.simpleMessage("修改域名嗅探配置"), "snifferDomainHint": MessageLookupByLibrary.simpleMessage("每行一个域名"), "snifferPorts": MessageLookupByLibrary.simpleMessage("端口"), "snifferPortsHint": MessageLookupByLibrary.simpleMessage( "例如: 80, 8080-8880", ), "snifferStatus": MessageLookupByLibrary.simpleMessage("状态"), "snifferStatusDesc": MessageLookupByLibrary.simpleMessage("开启嗅探服务设置"), "socksPort": MessageLookupByLibrary.simpleMessage("Socks端口"), "sort": MessageLookupByLibrary.simpleMessage("排序"), "source": MessageLookupByLibrary.simpleMessage("来源"), "sourceIp": MessageLookupByLibrary.simpleMessage("源IP"), "specialProxy": MessageLookupByLibrary.simpleMessage("特殊代理"), "specialRules": MessageLookupByLibrary.simpleMessage("特殊规则"), "spinningLines": MessageLookupByLibrary.simpleMessage("流光旋绕"), "stackMode": MessageLookupByLibrary.simpleMessage("栈模式"), "standard": MessageLookupByLibrary.simpleMessage("标准"), "start": MessageLookupByLibrary.simpleMessage("启动"), "startTest": MessageLookupByLibrary.simpleMessage("延迟测试"), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动"), "status": MessageLookupByLibrary.simpleMessage("状态"), "statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"), "stop": MessageLookupByLibrary.simpleMessage("停止"), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止"), "storeFix": MessageLookupByLibrary.simpleMessage("商店修复"), "storeFixDesc": MessageLookupByLibrary.simpleMessage("修复Google Play商店下载异常"), "strictRoute": MessageLookupByLibrary.simpleMessage("严格路由"), "strictRouteDesc": MessageLookupByLibrary.simpleMessage("使用TUN严格路由模式"), "style": MessageLookupByLibrary.simpleMessage("风格"), "subRule": MessageLookupByLibrary.simpleMessage("子规则"), "submit": MessageLookupByLibrary.simpleMessage("提交"), "success": MessageLookupByLibrary.simpleMessage("Success"), "switchLabel": MessageLookupByLibrary.simpleMessage("开关"), "switchToDomesticIp": MessageLookupByLibrary.simpleMessage("获取国内IP"), "sync": MessageLookupByLibrary.simpleMessage("同步"), "syncAll": MessageLookupByLibrary.simpleMessage("全部同步"), "syncFailed": MessageLookupByLibrary.simpleMessage("同步失败"), "system": MessageLookupByLibrary.simpleMessage("系统"), "systemApp": MessageLookupByLibrary.simpleMessage("系统应用"), "systemFont": MessageLookupByLibrary.simpleMessage("系统字体"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"), "tab": MessageLookupByLibrary.simpleMessage("标签页"), "tabAnimation": MessageLookupByLibrary.simpleMessage("切换动画"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage("仅在部分移动视图中有效"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发连接"), "testUrl": MessageLookupByLibrary.simpleMessage("测试链接"), "textScale": MessageLookupByLibrary.simpleMessage("文本缩放"), "theme": MessageLookupByLibrary.simpleMessage("主题"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置主题色彩及图标"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "threeBounce": MessageLookupByLibrary.simpleMessage("灵珠跃动"), "threeColumns": MessageLookupByLibrary.simpleMessage("三列"), "threeInOut": MessageLookupByLibrary.simpleMessage("三星舒合"), "tight": MessageLookupByLibrary.simpleMessage("紧凑"), "time": MessageLookupByLibrary.simpleMessage("时间"), "tip": MessageLookupByLibrary.simpleMessage("提示"), "tlsPortSniffer": MessageLookupByLibrary.simpleMessage("TLS 端口嗅探"), "toggle": MessageLookupByLibrary.simpleMessage("切换"), "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("调性点缀"), "tooManyRules": MessageLookupByLibrary.simpleMessage("最多允许2个规则"), "tools": MessageLookupByLibrary.simpleMessage("更多"), "tproxyPort": MessageLookupByLibrary.simpleMessage("Tproxy端口"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "tryManualRefresh": MessageLookupByLibrary.simpleMessage("请尝试手动刷新"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), "tunDesc": MessageLookupByLibrary.simpleMessage("接管当前设备全局流量"), "tunEnableRequireAdmin": MessageLookupByLibrary.simpleMessage( "启用虚拟网卡需要管理员权限,请以管理员身份运行程序", ), "tunnel": MessageLookupByLibrary.simpleMessage("Tunnel"), "tunnelAddress": MessageLookupByLibrary.simpleMessage("监听地址"), "tunnelAddressHint": MessageLookupByLibrary.simpleMessage( "例如: 127.0.0.1:6553", ), "tunnelDesc": MessageLookupByLibrary.simpleMessage("使用流量转发隧道"), "tunnelList": MessageLookupByLibrary.simpleMessage("转发列表"), "tunnelNetwork": MessageLookupByLibrary.simpleMessage("网络协议"), "tunnelNetworkHint": MessageLookupByLibrary.simpleMessage("例如: tcp, udp"), "tunnelProxy": MessageLookupByLibrary.simpleMessage("代理名称"), "tunnelProxyHint": MessageLookupByLibrary.simpleMessage("例如: proxy (可选)"), "tunnelTarget": MessageLookupByLibrary.simpleMessage("目标地址"), "tunnelTargetHint": MessageLookupByLibrary.simpleMessage( "例如: 114.114.114.114:53", ), "twoColumns": MessageLookupByLibrary.simpleMessage("两列"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "无法更新当前配置文件", ), "undo": MessageLookupByLibrary.simpleMessage("撤销"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手解析等额外延迟"), "unknown": MessageLookupByLibrary.simpleMessage("未知"), "unnamed": MessageLookupByLibrary.simpleMessage("未命名"), "update": MessageLookupByLibrary.simpleMessage("更新"), "upload": MessageLookupByLibrary.simpleMessage("上传"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"), "urlTip": m9, "useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), "value": MessageLookupByLibrary.simpleMessage("值"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("活力"), "view": MessageLookupByLibrary.simpleMessage("查看"), "vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"), "vpnEnableDesc": MessageLookupByLibrary.simpleMessage( "通过VpnService自动路由系统所有流量", ), "vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage( "为VpnService附加HTTP代理", ), "vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"), "wakelock": MessageLookupByLibrary.simpleMessage("亮屏锁"), "wakelockDescription": MessageLookupByLibrary.simpleMessage( "本功能不需要任何特殊权限,因为它仅启用屏幕唤醒锁,而不是任何CPU唤醒锁,应用会在后台保持必要的活跃,且屏幕不会自动熄灭,这在一些场景下会很有用", ), "wave": MessageLookupByLibrary.simpleMessage("波浪起伏"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), "whitelist": MessageLookupByLibrary.simpleMessage("白名单"), "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), "writeToSystem": MessageLookupByLibrary.simpleMessage("写入系统"), "writeToSystemDesc": MessageLookupByLibrary.simpleMessage("需要管理员权限"), "years": MessageLookupByLibrary.simpleMessage("年"), "zh_CN": MessageLookupByLibrary.simpleMessage("简体中文"), "zh_TC": MessageLookupByLibrary.simpleMessage("繁体中文"), }; } ================================================ FILE: lib/l10n/intl/messages_zh_TC.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a zh_TC locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh_TC'; static String m0(label) => "確定刪除選取的 ${label} 嗎?"; static String m1(label) => "確定刪除目前的 ${label} 嗎?"; static String m2(label) => "${label}詳情"; static String m3(label) => "${label}不能為空"; static String m4(label) => "${label}目前已存在"; static String m5(label) => "暫無 ${label}"; static String m6(label) => "${label}必須為數字"; static String m7(label) => "${label} 必須在 1024 到 49151 之間"; static String m8(count) => "已選擇 ${count} 項"; static String m9(label) => "${label}必須為 URL"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("關於"), "accessControl": MessageLookupByLibrary.simpleMessage("存取控制"), "accessControlAllowDesc": MessageLookupByLibrary.simpleMessage( "只允許選取的應用程式進入 VPN", ), "accessControlDesc": MessageLookupByLibrary.simpleMessage("設定應用存取代理"), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage( "選取的應用程式將被排除在VPN之外", ), "account": MessageLookupByLibrary.simpleMessage("帳號"), "action": MessageLookupByLibrary.simpleMessage("操作"), "action_mode": MessageLookupByLibrary.simpleMessage("切換模式"), "action_proxy": MessageLookupByLibrary.simpleMessage("系統代理"), "action_start": MessageLookupByLibrary.simpleMessage("啟動 / 停止"), "action_tun": MessageLookupByLibrary.simpleMessage("虛擬網卡"), "action_view": MessageLookupByLibrary.simpleMessage("顯示 / 隱藏"), "add": MessageLookupByLibrary.simpleMessage("新增"), "addProfile": MessageLookupByLibrary.simpleMessage("新增配置"), "addRule": MessageLookupByLibrary.simpleMessage("新增規則"), "addTunnel": MessageLookupByLibrary.simpleMessage("新增轉發"), "addedOriginRules": MessageLookupByLibrary.simpleMessage("附加到原始規則"), "address": MessageLookupByLibrary.simpleMessage("地址"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV 伺服器地址"), "addressTip": MessageLookupByLibrary.simpleMessage("請輸入有效的 WebDAV 地址"), "adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理員自啟動"), "adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage( "使用管理員模式開機自動啟動", ), "advancedSettings": MessageLookupByLibrary.simpleMessage("進階設定"), "ago": MessageLookupByLibrary.simpleMessage("前"), "agree": MessageLookupByLibrary.simpleMessage("同意"), "allApps": MessageLookupByLibrary.simpleMessage("所有應用程式"), "allowBypass": MessageLookupByLibrary.simpleMessage("允許繞過 VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage("開啟後部分應用程式可繞過 VPN"), "allowLan": MessageLookupByLibrary.simpleMessage("區域網路代理"), "allowLanDesc": MessageLookupByLibrary.simpleMessage("允許透過區域網路存取代理"), "alreadyInWhitelist": MessageLookupByLibrary.simpleMessage("目前應用程式已在白名單內"), "app": MessageLookupByLibrary.simpleMessage("應用"), "appAccessControl": MessageLookupByLibrary.simpleMessage("應用存取控制"), "appDesc": MessageLookupByLibrary.simpleMessage("處理應用相關設定"), "application": MessageLookupByLibrary.simpleMessage("應用程式"), "applicationDesc": MessageLookupByLibrary.simpleMessage("修改應用程式設定"), "auto": MessageLookupByLibrary.simpleMessage("自動"), "autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自動檢查更新"), "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage("應用啟動時自動檢查更新"), "autoCloseConnections": MessageLookupByLibrary.simpleMessage("自動關閉連線"), "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage( "切換節點後自動關閉連線", ), "autoLaunch": MessageLookupByLibrary.simpleMessage("開機啟動"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟隨系統自動啟動"), "autoRun": MessageLookupByLibrary.simpleMessage("自動連線"), "autoRunDesc": MessageLookupByLibrary.simpleMessage("應用打開後自動連線"), "autoSetSystemDns": MessageLookupByLibrary.simpleMessage("自動設定系統 DNS"), "autoUpdate": MessageLookupByLibrary.simpleMessage("自動更新"), "autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自動更新間隔(分鐘)"), "backup": MessageLookupByLibrary.simpleMessage("備份"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("備份與還原"), "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage( "透過線上或本機檔案同步資料", ), "backupSuccess": MessageLookupByLibrary.simpleMessage("備份成功"), "basicConfig": MessageLookupByLibrary.simpleMessage("內核配置"), "basicConfigDesc": MessageLookupByLibrary.simpleMessage("全域修改內核配置"), "batteryOptimization": MessageLookupByLibrary.simpleMessage("電池最佳化"), "batteryOptimizationDesc": MessageLookupByLibrary.simpleMessage( "請求 Android 電池最佳化白名單權限", ), "bind": MessageLookupByLibrary.simpleMessage("綁定"), "blacklist": MessageLookupByLibrary.simpleMessage("黑名單"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名單模式"), "bypassDomain": MessageLookupByLibrary.simpleMessage("排除網域"), "bypassDomainDesc": MessageLookupByLibrary.simpleMessage("僅在系統代理啟用時生效"), "bypassPrivateRoute": MessageLookupByLibrary.simpleMessage("繞過私有網路"), "bypassPrivateRouteDesc": MessageLookupByLibrary.simpleMessage( "自動繞過私有網路IP位址", ), "cacheAlgorithm": MessageLookupByLibrary.simpleMessage("快取演算法"), "cacheCorrupt": MessageLookupByLibrary.simpleMessage("快取已損壞,是否清空?"), "cameraPermissionDenied": MessageLookupByLibrary.simpleMessage("相機權限被拒絕"), "cameraPermissionDesc": MessageLookupByLibrary.simpleMessage( "掃描二維碼需要相機權限,請在設定中授予相機權限。", ), "cancel": MessageLookupByLibrary.simpleMessage("取消"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消過濾系統應用程式"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全選"), "checkError": MessageLookupByLibrary.simpleMessage("檢測失敗"), "checkOrAddProfile": MessageLookupByLibrary.simpleMessage("請先新增配置"), "checkUpdate": MessageLookupByLibrary.simpleMessage("檢查更新"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("目前的應用程式已經是最新版了"), "checking": MessageLookupByLibrary.simpleMessage("檢測中..."), "circle": MessageLookupByLibrary.simpleMessage("圓環流轉"), "clearCacheDesc": MessageLookupByLibrary.simpleMessage( "是否需要清理 FakeIP & DNS 快取?", ), "clearCacheTitle": MessageLookupByLibrary.simpleMessage("清理快取"), "clearData": MessageLookupByLibrary.simpleMessage("清除資料"), "clipboard": MessageLookupByLibrary.simpleMessage("剪貼簿"), "clipboardDesc": MessageLookupByLibrary.simpleMessage("自動獲取剪貼簿訂閱連結"), "clipboardExport": MessageLookupByLibrary.simpleMessage("匯出剪貼簿"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪貼簿匯入"), "color": MessageLookupByLibrary.simpleMessage("顏色"), "colorSchemes": MessageLookupByLibrary.simpleMessage("配色方案"), "columns": MessageLookupByLibrary.simpleMessage("列數"), "compatible": MessageLookupByLibrary.simpleMessage("相容模式"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( "開啟將失去部分應用能力,獲得全量的 Clash 支援", ), "concurrencyLimit": MessageLookupByLibrary.simpleMessage("並發限制"), "concurrencyLimitDesc": MessageLookupByLibrary.simpleMessage("延遲測試的最大並發數量"), "confirm": MessageLookupByLibrary.simpleMessage("確定"), "connection": MessageLookupByLibrary.simpleMessage("活躍連線"), "connections": MessageLookupByLibrary.simpleMessage("連線"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看目前連線資料"), "connectivity": MessageLookupByLibrary.simpleMessage("連通性:"), "contactMe": MessageLookupByLibrary.simpleMessage("聯絡我"), "content": MessageLookupByLibrary.simpleMessage("內容"), "contentScheme": MessageLookupByLibrary.simpleMessage("內容主題"), "controlSecret": MessageLookupByLibrary.simpleMessage("控制密碼"), "controlSecretDesc": MessageLookupByLibrary.simpleMessage( "RESTful API 的存取密碼", ), "copy": MessageLookupByLibrary.simpleMessage("複製"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("複製環境變數"), "copyLink": MessageLookupByLibrary.simpleMessage("複製連結"), "copySuccess": MessageLookupByLibrary.simpleMessage("複製成功"), "core": MessageLookupByLibrary.simpleMessage("內核"), "coreConnected": MessageLookupByLibrary.simpleMessage("已連線"), "coreInfo": MessageLookupByLibrary.simpleMessage("內核資訊"), "coreSuspended": MessageLookupByLibrary.simpleMessage("已掛起"), "country": MessageLookupByLibrary.simpleMessage("區域"), "crashTest": MessageLookupByLibrary.simpleMessage("崩潰測試"), "create": MessageLookupByLibrary.simpleMessage("建立"), "creationTime": MessageLookupByLibrary.simpleMessage("建立時間"), "custom": MessageLookupByLibrary.simpleMessage("自訂"), "customUrl": MessageLookupByLibrary.simpleMessage("自訂 URL"), "cut": MessageLookupByLibrary.simpleMessage("剪下"), "dark": MessageLookupByLibrary.simpleMessage("深色"), "dashboard": MessageLookupByLibrary.simpleMessage("首頁"), "days": MessageLookupByLibrary.simpleMessage("天"), "defaultNameserver": MessageLookupByLibrary.simpleMessage("預設網域名稱伺服器"), "defaultNameserverDesc": MessageLookupByLibrary.simpleMessage( "用於解析 DNS 伺服器", ), "defaultSort": MessageLookupByLibrary.simpleMessage("按預設排序"), "defaultText": MessageLookupByLibrary.simpleMessage("預設"), "delay": MessageLookupByLibrary.simpleMessage("延遲"), "delayAnimation": MessageLookupByLibrary.simpleMessage("延遲動畫"), "delayAnimationDesc": MessageLookupByLibrary.simpleMessage("自訂測試過程中的動畫顯示"), "delaySort": MessageLookupByLibrary.simpleMessage("按延遲排序"), "delete": MessageLookupByLibrary.simpleMessage("刪除"), "deleteMultipTip": m0, "deleteTip": m1, "deleteTunnel": MessageLookupByLibrary.simpleMessage("刪除轉發"), "desc": MessageLookupByLibrary.simpleMessage( "Bettbox 基於強大靈活的 Mihomo (Clash.Meta) 代理內核,致力於提供更好的體驗,Forked from FlClash,Better Experience, Out of the box", ), "destination": MessageLookupByLibrary.simpleMessage("目標地址"), "destinationGeoIP": MessageLookupByLibrary.simpleMessage("目標地理定位"), "destinationIPASN": MessageLookupByLibrary.simpleMessage("目標 IP ASN"), "details": m2, "detectionTip": MessageLookupByLibrary.simpleMessage("依賴第三方 API,僅供參考"), "developerMode": MessageLookupByLibrary.simpleMessage("開發者模式"), "developerModeEnableTip": MessageLookupByLibrary.simpleMessage("開發者模式已啟用。"), "dialerIp4pConvert": MessageLookupByLibrary.simpleMessage("啟用撥號 IP4P 地址轉換"), "dialerIp4pConvertDesc": MessageLookupByLibrary.simpleMessage( "啟用撥號器的 IP4P 地址轉換功能", ), "direct": MessageLookupByLibrary.simpleMessage("直連"), "directNameserver": MessageLookupByLibrary.simpleMessage("直連網域名稱伺服器"), "directNameserverDesc": MessageLookupByLibrary.simpleMessage("用於解析直連出口的網域"), "directNameserverFollowPolicy": MessageLookupByLibrary.simpleMessage( "直連 DNS 遵循規則", ), "disableQuic": MessageLookupByLibrary.simpleMessage("禁用QUIC"), "disableQuicDesc": MessageLookupByLibrary.simpleMessage("禁用QUIC以解決特定網路問題"), "disclaimer": MessageLookupByLibrary.simpleMessage("免責聲明"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage( "本軟體為開源免費軟體,僅供學習交流等非商業性質的個人測試使用,代理服務商的行為均與本軟體無關,同意聲明代表您已完全知曉並確認了這一點,如不同意,請選擇退出!", ), "discoverNewVersion": MessageLookupByLibrary.simpleMessage("發現新版本"), "discovery": MessageLookupByLibrary.simpleMessage("發現新版本"), "dnsDesc": MessageLookupByLibrary.simpleMessage("更新 DNS 相關設定"), "dnsHijack": MessageLookupByLibrary.simpleMessage("DNS 劫持"), "dnsHijackDesc": MessageLookupByLibrary.simpleMessage("將解析匯入內部 DNS 模組"), "dnsMode": MessageLookupByLibrary.simpleMessage("DNS 模式"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通過"), "domain": MessageLookupByLibrary.simpleMessage("網域"), "doubleBounce": MessageLookupByLibrary.simpleMessage("雙重彈奏"), "download": MessageLookupByLibrary.simpleMessage("下載"), "dozeSuspend": MessageLookupByLibrary.simpleMessage("休眠支援"), "dozeSuspendDesc": MessageLookupByLibrary.simpleMessage( "開啟後同步系統 Doze 休眠模式", ), "edit": MessageLookupByLibrary.simpleMessage("編輯"), "editTunnel": MessageLookupByLibrary.simpleMessage("編輯轉發"), "emptyTip": m3, "en": MessageLookupByLibrary.simpleMessage("英語"), "enableCrashReport": MessageLookupByLibrary.simpleMessage("應用崩潰分析"), "enableCrashReportDesc": MessageLookupByLibrary.simpleMessage( "必要時上傳應用崩潰日誌", ), "enableOverride": MessageLookupByLibrary.simpleMessage("啟用覆寫"), "endpointIndependentNat": MessageLookupByLibrary.simpleMessage("NAT 增強"), "endpointIndependentNatDesc": MessageLookupByLibrary.simpleMessage( "啟用獨立於端點的 NAT", ), "entries": MessageLookupByLibrary.simpleMessage("個項目"), "exclude": MessageLookupByLibrary.simpleMessage("背景隱藏"), "excludeChina": MessageLookupByLibrary.simpleMessage("排除國內"), "excludeChinaDesc": MessageLookupByLibrary.simpleMessage( "放行中國QUIC流量而非全部禁用", ), "excludeDesc": MessageLookupByLibrary.simpleMessage("從最近任務中隱藏應用程式"), "existsTip": m4, "exit": MessageLookupByLibrary.simpleMessage("退出"), "expand": MessageLookupByLibrary.simpleMessage("標準"), "experimental": MessageLookupByLibrary.simpleMessage("Experimental"), "experimentalDesc": MessageLookupByLibrary.simpleMessage("實驗性配置請謹慎使用"), "expirationTime": MessageLookupByLibrary.simpleMessage("到期時間"), "exportFile": MessageLookupByLibrary.simpleMessage("匯出檔案"), "exportLogs": MessageLookupByLibrary.simpleMessage("匯出日誌"), "exportSuccess": MessageLookupByLibrary.simpleMessage("匯出成功"), "expressiveScheme": MessageLookupByLibrary.simpleMessage("表現力"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制"), "externalControllerDesc": MessageLookupByLibrary.simpleMessage( "透過線上連接埠控制內核", ), "externalLink": MessageLookupByLibrary.simpleMessage("外部連結"), "externalResources": MessageLookupByLibrary.simpleMessage("外部資源"), "fadingCircle": MessageLookupByLibrary.simpleMessage("環影隱漸"), "fadingFour": MessageLookupByLibrary.simpleMessage("四方爍動"), "fakeIpFilterMode": MessageLookupByLibrary.simpleMessage("FakeIP 過濾模式"), "fakeIpFilterModeDesc": MessageLookupByLibrary.simpleMessage( "指定 FakeIP 過濾模式", ), "fakeipFilter": MessageLookupByLibrary.simpleMessage("FakeIP 過濾清單"), "fakeipRange": MessageLookupByLibrary.simpleMessage("FakeIP 範圍"), "fakeipRangeV6": MessageLookupByLibrary.simpleMessage("FakeIPv6 範圍"), "fakeipTtl": MessageLookupByLibrary.simpleMessage("FakeIP 有效時間"), "fallback": MessageLookupByLibrary.simpleMessage("Fallback"), "fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情況下使用境外 DNS"), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback 過濾"), "fcmOptimization": MessageLookupByLibrary.simpleMessage("FCM 優化"), "fcmOptimizationDesc": MessageLookupByLibrary.simpleMessage( "增強 FCM 直連時的網路穩定性", ), "fcmTip": MessageLookupByLibrary.simpleMessage( "FCM 連線和支援取決於裝置本身,顯示結果僅供參考。因系統權限原因,您需要關閉網路中的 \"允許繞過 VPN\" 選項,以獲得更加準確的結果", ), "fidelityScheme": MessageLookupByLibrary.simpleMessage("高保真"), "file": MessageLookupByLibrary.simpleMessage("檔案"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上傳設定檔"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage("檔案有修改,是否儲存修改"), "filterSystemApp": MessageLookupByLibrary.simpleMessage("過濾系統應用程式"), "findProcessMode": MessageLookupByLibrary.simpleMessage("尋找處理程序"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("開啟後將可以尋找處理程序"), "fontFamily": MessageLookupByLibrary.simpleMessage("字體"), "forceDnsMapping": MessageLookupByLibrary.simpleMessage("強制 DNS 映射"), "forceDnsMappingDesc": MessageLookupByLibrary.simpleMessage( "強制將 DNS 查詢結果映射到連線", ), "forceDomain": MessageLookupByLibrary.simpleMessage("強制嗅探網域"), "forceGCDesc": MessageLookupByLibrary.simpleMessage( "是否進行強制內核垃圾回收?實驗性功能,請謹慎使用", ), "forceGCTitle": MessageLookupByLibrary.simpleMessage("強制垃圾回收"), "formatError": MessageLookupByLibrary.simpleMessage("請檢查格式是否正確"), "fourColumns": MessageLookupByLibrary.simpleMessage("四列"), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("果繽紛"), "general": MessageLookupByLibrary.simpleMessage("一般"), "generalDesc": MessageLookupByLibrary.simpleMessage("修改一般設定"), "generateSecret": MessageLookupByLibrary.simpleMessage("產生"), "geoData": MessageLookupByLibrary.simpleMessage("地理資料"), "geodataLoader": MessageLookupByLibrary.simpleMessage("GEO 節能"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage( "開啟後使用 GEO 低記憶體載入器", ), "geoipCode": MessageLookupByLibrary.simpleMessage("GeoIP 代碼"), "getOriginRules": MessageLookupByLibrary.simpleMessage("獲取原始規則"), "global": MessageLookupByLibrary.simpleMessage("全域"), "go": MessageLookupByLibrary.simpleMessage("前往"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下載"), "harmonyFont": MessageLookupByLibrary.simpleMessage("鴻蒙字體"), "harmonyFontDesc": MessageLookupByLibrary.simpleMessage( "使用最佳化的 HarmonyOS Sans", ), "hasCacheChange": MessageLookupByLibrary.simpleMessage("是否快取修改"), "healthCheckTimeout": MessageLookupByLibrary.simpleMessage("超時時間"), "healthCheckTimeoutDesc": MessageLookupByLibrary.simpleMessage( "節點健康檢查超時時間", ), "highRefreshRate": MessageLookupByLibrary.simpleMessage("高重新整理率"), "highRefreshRateDesc": MessageLookupByLibrary.simpleMessage( "啟用裝置最高重新整理率支援", ), "host": MessageLookupByLibrary.simpleMessage("主機"), "hostsDesc": MessageLookupByLibrary.simpleMessage("附加目前配置 Hosts"), "hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷鍵衝突"), "hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷鍵管理"), "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage("使用鍵盤控制應用程式"), "hours": MessageLookupByLibrary.simpleMessage("小時"), "httpPortSniffer": MessageLookupByLibrary.simpleMessage("HTTP 連接埠嗅探"), "icmpForwarding": MessageLookupByLibrary.simpleMessage("ICMP 轉發"), "icmpForwardingDesc": MessageLookupByLibrary.simpleMessage( "開啟後將支援 ICMP Ping", ), "icon": MessageLookupByLibrary.simpleMessage("圖片"), "iconConfiguration": MessageLookupByLibrary.simpleMessage("圖片設定"), "iconStyle": MessageLookupByLibrary.simpleMessage("圖示樣式"), "import": MessageLookupByLibrary.simpleMessage("匯入"), "importFailed": MessageLookupByLibrary.simpleMessage("匯入失敗"), "importFile": MessageLookupByLibrary.simpleMessage("透過檔案匯入"), "importFromCode": MessageLookupByLibrary.simpleMessage("透過程式碼匯入"), "importFromURL": MessageLookupByLibrary.simpleMessage("從 URL 匯入"), "importUrl": MessageLookupByLibrary.simpleMessage("透過 URL 匯入"), "infiniteTime": MessageLookupByLibrary.simpleMessage("長期有效"), "init": MessageLookupByLibrary.simpleMessage("初始化"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("請輸入正確的快捷鍵"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("智慧選擇"), "internet": MessageLookupByLibrary.simpleMessage("網際網路"), "interval": MessageLookupByLibrary.simpleMessage("間隔"), "intranetIP": MessageLookupByLibrary.simpleMessage("內網 IP"), "invalidIpFormat": MessageLookupByLibrary.simpleMessage("無效的 IP 或 CIDR 格式"), "ipClickBehavior": MessageLookupByLibrary.simpleMessage("顯示切換"), "ipPrivacyProtection": MessageLookupByLibrary.simpleMessage("隱藏 IP 顯示"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP / 遮罩"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("開啟後將可以接收 IPv6 流量"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允許 IPv6 入站"), "ja": MessageLookupByLibrary.simpleMessage("日語"), "just": MessageLookupByLibrary.simpleMessage("剛剛"), "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP 保持活動間隔"), "key": MessageLookupByLibrary.simpleMessage("鍵"), "language": MessageLookupByLibrary.simpleMessage("語言"), "layout": MessageLookupByLibrary.simpleMessage("版面配置"), "light": MessageLookupByLibrary.simpleMessage("淺色"), "lightIcon": MessageLookupByLibrary.simpleMessage("丹青留白"), "lightIconDesc": MessageLookupByLibrary.simpleMessage("手動切換淺色系桌面應用程式圖示"), "list": MessageLookupByLibrary.simpleMessage("清單"), "listen": MessageLookupByLibrary.simpleMessage("監聽"), "local": MessageLookupByLibrary.simpleMessage("本機"), "localBackupDesc": MessageLookupByLibrary.simpleMessage("備份資料到本機"), "localRecoveryDesc": MessageLookupByLibrary.simpleMessage("透過檔案還原資料"), "log": MessageLookupByLibrary.simpleMessage("日誌"), "logLevel": MessageLookupByLibrary.simpleMessage("日誌等級"), "logcat": MessageLookupByLibrary.simpleMessage("日誌捕獲"), "logcatDesc": MessageLookupByLibrary.simpleMessage("開啟後將會顯示日誌入口"), "logs": MessageLookupByLibrary.simpleMessage("日誌"), "logsDesc": MessageLookupByLibrary.simpleMessage("查看日誌擷取記錄"), "logsTest": MessageLookupByLibrary.simpleMessage("日誌測試"), "loopback": MessageLookupByLibrary.simpleMessage("迴環解鎖工具"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("用於 UWP 迴環解鎖"), "loose": MessageLookupByLibrary.simpleMessage("寬鬆"), "manualRefreshIp": MessageLookupByLibrary.simpleMessage("重新取得 IP"), "memoryInfo": MessageLookupByLibrary.simpleMessage("記憶體資訊"), "messageTest": MessageLookupByLibrary.simpleMessage("訊息測試"), "messageTestTip": MessageLookupByLibrary.simpleMessage("這是一條訊息。"), "min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出最小化"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系統預設退出事件"), "minutes": MessageLookupByLibrary.simpleMessage("分鐘"), "mixedPort": MessageLookupByLibrary.simpleMessage("混合連接埠"), "mode": MessageLookupByLibrary.simpleMessage("模式"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("單色"), "months": MessageLookupByLibrary.simpleMessage("月"), "more": MessageLookupByLibrary.simpleMessage("查看"), "name": MessageLookupByLibrary.simpleMessage("名稱"), "nameSort": MessageLookupByLibrary.simpleMessage("按名稱排序"), "nameserver": MessageLookupByLibrary.simpleMessage("網域名稱伺服器"), "nameserverDesc": MessageLookupByLibrary.simpleMessage("用於解析網域"), "nameserverPolicy": MessageLookupByLibrary.simpleMessage("網域名稱伺服器策略"), "nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage( "指定對應網域名稱伺服器策略", ), "navBarHapticFeedback": MessageLookupByLibrary.simpleMessage("觸覺回饋"), "navBarHapticFeedbackDesc": MessageLookupByLibrary.simpleMessage( "底部導覽列切換震動回饋", ), "network": MessageLookupByLibrary.simpleMessage("網路"), "networkDesc": MessageLookupByLibrary.simpleMessage("修改網路相關設定"), "networkDetection": MessageLookupByLibrary.simpleMessage("網路檢測"), "networkFix": MessageLookupByLibrary.simpleMessage("網路修復"), "networkFixDesc": MessageLookupByLibrary.simpleMessage( "修復 Windows 網路檢測地球圖示問題", ), "networkMatch": MessageLookupByLibrary.simpleMessage("網路匹配"), "networkMatchHint": MessageLookupByLibrary.simpleMessage( "輸入 IP 或 CIDR,最多 2 個,以逗號分隔", ), "networkSpeed": MessageLookupByLibrary.simpleMessage("網路速度"), "networkType": MessageLookupByLibrary.simpleMessage("網路類型"), "neutralScheme": MessageLookupByLibrary.simpleMessage("中性"), "noAnimation": MessageLookupByLibrary.simpleMessage("預設"), "noData": MessageLookupByLibrary.simpleMessage("暫無資料"), "noHotKey": MessageLookupByLibrary.simpleMessage("暫無快捷鍵"), "noIcon": MessageLookupByLibrary.simpleMessage("無圖示"), "noInfo": MessageLookupByLibrary.simpleMessage("暫無資訊"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暫無更多資訊"), "noNetwork": MessageLookupByLibrary.simpleMessage("無網路"), "noNetworkApp": MessageLookupByLibrary.simpleMessage("無網路應用程式"), "noProxy": MessageLookupByLibrary.simpleMessage("暫無代理"), "noProxyDesc": MessageLookupByLibrary.simpleMessage("請建立配置或新增有效的設定檔"), "noResolve": MessageLookupByLibrary.simpleMessage("不解析 IP"), "noStatusAvailable": MessageLookupByLibrary.simpleMessage("未獲取到狀態"), "nodeExclusion": MessageLookupByLibrary.simpleMessage("節點排除"), "nodeExclusionDesc": MessageLookupByLibrary.simpleMessage("排除所有匹配到的節點"), "nodeExclusionPlaceholder": MessageLookupByLibrary.simpleMessage( "HK|香港|🇭🇰", ), "none": MessageLookupByLibrary.simpleMessage("無"), "notRecommended": MessageLookupByLibrary.simpleMessage("不推薦"), "notSelectedTip": MessageLookupByLibrary.simpleMessage("目前的代理群組無法選取"), "ntp": MessageLookupByLibrary.simpleMessage("NTP"), "ntpDesc": MessageLookupByLibrary.simpleMessage("使用 NTP 時間服務"), "ntpInterval": MessageLookupByLibrary.simpleMessage("更新時間"), "ntpPort": MessageLookupByLibrary.simpleMessage("連接埠"), "ntpServer": MessageLookupByLibrary.simpleMessage("伺服器"), "ntpStatus": MessageLookupByLibrary.simpleMessage("狀態"), "ntpStatusDesc": MessageLookupByLibrary.simpleMessage("開啟 NTP 時間服務"), "nullProfileDesc": MessageLookupByLibrary.simpleMessage("沒有設定檔,請先新增設定檔"), "nullTip": m5, "numberTip": m6, "oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "onlinePanel": MessageLookupByLibrary.simpleMessage("線上面板"), "onlyIcon": MessageLookupByLibrary.simpleMessage("僅圖示"), "onlyOtherApps": MessageLookupByLibrary.simpleMessage("僅第三方應用程式"), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("代理流量統計"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( "開啟後將只統計代理流量", ), "openDashboard": MessageLookupByLibrary.simpleMessage("打開 Zashboard"), "openSettings": MessageLookupByLibrary.simpleMessage("打開設定"), "options": MessageLookupByLibrary.simpleMessage("選項"), "other": MessageLookupByLibrary.simpleMessage("其他"), "otherContributors": MessageLookupByLibrary.simpleMessage("其他貢獻者"), "otherSettings": MessageLookupByLibrary.simpleMessage("增強工具"), "otherSettingsDesc": MessageLookupByLibrary.simpleMessage("修改增強工具設定"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "override": MessageLookupByLibrary.simpleMessage("覆寫"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆寫代理相關配置"), "overrideDestination": MessageLookupByLibrary.simpleMessage("覆蓋目標地址"), "overrideDestinationDesc": MessageLookupByLibrary.simpleMessage( "使用嗅探結果覆蓋連線目標地址", ), "overrideDns": MessageLookupByLibrary.simpleMessage("覆寫 DNS"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage( "開啟後將覆蓋配置中的 DNS 選項", ), "overrideExperimental": MessageLookupByLibrary.simpleMessage( "覆寫 Experimental", ), "overrideExperimentalDesc": MessageLookupByLibrary.simpleMessage( "開啟後將覆蓋配置中的實驗性配置", ), "overrideInvalidTip": MessageLookupByLibrary.simpleMessage("在腳本模式下不生效"), "overrideNtp": MessageLookupByLibrary.simpleMessage("覆寫 NTP"), "overrideNtpDesc": MessageLookupByLibrary.simpleMessage( "開啟後將覆蓋配置中的 NTP 選項", ), "overrideOriginRules": MessageLookupByLibrary.simpleMessage("覆蓋原始規則"), "overrideSniffer": MessageLookupByLibrary.simpleMessage("覆寫 Sniffer"), "overrideSnifferDesc": MessageLookupByLibrary.simpleMessage( "開啟後將覆蓋配置中的 Sniffer 選項", ), "overrideTestUrl": MessageLookupByLibrary.simpleMessage("覆蓋配置"), "overrideTunnel": MessageLookupByLibrary.simpleMessage("覆寫 Tunnel"), "overrideTunnelDesc": MessageLookupByLibrary.simpleMessage( "開啟後將覆蓋配置中的 Tunnel 選項", ), "packageListPermissionDenied": MessageLookupByLibrary.simpleMessage( "權限被拒絕。沒有權限無法存取應用程式清單。", ), "packageListPermissionRequired": MessageLookupByLibrary.simpleMessage( "此功能需要存取已安裝應用程式清單的權限。是否授予此權限?", ), "palette": MessageLookupByLibrary.simpleMessage("調色盤"), "parsePureIp": MessageLookupByLibrary.simpleMessage("解析純 IP 連線"), "parsePureIpDesc": MessageLookupByLibrary.simpleMessage("解析純 IP 連線"), "password": MessageLookupByLibrary.simpleMessage("密碼"), "paste": MessageLookupByLibrary.simpleMessage("貼上"), "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("請綁定 WebDAV"), "pleaseCloseSystemProxyFirst": MessageLookupByLibrary.simpleMessage( "請先關閉系統代理", ), "pleaseCloseTunFirst": MessageLookupByLibrary.simpleMessage("請先關閉虛擬網卡"), "pleaseEnterScriptName": MessageLookupByLibrary.simpleMessage("請輸入腳本名稱"), "pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage( "請輸入管理員密碼", ), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("請上傳檔案"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "請上傳有效的二維碼", ), "port": MessageLookupByLibrary.simpleMessage("連接埠"), "portConflictTip": MessageLookupByLibrary.simpleMessage("請輸入不同的連接埠"), "portTip": m7, "powerSwitch": MessageLookupByLibrary.simpleMessage("啟動開關"), "preferH3Desc": MessageLookupByLibrary.simpleMessage("優先使用 DOH 的 http/3"), "pressKeyboard": MessageLookupByLibrary.simpleMessage("請按下按鍵"), "preview": MessageLookupByLibrary.simpleMessage("預覽"), "profile": MessageLookupByLibrary.simpleMessage("配置"), "profileAutoUpdateIntervalInvalidValidationDesc": MessageLookupByLibrary.simpleMessage("請輸入有效間隔時間格式"), "profileAutoUpdateIntervalNullValidationDesc": MessageLookupByLibrary.simpleMessage("請輸入自動更新間隔時間"), "profileHasUpdate": MessageLookupByLibrary.simpleMessage( "設定檔已經修改,是否關閉自動更新 ", ), "profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage( "請輸入配置名稱", ), "profileParseErrorDesc": MessageLookupByLibrary.simpleMessage("設定檔解析錯誤"), "profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage( "請輸入有效配置 URL", ), "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage( "請輸入配置 URL", ), "profiles": MessageLookupByLibrary.simpleMessage("配置"), "profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"), "progress": MessageLookupByLibrary.simpleMessage("處理程序"), "project": MessageLookupByLibrary.simpleMessage("專案"), "providers": MessageLookupByLibrary.simpleMessage("提供者"), "proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理設定"), "proxyChains": MessageLookupByLibrary.simpleMessage("代理鏈"), "proxyGroup": MessageLookupByLibrary.simpleMessage("代理群組"), "proxyNameserver": MessageLookupByLibrary.simpleMessage("代理網域名稱伺服器"), "proxyNameserverDesc": MessageLookupByLibrary.simpleMessage("用於解析代理節點的網域"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理連接埠"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("設定 Clash 監聽連接埠"), "proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"), "pulse": MessageLookupByLibrary.simpleMessage("脈衝律動"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("純黑模式"), "qrcode": MessageLookupByLibrary.simpleMessage("二維碼"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("掃描二維碼獲取設定檔"), "quicGoDisableEcn": MessageLookupByLibrary.simpleMessage("停用 QUIC 顯式擁塞通知"), "quicGoDisableEcnDesc": MessageLookupByLibrary.simpleMessage( "停用 QUIC 的顯式擁塞通知功能", ), "quicGoDisableGso": MessageLookupByLibrary.simpleMessage("停用 QUIC 通用分段卸載"), "quicGoDisableGsoDesc": MessageLookupByLibrary.simpleMessage( "停用 QUIC 的通用分段卸載功能", ), "quicPortSniffer": MessageLookupByLibrary.simpleMessage("QUIC 連接埠嗅探"), "quickResponse": MessageLookupByLibrary.simpleMessage("快速響應"), "quickResponseDesc": MessageLookupByLibrary.simpleMessage("網路發生變化時主動斷開連接"), "rainbowScheme": MessageLookupByLibrary.simpleMessage("彩虹"), "recovery": MessageLookupByLibrary.simpleMessage("還原"), "recoveryAll": MessageLookupByLibrary.simpleMessage("還原所有資料"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("僅還原設定檔"), "recoveryStrategy": MessageLookupByLibrary.simpleMessage("還原策略"), "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage("相容"), "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage("覆蓋"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("還原成功"), "redirPort": MessageLookupByLibrary.simpleMessage("Redir 連接埠"), "redo": MessageLookupByLibrary.simpleMessage("重做"), "refreshAppList": MessageLookupByLibrary.simpleMessage("重新整理應用程式清單"), "refreshAppListConfirm": MessageLookupByLibrary.simpleMessage( "是否重新整理應用程式清單?", ), "regExp": MessageLookupByLibrary.simpleMessage("正規表示式"), "remote": MessageLookupByLibrary.simpleMessage("遠端"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage("備份資料到 WebDAV"), "remoteDestination": MessageLookupByLibrary.simpleMessage("遠端目標"), "remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage( "透過 WebDAV 還原資料", ), "remove": MessageLookupByLibrary.simpleMessage("移除"), "rename": MessageLookupByLibrary.simpleMessage("重新命名"), "request": MessageLookupByLibrary.simpleMessage("請求"), "requests": MessageLookupByLibrary.simpleMessage("請求"), "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近請求記錄"), "reset": MessageLookupByLibrary.simpleMessage("重設"), "resetTip": MessageLookupByLibrary.simpleMessage("確定要重設嗎?"), "resources": MessageLookupByLibrary.simpleMessage("資源"), "resourcesDesc": MessageLookupByLibrary.simpleMessage("外部資源相關資訊"), "respectRules": MessageLookupByLibrary.simpleMessage("遵守規則"), "respectRulesDesc": MessageLookupByLibrary.simpleMessage("DNS 連線跟隨 Rules"), "restart": MessageLookupByLibrary.simpleMessage("重啟"), "restartCoreDesc": MessageLookupByLibrary.simpleMessage("是否手動重啟內核?"), "restartCoreTitle": MessageLookupByLibrary.simpleMessage("重啟內核"), "restartTip": MessageLookupByLibrary.simpleMessage("重啟TUN後改變生效"), "retry": MessageLookupByLibrary.simpleMessage("重試"), "rotatingCircle": MessageLookupByLibrary.simpleMessage("單圓自轉"), "ru": MessageLookupByLibrary.simpleMessage("俄語"), "rule": MessageLookupByLibrary.simpleMessage("規則"), "ruleName": MessageLookupByLibrary.simpleMessage("規則名稱"), "ruleProviders": MessageLookupByLibrary.simpleMessage("規則提供者"), "ruleTarget": MessageLookupByLibrary.simpleMessage("規則目標"), "runTime": MessageLookupByLibrary.simpleMessage("啟動時間"), "runtimeConfig": MessageLookupByLibrary.simpleMessage("執行時配置"), "save": MessageLookupByLibrary.simpleMessage("儲存"), "saveChanges": MessageLookupByLibrary.simpleMessage("是否儲存更改?"), "saveTip": MessageLookupByLibrary.simpleMessage("確定要儲存嗎?"), "script": MessageLookupByLibrary.simpleMessage("腳本"), "scriptDesc": MessageLookupByLibrary.simpleMessage("配置全局覆寫腳本"), "search": MessageLookupByLibrary.simpleMessage("搜尋"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), "secretCopied": MessageLookupByLibrary.simpleMessage("密碼已複製到剪貼簿"), "selectAll": MessageLookupByLibrary.simpleMessage("全選"), "selected": MessageLookupByLibrary.simpleMessage("已選擇"), "selectedCountTitle": m8, "serviceReady": MessageLookupByLibrary.simpleMessage("服務已就緒"), "serviceRunning": MessageLookupByLibrary.simpleMessage("服務正在執行中"), "settings": MessageLookupByLibrary.simpleMessage("設定"), "show": MessageLookupByLibrary.simpleMessage("顯示"), "shrink": MessageLookupByLibrary.simpleMessage("緊湊"), "silentLaunch": MessageLookupByLibrary.simpleMessage("靜默啟動"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("不打開軟體直接在背景啟動"), "size": MessageLookupByLibrary.simpleMessage("尺寸"), "skipDomain": MessageLookupByLibrary.simpleMessage("跳過網域"), "skipDstAddress": MessageLookupByLibrary.simpleMessage("跳過目標 IP"), "skipSrcAddress": MessageLookupByLibrary.simpleMessage("跳過來源 IP"), "smartAutoStop": MessageLookupByLibrary.simpleMessage("智慧啟停"), "smartAutoStopDesc": MessageLookupByLibrary.simpleMessage("連線指定網路後停止代理服務"), "smartAutoStopServiceRunning": MessageLookupByLibrary.simpleMessage( "智慧啟停服務執行中", ), "smartDelayLaunch": MessageLookupByLibrary.simpleMessage("智慧延遲"), "smartDelayLaunchDesc": MessageLookupByLibrary.simpleMessage("在網路成功連線以後啟動"), "sniffer": MessageLookupByLibrary.simpleMessage("Sniffer"), "snifferAddressHint": MessageLookupByLibrary.simpleMessage("每行一個地址"), "snifferDesc": MessageLookupByLibrary.simpleMessage("修改網域嗅探配置"), "snifferDomainHint": MessageLookupByLibrary.simpleMessage("每行一個網域"), "snifferPorts": MessageLookupByLibrary.simpleMessage("連接埠"), "snifferPortsHint": MessageLookupByLibrary.simpleMessage( "例如: 80, 8080-8880", ), "snifferStatus": MessageLookupByLibrary.simpleMessage("狀態"), "snifferStatusDesc": MessageLookupByLibrary.simpleMessage("開啟嗅探服務設定"), "socksPort": MessageLookupByLibrary.simpleMessage("Socks 連接埠"), "sort": MessageLookupByLibrary.simpleMessage("排序"), "source": MessageLookupByLibrary.simpleMessage("來源"), "sourceIp": MessageLookupByLibrary.simpleMessage("來源 IP"), "specialProxy": MessageLookupByLibrary.simpleMessage("特殊代理"), "specialRules": MessageLookupByLibrary.simpleMessage("特殊規則"), "spinningLines": MessageLookupByLibrary.simpleMessage("流光旋繞"), "stackMode": MessageLookupByLibrary.simpleMessage("堆疊模式"), "standard": MessageLookupByLibrary.simpleMessage("標準"), "start": MessageLookupByLibrary.simpleMessage("啟動"), "startTest": MessageLookupByLibrary.simpleMessage("延遲測試"), "startVpn": MessageLookupByLibrary.simpleMessage("正在啟動"), "status": MessageLookupByLibrary.simpleMessage("狀態"), "statusDesc": MessageLookupByLibrary.simpleMessage("關閉後將使用系統 DNS"), "stop": MessageLookupByLibrary.simpleMessage("停止"), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止"), "storeFix": MessageLookupByLibrary.simpleMessage("商店修復"), "storeFixDesc": MessageLookupByLibrary.simpleMessage( "修復 Google Play 商店下載異常", ), "strictRoute": MessageLookupByLibrary.simpleMessage("嚴格路由"), "strictRouteDesc": MessageLookupByLibrary.simpleMessage("使用 TUN 嚴格路由模式"), "style": MessageLookupByLibrary.simpleMessage("風格"), "subRule": MessageLookupByLibrary.simpleMessage("子規則"), "submit": MessageLookupByLibrary.simpleMessage("提交"), "success": MessageLookupByLibrary.simpleMessage("Success"), "switchLabel": MessageLookupByLibrary.simpleMessage("開關"), "switchToDomesticIp": MessageLookupByLibrary.simpleMessage("取得國內 IP"), "sync": MessageLookupByLibrary.simpleMessage("同步"), "syncAll": MessageLookupByLibrary.simpleMessage("全部同步"), "syncFailed": MessageLookupByLibrary.simpleMessage("同步失敗"), "system": MessageLookupByLibrary.simpleMessage("系統"), "systemApp": MessageLookupByLibrary.simpleMessage("系統應用程式"), "systemFont": MessageLookupByLibrary.simpleMessage("系統字體"), "systemProxy": MessageLookupByLibrary.simpleMessage("系統代理"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("設定系統代理"), "tab": MessageLookupByLibrary.simpleMessage("分頁"), "tabAnimation": MessageLookupByLibrary.simpleMessage("切換動畫"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage("僅在部分行動檢視中有效"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP 並發"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("開啟後允許 TCP 並發連線"), "testUrl": MessageLookupByLibrary.simpleMessage("測試連結"), "textScale": MessageLookupByLibrary.simpleMessage("文字縮放"), "theme": MessageLookupByLibrary.simpleMessage("主題"), "themeColor": MessageLookupByLibrary.simpleMessage("主題色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("設定主題色彩及圖示"), "themeMode": MessageLookupByLibrary.simpleMessage("主題模式"), "threeBounce": MessageLookupByLibrary.simpleMessage("靈珠躍動"), "threeColumns": MessageLookupByLibrary.simpleMessage("三列"), "threeInOut": MessageLookupByLibrary.simpleMessage("三星舒合"), "tight": MessageLookupByLibrary.simpleMessage("緊湊"), "time": MessageLookupByLibrary.simpleMessage("時間"), "tip": MessageLookupByLibrary.simpleMessage("提示"), "tlsPortSniffer": MessageLookupByLibrary.simpleMessage("TLS 連接埠嗅探"), "toggle": MessageLookupByLibrary.simpleMessage("切換"), "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("調性點綴"), "tooManyRules": MessageLookupByLibrary.simpleMessage("最多允許 2 個規則"), "tools": MessageLookupByLibrary.simpleMessage("更多"), "tproxyPort": MessageLookupByLibrary.simpleMessage("Tproxy 連接埠"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量統計"), "tryManualRefresh": MessageLookupByLibrary.simpleMessage("請嘗試手動重新整理"), "tun": MessageLookupByLibrary.simpleMessage("虛擬網卡"), "tunDesc": MessageLookupByLibrary.simpleMessage("接管目前裝置全域流量"), "tunEnableRequireAdmin": MessageLookupByLibrary.simpleMessage( "啟用虛擬網卡需要管理員權限,請以管理員身分執行程式", ), "tunnel": MessageLookupByLibrary.simpleMessage("Tunnel"), "tunnelAddress": MessageLookupByLibrary.simpleMessage("監聽地址"), "tunnelAddressHint": MessageLookupByLibrary.simpleMessage( "例如: 127.0.0.1:6553", ), "tunnelDesc": MessageLookupByLibrary.simpleMessage("使用流量轉發隧道"), "tunnelList": MessageLookupByLibrary.simpleMessage("轉發清單"), "tunnelNetwork": MessageLookupByLibrary.simpleMessage("網路協定"), "tunnelNetworkHint": MessageLookupByLibrary.simpleMessage("例如: tcp, udp"), "tunnelProxy": MessageLookupByLibrary.simpleMessage("代理名稱"), "tunnelProxyHint": MessageLookupByLibrary.simpleMessage("例如: proxy (可選)"), "tunnelTarget": MessageLookupByLibrary.simpleMessage("目標地址"), "tunnelTargetHint": MessageLookupByLibrary.simpleMessage( "例如: 114.114.114.114:53", ), "twoColumns": MessageLookupByLibrary.simpleMessage("兩列"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "無法更新目前的設定檔", ), "undo": MessageLookupByLibrary.simpleMessage("復原"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("統一延遲"), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除交握解析等額外延遲"), "unknown": MessageLookupByLibrary.simpleMessage("未知"), "unnamed": MessageLookupByLibrary.simpleMessage("未命名"), "update": MessageLookupByLibrary.simpleMessage("更新"), "upload": MessageLookupByLibrary.simpleMessage("上傳"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("透過 URL 獲取設定檔"), "urlTip": m9, "useHosts": MessageLookupByLibrary.simpleMessage("使用 Hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系統 Hosts"), "value": MessageLookupByLibrary.simpleMessage("值"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("活力"), "view": MessageLookupByLibrary.simpleMessage("查看"), "vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相關設定"), "vpnEnableDesc": MessageLookupByLibrary.simpleMessage( "透過 VpnService 自動路由系統所有流量", ), "vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage( "為 VpnService 附加 HTTP 代理", ), "vpnTip": MessageLookupByLibrary.simpleMessage("重啟VPN後改變生效"), "wakelock": MessageLookupByLibrary.simpleMessage("亮螢幕鎖"), "wakelockDescription": MessageLookupByLibrary.simpleMessage( "本功能不需要任何特殊權限,因為它僅啟用螢幕喚醒鎖,而不是任何 CPU 喚醒鎖,應用程式會在背景保持必要的活躍,且螢幕不會自動熄滅,這在一些場景下會很有用", ), "wave": MessageLookupByLibrary.simpleMessage("波浪起伏"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV 設定"), "whitelist": MessageLookupByLibrary.simpleMessage("白名單"), "whitelistMode": MessageLookupByLibrary.simpleMessage("白名單模式"), "writeToSystem": MessageLookupByLibrary.simpleMessage("寫入系統"), "writeToSystemDesc": MessageLookupByLibrary.simpleMessage("需要管理員權限"), "years": MessageLookupByLibrary.simpleMessage("年"), "zh_CN": MessageLookupByLibrary.simpleMessage("簡體中文"), "zh_TC": MessageLookupByLibrary.simpleMessage("繁體中文"), }; } ================================================ FILE: lib/l10n/l10n.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'intl/messages_all.dart'; // ************************************************************************** // Generator: Flutter Intl IDE plugin // Made by Localizely // ************************************************************************** // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars // ignore_for_file: join_return_with_assignment, prefer_final_in_for_each // ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes class AppLocalizations { AppLocalizations(); static AppLocalizations? _current; static AppLocalizations get current { assert( _current != null, 'No instance of AppLocalizations was loaded. Try to initialize the AppLocalizations delegate before accessing AppLocalizations.current.', ); return _current!; } static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; final instance = AppLocalizations(); AppLocalizations._current = instance; return instance; }); } static AppLocalizations of(BuildContext context) { final instance = AppLocalizations.maybeOf(context); assert( instance != null, 'No instance of AppLocalizations present in the widget tree. Did you add AppLocalizations.delegate in localizationsDelegates?', ); return instance!; } static AppLocalizations? maybeOf(BuildContext context) { return Localizations.of(context, AppLocalizations); } /// `Rule` String get rule { return Intl.message('Rule', name: 'rule', desc: '', args: []); } /// `Global` String get global { return Intl.message('Global', name: 'global', desc: '', args: []); } /// `Direct` String get direct { return Intl.message('Direct', name: 'direct', desc: '', args: []); } /// `Dashboard` String get dashboard { return Intl.message('Dashboard', name: 'dashboard', desc: '', args: []); } /// `Proxies` String get proxies { return Intl.message('Proxies', name: 'proxies', desc: '', args: []); } /// `Profile` String get profile { return Intl.message('Profile', name: 'profile', desc: '', args: []); } /// `Profiles` String get profiles { return Intl.message('Profiles', name: 'profiles', desc: '', args: []); } /// `Tools` String get tools { return Intl.message('Tools', name: 'tools', desc: '', args: []); } /// `Logs` String get logs { return Intl.message('Logs', name: 'logs', desc: '', args: []); } /// `View captured logs` String get logsDesc { return Intl.message( 'View captured logs', name: 'logsDesc', desc: '', args: [], ); } /// `Resources` String get resources { return Intl.message('Resources', name: 'resources', desc: '', args: []); } /// `Sync All` String get syncAll { return Intl.message('Sync All', name: 'syncAll', desc: '', args: []); } /// `Sync Failed` String get syncFailed { return Intl.message('Sync Failed', name: 'syncFailed', desc: '', args: []); } /// `External resource info` String get resourcesDesc { return Intl.message( 'External resource info', name: 'resourcesDesc', desc: '', args: [], ); } /// `Global override script config` String get scriptDesc { return Intl.message( 'Global override script config', name: 'scriptDesc', desc: '', args: [], ); } /// `Traffic Usage` String get trafficUsage { return Intl.message( 'Traffic Usage', name: 'trafficUsage', desc: '', args: [], ); } /// `Core Info` String get coreInfo { return Intl.message('Core Info', name: 'coreInfo', desc: '', args: []); } /// `Network Speed` String get networkSpeed { return Intl.message( 'Network Speed', name: 'networkSpeed', desc: '', args: [], ); } /// `Outbound Mode` String get outboundMode { return Intl.message( 'Outbound Mode', name: 'outboundMode', desc: '', args: [], ); } /// `Network Detection` String get networkDetection { return Intl.message( 'Network Detection', name: 'networkDetection', desc: '', args: [], ); } /// `Upload` String get upload { return Intl.message('Upload', name: 'upload', desc: '', args: []); } /// `Download` String get download { return Intl.message('Download', name: 'download', desc: '', args: []); } /// `No Proxy` String get noProxy { return Intl.message('No Proxy', name: 'noProxy', desc: '', args: []); } /// `Please create or add a valid profile` String get noProxyDesc { return Intl.message( 'Please create or add a valid profile', name: 'noProxyDesc', desc: '', args: [], ); } /// `No profile. Please add one.` String get nullProfileDesc { return Intl.message( 'No profile. Please add one.', name: 'nullProfileDesc', desc: '', args: [], ); } /// `Settings` String get settings { return Intl.message('Settings', name: 'settings', desc: '', args: []); } /// `Language` String get language { return Intl.message('Language', name: 'language', desc: '', args: []); } /// `Default` String get defaultText { return Intl.message('Default', name: 'defaultText', desc: '', args: []); } /// `More` String get more { return Intl.message('More', name: 'more', desc: '', args: []); } /// `Other` String get other { return Intl.message('Other', name: 'other', desc: '', args: []); } /// `Enhanced Tools` String get otherSettings { return Intl.message( 'Enhanced Tools', name: 'otherSettings', desc: '', args: [], ); } /// `Modify enhanced tool settings` String get otherSettingsDesc { return Intl.message( 'Modify enhanced tool settings', name: 'otherSettingsDesc', desc: '', args: [], ); } /// `Smart Auto-Stop` String get smartAutoStop { return Intl.message( 'Smart Auto-Stop', name: 'smartAutoStop', desc: '', args: [], ); } /// `Stop VPN on specific networks` String get smartAutoStopDesc { return Intl.message( 'Stop VPN on specific networks', name: 'smartAutoStopDesc', desc: '', args: [], ); } /// `Network Match` String get networkMatch { return Intl.message( 'Network Match', name: 'networkMatch', desc: '', args: [], ); } /// `Max 2 IPs/CIDRs, comma-separated` String get networkMatchHint { return Intl.message( 'Max 2 IPs/CIDRs, comma-separated', name: 'networkMatchHint', desc: '', args: [], ); } /// `Smart Auto-Stop running` String get smartAutoStopServiceRunning { return Intl.message( 'Smart Auto-Stop running', name: 'smartAutoStopServiceRunning', desc: '', args: [], ); } /// `Service Running` String get serviceRunning { return Intl.message( 'Service Running', name: 'serviceRunning', desc: '', args: [], ); } /// `Connected` String get coreConnected { return Intl.message('Connected', name: 'coreConnected', desc: '', args: []); } /// `Suspended` String get coreSuspended { return Intl.message('Suspended', name: 'coreSuspended', desc: '', args: []); } /// `Invalid IP or CIDR format` String get invalidIpFormat { return Intl.message( 'Invalid IP or CIDR format', name: 'invalidIpFormat', desc: '', args: [], ); } /// `Max 2 rules allowed` String get tooManyRules { return Intl.message( 'Max 2 rules allowed', name: 'tooManyRules', desc: '', args: [], ); } /// `Doze Support` String get dozeSuspend { return Intl.message( 'Doze Support', name: 'dozeSuspend', desc: '', args: [], ); } /// `Sync with system Doze mode` String get dozeSuspendDesc { return Intl.message( 'Sync with system Doze mode', name: 'dozeSuspendDesc', desc: '', args: [], ); } /// `Store Fix` String get storeFix { return Intl.message('Store Fix', name: 'storeFix', desc: '', args: []); } /// `Fix Play Store download issues` String get storeFixDesc { return Intl.message( 'Fix Play Store download issues', name: 'storeFixDesc', desc: '', args: [], ); } /// `Disable QUIC` String get disableQuic { return Intl.message( 'Disable QUIC', name: 'disableQuic', desc: '', args: [], ); } /// `Disable QUIC to resolve specific network issues` String get disableQuicDesc { return Intl.message( 'Disable QUIC to resolve specific network issues', name: 'disableQuicDesc', desc: '', args: [], ); } /// `Exclude China` String get excludeChina { return Intl.message( 'Exclude China', name: 'excludeChina', desc: '', args: [], ); } /// `Allow China QUIC traffic instead of blocking all` String get excludeChinaDesc { return Intl.message( 'Allow China QUIC traffic instead of blocking all', name: 'excludeChinaDesc', desc: '', args: [], ); } /// `FCM Optimization` String get fcmOptimization { return Intl.message( 'FCM Optimization', name: 'fcmOptimization', desc: '', args: [], ); } /// `Enhance FCM connection stability` String get fcmOptimizationDesc { return Intl.message( 'Enhance FCM connection stability', name: 'fcmOptimizationDesc', desc: '', args: [], ); } /// `Quick Response` String get quickResponse { return Intl.message( 'Quick Response', name: 'quickResponse', desc: '', args: [], ); } /// `Disconnect on network change (WiFi/Mobile)` String get quickResponseDesc { return Intl.message( 'Disconnect on network change (WiFi/Mobile)', name: 'quickResponseDesc', desc: '', args: [], ); } /// `Network Fix` String get networkFix { return Intl.message('Network Fix', name: 'networkFix', desc: '', args: []); } /// `Fix Windows network globe icon issue` String get networkFixDesc { return Intl.message( 'Fix Windows network globe icon issue', name: 'networkFixDesc', desc: '', args: [], ); } /// `Battery Optimization` String get batteryOptimization { return Intl.message( 'Battery Optimization', name: 'batteryOptimization', desc: '', args: [], ); } /// `Request battery optimization whitelist` String get batteryOptimizationDesc { return Intl.message( 'Request battery optimization whitelist', name: 'batteryOptimizationDesc', desc: '', args: [], ); } /// `Already in whitelist` String get alreadyInWhitelist { return Intl.message( 'Already in whitelist', name: 'alreadyInWhitelist', desc: '', args: [], ); } /// `About` String get about { return Intl.message('About', name: 'about', desc: '', args: []); } /// `English` String get en { return Intl.message('English', name: 'en', desc: '', args: []); } /// `Japanese` String get ja { return Intl.message('Japanese', name: 'ja', desc: '', args: []); } /// `Russian` String get ru { return Intl.message('Russian', name: 'ru', desc: '', args: []); } /// `Simplified Chinese` String get zh_CN { return Intl.message( 'Simplified Chinese', name: 'zh_CN', desc: '', args: [], ); } /// `Traditional Chinese` String get zh_TC { return Intl.message( 'Traditional Chinese', name: 'zh_TC', desc: '', args: [], ); } /// `Theme` String get theme { return Intl.message('Theme', name: 'theme', desc: '', args: []); } /// `Set theme color and icon` String get themeDesc { return Intl.message( 'Set theme color and icon', name: 'themeDesc', desc: '', args: [], ); } /// `Override` String get override { return Intl.message('Override', name: 'override', desc: '', args: []); } /// `Override proxy configurations` String get overrideDesc { return Intl.message( 'Override proxy configurations', name: 'overrideDesc', desc: '', args: [], ); } /// `Allow LAN` String get allowLan { return Intl.message('Allow LAN', name: 'allowLan', desc: '', args: []); } /// `Allow LAN access to proxy` String get allowLanDesc { return Intl.message( 'Allow LAN access to proxy', name: 'allowLanDesc', desc: '', args: [], ); } /// `TUN` String get tun { return Intl.message('TUN', name: 'tun', desc: '', args: []); } /// `Take over global device traffic` String get tunDesc { return Intl.message( 'Take over global device traffic', name: 'tunDesc', desc: '', args: [], ); } /// `Minimize on Exit` String get minimizeOnExit { return Intl.message( 'Minimize on Exit', name: 'minimizeOnExit', desc: '', args: [], ); } /// `Override default exit behavior` String get minimizeOnExitDesc { return Intl.message( 'Override default exit behavior', name: 'minimizeOnExitDesc', desc: '', args: [], ); } /// `Auto Launch` String get autoLaunch { return Intl.message('Auto Launch', name: 'autoLaunch', desc: '', args: []); } /// `Launch on system startup` String get autoLaunchDesc { return Intl.message( 'Launch on system startup', name: 'autoLaunchDesc', desc: '', args: [], ); } /// `Smart Delay` String get smartDelayLaunch { return Intl.message( 'Smart Delay', name: 'smartDelayLaunch', desc: '', args: [], ); } /// `Launch after network connected` String get smartDelayLaunchDesc { return Intl.message( 'Launch after network connected', name: 'smartDelayLaunchDesc', desc: '', args: [], ); } /// `Silent Launch` String get silentLaunch { return Intl.message( 'Silent Launch', name: 'silentLaunch', desc: '', args: [], ); } /// `Start in the background` String get silentLaunchDesc { return Intl.message( 'Start in the background', name: 'silentLaunchDesc', desc: '', args: [], ); } /// `Auto Run` String get autoRun { return Intl.message('Auto Run', name: 'autoRun', desc: '', args: []); } /// `Connect on app launch` String get autoRunDesc { return Intl.message( 'Connect on app launch', name: 'autoRunDesc', desc: '', args: [], ); } /// `Log Capture` String get logcat { return Intl.message('Log Capture', name: 'logcat', desc: '', args: []); } /// `Show log capture entry` String get logcatDesc { return Intl.message( 'Show log capture entry', name: 'logcatDesc', desc: '', args: [], ); } /// `Crash Analytics` String get enableCrashReport { return Intl.message( 'Crash Analytics', name: 'enableCrashReport', desc: '', args: [], ); } /// `Upload crash logs when needed` String get enableCrashReportDesc { return Intl.message( 'Upload crash logs when needed', name: 'enableCrashReportDesc', desc: '', args: [], ); } /// `Auto Check Updates` String get autoCheckUpdate { return Intl.message( 'Auto Check Updates', name: 'autoCheckUpdate', desc: '', args: [], ); } /// `Check updates on app launch` String get autoCheckUpdateDesc { return Intl.message( 'Check updates on app launch', name: 'autoCheckUpdateDesc', desc: '', args: [], ); } /// `Access Control` String get accessControl { return Intl.message( 'Access Control', name: 'accessControl', desc: '', args: [], ); } /// `Configure per-app proxy access` String get accessControlDesc { return Intl.message( 'Configure per-app proxy access', name: 'accessControlDesc', desc: '', args: [], ); } /// `Clear Cache` String get clearCacheTitle { return Intl.message( 'Clear Cache', name: 'clearCacheTitle', desc: '', args: [], ); } /// `Clear FakeIP and DNS cache?` String get clearCacheDesc { return Intl.message( 'Clear FakeIP and DNS cache?', name: 'clearCacheDesc', desc: '', args: [], ); } /// `Force Garbage Collection` String get forceGCTitle { return Intl.message( 'Force Garbage Collection', name: 'forceGCTitle', desc: '', args: [], ); } /// `Force kernel garbage collection? Experimental, use with caution.` String get forceGCDesc { return Intl.message( 'Force kernel garbage collection? Experimental, use with caution.', name: 'forceGCDesc', desc: '', args: [], ); } /// `FCM support depends on your device; results are for reference. Disable 'Allow Bypass VPN' in network settings for accurate results.` String get fcmTip { return Intl.message( 'FCM support depends on your device; results are for reference. Disable \'Allow Bypass VPN\' in network settings for accurate results.', name: 'fcmTip', desc: '', args: [], ); } /// `Application` String get application { return Intl.message('Application', name: 'application', desc: '', args: []); } /// `Modify application settings` String get applicationDesc { return Intl.message( 'Modify application settings', name: 'applicationDesc', desc: '', args: [], ); } /// `Edit` String get edit { return Intl.message('Edit', name: 'edit', desc: '', args: []); } /// `Confirm` String get confirm { return Intl.message('Confirm', name: 'confirm', desc: '', args: []); } /// `Update` String get update { return Intl.message('Update', name: 'update', desc: '', args: []); } /// `Add` String get add { return Intl.message('Add', name: 'add', desc: '', args: []); } /// `Save` String get save { return Intl.message('Save', name: 'save', desc: '', args: []); } /// `Delete` String get delete { return Intl.message('Delete', name: 'delete', desc: '', args: []); } /// `Years` String get years { return Intl.message('Years', name: 'years', desc: '', args: []); } /// `Months` String get months { return Intl.message('Months', name: 'months', desc: '', args: []); } /// `Hours` String get hours { return Intl.message('Hours', name: 'hours', desc: '', args: []); } /// `Days` String get days { return Intl.message('Days', name: 'days', desc: '', args: []); } /// `Minutes` String get minutes { return Intl.message('Minutes', name: 'minutes', desc: '', args: []); } /// `Seconds` String get seconds { return Intl.message('Seconds', name: 'seconds', desc: '', args: []); } /// ` Ago` String get ago { return Intl.message(' Ago', name: 'ago', desc: '', args: []); } /// `Please close TUN first` String get pleaseCloseTunFirst { return Intl.message( 'Please close TUN first', name: 'pleaseCloseTunFirst', desc: '', args: [], ); } /// `Please close System Proxy first` String get pleaseCloseSystemProxyFirst { return Intl.message( 'Please close System Proxy first', name: 'pleaseCloseSystemProxyFirst', desc: '', args: [], ); } /// `Just now` String get just { return Intl.message('Just now', name: 'just', desc: '', args: []); } /// `QR Code` String get qrcode { return Intl.message('QR Code', name: 'qrcode', desc: '', args: []); } /// `Scan QR code to get profile` String get qrcodeDesc { return Intl.message( 'Scan QR code to get profile', name: 'qrcodeDesc', desc: '', args: [], ); } /// `Clipboard` String get clipboard { return Intl.message('Clipboard', name: 'clipboard', desc: '', args: []); } /// `Get profile link from clipboard` String get clipboardDesc { return Intl.message( 'Get profile link from clipboard', name: 'clipboardDesc', desc: '', args: [], ); } /// `URL` String get url { return Intl.message('URL', name: 'url', desc: '', args: []); } /// `Get profile via URL` String get urlDesc { return Intl.message( 'Get profile via URL', name: 'urlDesc', desc: '', args: [], ); } /// `File` String get file { return Intl.message('File', name: 'file', desc: '', args: []); } /// `Upload profile file` String get fileDesc { return Intl.message( 'Upload profile file', name: 'fileDesc', desc: '', args: [], ); } /// `Name` String get name { return Intl.message('Name', name: 'name', desc: '', args: []); } /// `Please enter a profile name` String get profileNameNullValidationDesc { return Intl.message( 'Please enter a profile name', name: 'profileNameNullValidationDesc', desc: '', args: [], ); } /// `Please enter a profile URL` String get profileUrlNullValidationDesc { return Intl.message( 'Please enter a profile URL', name: 'profileUrlNullValidationDesc', desc: '', args: [], ); } /// `Please enter a valid URL` String get profileUrlInvalidValidationDesc { return Intl.message( 'Please enter a valid URL', name: 'profileUrlInvalidValidationDesc', desc: '', args: [], ); } /// `Auto Update` String get autoUpdate { return Intl.message('Auto Update', name: 'autoUpdate', desc: '', args: []); } /// `Auto update interval (min)` String get autoUpdateInterval { return Intl.message( 'Auto update interval (min)', name: 'autoUpdateInterval', desc: '', args: [], ); } /// `Please enter update interval` String get profileAutoUpdateIntervalNullValidationDesc { return Intl.message( 'Please enter update interval', name: 'profileAutoUpdateIntervalNullValidationDesc', desc: '', args: [], ); } /// `Please enter a valid interval` String get profileAutoUpdateIntervalInvalidValidationDesc { return Intl.message( 'Please enter a valid interval', name: 'profileAutoUpdateIntervalInvalidValidationDesc', desc: '', args: [], ); } /// `Theme Mode` String get themeMode { return Intl.message('Theme Mode', name: 'themeMode', desc: '', args: []); } /// `Theme Color` String get themeColor { return Intl.message('Theme Color', name: 'themeColor', desc: '', args: []); } /// `Preview` String get preview { return Intl.message('Preview', name: 'preview', desc: '', args: []); } /// `Runtime Config` String get runtimeConfig { return Intl.message( 'Runtime Config', name: 'runtimeConfig', desc: '', args: [], ); } /// `Camera Permission Denied` String get cameraPermissionDenied { return Intl.message( 'Camera Permission Denied', name: 'cameraPermissionDenied', desc: '', args: [], ); } /// `Camera permission is required to scan QR codes. Please grant it in settings.` String get cameraPermissionDesc { return Intl.message( 'Camera permission is required to scan QR codes. Please grant it in settings.', name: 'cameraPermissionDesc', desc: '', args: [], ); } /// `Open Settings` String get openSettings { return Intl.message( 'Open Settings', name: 'openSettings', desc: '', args: [], ); } /// `Retry` String get retry { return Intl.message('Retry', name: 'retry', desc: '', args: []); } /// `Permission to access installed apps is required. Grant now?` String get packageListPermissionRequired { return Intl.message( 'Permission to access installed apps is required. Grant now?', name: 'packageListPermissionRequired', desc: '', args: [], ); } /// `Permission denied. Cannot access app list.` String get packageListPermissionDenied { return Intl.message( 'Permission denied. Cannot access app list.', name: 'packageListPermissionDenied', desc: '', args: [], ); } /// `Auto` String get auto { return Intl.message('Auto', name: 'auto', desc: '', args: []); } /// `Light` String get light { return Intl.message('Light', name: 'light', desc: '', args: []); } /// `Dark` String get dark { return Intl.message('Dark', name: 'dark', desc: '', args: []); } /// `Import from URL` String get importFromURL { return Intl.message( 'Import from URL', name: 'importFromURL', desc: '', args: [], ); } /// `Submit` String get submit { return Intl.message('Submit', name: 'submit', desc: '', args: []); } /// `Do you want to pass` String get doYouWantToPass { return Intl.message( 'Do you want to pass', name: 'doYouWantToPass', desc: '', args: [], ); } /// `Create` String get create { return Intl.message('Create', name: 'create', desc: '', args: []); } /// `Default Sort` String get defaultSort { return Intl.message( 'Default Sort', name: 'defaultSort', desc: '', args: [], ); } /// `Sort by Delay` String get delaySort { return Intl.message('Sort by Delay', name: 'delaySort', desc: '', args: []); } /// `Sort by Name` String get nameSort { return Intl.message('Sort by Name', name: 'nameSort', desc: '', args: []); } /// `Please upload a file` String get pleaseUploadFile { return Intl.message( 'Please upload a file', name: 'pleaseUploadFile', desc: '', args: [], ); } /// `Please upload a valid QR code` String get pleaseUploadValidQrcode { return Intl.message( 'Please upload a valid QR code', name: 'pleaseUploadValidQrcode', desc: '', args: [], ); } /// `Blacklist Mode` String get blacklistMode { return Intl.message( 'Blacklist Mode', name: 'blacklistMode', desc: '', args: [], ); } /// `Whitelist Mode` String get whitelistMode { return Intl.message( 'Whitelist Mode', name: 'whitelistMode', desc: '', args: [], ); } /// `Filter System Apps` String get filterSystemApp { return Intl.message( 'Filter System Apps', name: 'filterSystemApp', desc: '', args: [], ); } /// `Show System Apps` String get cancelFilterSystemApp { return Intl.message( 'Show System Apps', name: 'cancelFilterSystemApp', desc: '', args: [], ); } /// `Select All` String get selectAll { return Intl.message('Select All', name: 'selectAll', desc: '', args: []); } /// `Deselect All` String get cancelSelectAll { return Intl.message( 'Deselect All', name: 'cancelSelectAll', desc: '', args: [], ); } /// `App Access Control` String get appAccessControl { return Intl.message( 'App Access Control', name: 'appAccessControl', desc: '', args: [], ); } /// `Only route selected apps through VPN` String get accessControlAllowDesc { return Intl.message( 'Only route selected apps through VPN', name: 'accessControlAllowDesc', desc: '', args: [], ); } /// `Exclude selected apps from VPN` String get accessControlNotAllowDesc { return Intl.message( 'Exclude selected apps from VPN', name: 'accessControlNotAllowDesc', desc: '', args: [], ); } /// `Selected` String get selected { return Intl.message('Selected', name: 'selected', desc: '', args: []); } /// `Unable to update current profile` String get unableToUpdateCurrentProfileDesc { return Intl.message( 'Unable to update current profile', name: 'unableToUpdateCurrentProfileDesc', desc: '', args: [], ); } /// `No more info` String get noMoreInfoDesc { return Intl.message( 'No more info', name: 'noMoreInfoDesc', desc: '', args: [], ); } /// `Profile parse error` String get profileParseErrorDesc { return Intl.message( 'Profile parse error', name: 'profileParseErrorDesc', desc: '', args: [], ); } /// `Proxy Port` String get proxyPort { return Intl.message('Proxy Port', name: 'proxyPort', desc: '', args: []); } /// `Set the Clash listening port` String get proxyPortDesc { return Intl.message( 'Set the Clash listening port', name: 'proxyPortDesc', desc: '', args: [], ); } /// `Port` String get port { return Intl.message('Port', name: 'port', desc: '', args: []); } /// `Log Level` String get logLevel { return Intl.message('Log Level', name: 'logLevel', desc: '', args: []); } /// `Show` String get show { return Intl.message('Show', name: 'show', desc: '', args: []); } /// `Exit` String get exit { return Intl.message('Exit', name: 'exit', desc: '', args: []); } /// `System Proxy` String get systemProxy { return Intl.message( 'System Proxy', name: 'systemProxy', desc: '', args: [], ); } /// `Project` String get project { return Intl.message('Project', name: 'project', desc: '', args: []); } /// `Core` String get core { return Intl.message('Core', name: 'core', desc: '', args: []); } /// `Tab Animation` String get tabAnimation { return Intl.message( 'Tab Animation', name: 'tabAnimation', desc: '', args: [], ); } /// `Bettbox is based on the powerful and flexible Mihomo (Clash.Meta) proxy kernel, dedicated to a superior user experience. Forked from FlClash: Better Experience, Out of the box` String get desc { return Intl.message( 'Bettbox is based on the powerful and flexible Mihomo (Clash.Meta) proxy kernel, dedicated to a superior user experience. Forked from FlClash: Better Experience, Out of the box', name: 'desc', desc: '', args: [], ); } /// `Starting...` String get startVpn { return Intl.message('Starting...', name: 'startVpn', desc: '', args: []); } /// `Stopping...` String get stopVpn { return Intl.message('Stopping...', name: 'stopVpn', desc: '', args: []); } /// `New Version Found` String get discovery { return Intl.message( 'New Version Found', name: 'discovery', desc: '', args: [], ); } /// `Compatible Mode` String get compatible { return Intl.message( 'Compatible Mode', name: 'compatible', desc: '', args: [], ); } /// `Reduces some features for full Clash compatibility` String get compatibleDesc { return Intl.message( 'Reduces some features for full Clash compatibility', name: 'compatibleDesc', desc: '', args: [], ); } /// `Current proxy group cannot be selected.` String get notSelectedTip { return Intl.message( 'Current proxy group cannot be selected.', name: 'notSelectedTip', desc: '', args: [], ); } /// `Tip` String get tip { return Intl.message('Tip', name: 'tip', desc: '', args: []); } /// `Backup & Restore` String get backupAndRecovery { return Intl.message( 'Backup & Restore', name: 'backupAndRecovery', desc: '', args: [], ); } /// `Sync data via WebDAV or local files` String get backupAndRecoveryDesc { return Intl.message( 'Sync data via WebDAV or local files', name: 'backupAndRecoveryDesc', desc: '', args: [], ); } /// `Account` String get account { return Intl.message('Account', name: 'account', desc: '', args: []); } /// `Backup` String get backup { return Intl.message('Backup', name: 'backup', desc: '', args: []); } /// `Restore` String get recovery { return Intl.message('Restore', name: 'recovery', desc: '', args: []); } /// `Restore Profiles Only` String get recoveryProfiles { return Intl.message( 'Restore Profiles Only', name: 'recoveryProfiles', desc: '', args: [], ); } /// `Restore All Data` String get recoveryAll { return Intl.message( 'Restore All Data', name: 'recoveryAll', desc: '', args: [], ); } /// `Restore Successful` String get recoverySuccess { return Intl.message( 'Restore Successful', name: 'recoverySuccess', desc: '', args: [], ); } /// `Backup Successful` String get backupSuccess { return Intl.message( 'Backup Successful', name: 'backupSuccess', desc: '', args: [], ); } /// `No Info` String get noInfo { return Intl.message('No Info', name: 'noInfo', desc: '', args: []); } /// `Please bind WebDAV` String get pleaseBindWebDAV { return Intl.message( 'Please bind WebDAV', name: 'pleaseBindWebDAV', desc: '', args: [], ); } /// `Bind` String get bind { return Intl.message('Bind', name: 'bind', desc: '', args: []); } /// `Connectivity:` String get connectivity { return Intl.message( 'Connectivity:', name: 'connectivity', desc: '', args: [], ); } /// `WebDAV Configuration` String get webDAVConfiguration { return Intl.message( 'WebDAV Configuration', name: 'webDAVConfiguration', desc: '', args: [], ); } /// `Address` String get address { return Intl.message('Address', name: 'address', desc: '', args: []); } /// `WebDAV server address` String get addressHelp { return Intl.message( 'WebDAV server address', name: 'addressHelp', desc: '', args: [], ); } /// `Please enter a valid WebDAV address` String get addressTip { return Intl.message( 'Please enter a valid WebDAV address', name: 'addressTip', desc: '', args: [], ); } /// `Password` String get password { return Intl.message('Password', name: 'password', desc: '', args: []); } /// `Check for Updates` String get checkUpdate { return Intl.message( 'Check for Updates', name: 'checkUpdate', desc: '', args: [], ); } /// `New Version Available` String get discoverNewVersion { return Intl.message( 'New Version Available', name: 'discoverNewVersion', desc: '', args: [], ); } /// `Already on the latest version` String get checkUpdateError { return Intl.message( 'Already on the latest version', name: 'checkUpdateError', desc: '', args: [], ); } /// `Download Now` String get goDownload { return Intl.message('Download Now', name: 'goDownload', desc: '', args: []); } /// `Unknown` String get unknown { return Intl.message('Unknown', name: 'unknown', desc: '', args: []); } /// `GeoData` String get geoData { return Intl.message('GeoData', name: 'geoData', desc: '', args: []); } /// `External Resources` String get externalResources { return Intl.message( 'External Resources', name: 'externalResources', desc: '', args: [], ); } /// `Checking...` String get checking { return Intl.message('Checking...', name: 'checking', desc: '', args: []); } /// `Country` String get country { return Intl.message('Country', name: 'country', desc: '', args: []); } /// `Check Failed` String get checkError { return Intl.message('Check Failed', name: 'checkError', desc: '', args: []); } /// `Search` String get search { return Intl.message('Search', name: 'search', desc: '', args: []); } /// `Allow Bypassing VPN` String get allowBypass { return Intl.message( 'Allow Bypassing VPN', name: 'allowBypass', desc: '', args: [], ); } /// `Allow specific apps to bypass VPN` String get allowBypassDesc { return Intl.message( 'Allow specific apps to bypass VPN', name: 'allowBypassDesc', desc: '', args: [], ); } /// `External Controller` String get externalController { return Intl.message( 'External Controller', name: 'externalController', desc: '', args: [], ); } /// `Control core via online port` String get externalControllerDesc { return Intl.message( 'Control core via online port', name: 'externalControllerDesc', desc: '', args: [], ); } /// `Control Secret` String get controlSecret { return Intl.message( 'Control Secret', name: 'controlSecret', desc: '', args: [], ); } /// `RESTful API access password` String get controlSecretDesc { return Intl.message( 'RESTful API access password', name: 'controlSecretDesc', desc: '', args: [], ); } /// `Generate` String get generateSecret { return Intl.message('Generate', name: 'generateSecret', desc: '', args: []); } /// `Secret copied to clipboard` String get secretCopied { return Intl.message( 'Secret copied to clipboard', name: 'secretCopied', desc: '', args: [], ); } /// `Enable IPv6 traffic routing` String get ipv6Desc { return Intl.message( 'Enable IPv6 traffic routing', name: 'ipv6Desc', desc: '', args: [], ); } /// `App` String get app { return Intl.message('App', name: 'app', desc: '', args: []); } /// `General` String get general { return Intl.message('General', name: 'general', desc: '', args: []); } /// `Attach HTTP proxy to VpnService` String get vpnSystemProxyDesc { return Intl.message( 'Attach HTTP proxy to VpnService', name: 'vpnSystemProxyDesc', desc: '', args: [], ); } /// `Set system proxy` String get systemProxyDesc { return Intl.message( 'Set system proxy', name: 'systemProxyDesc', desc: '', args: [], ); } /// `Unified Delay` String get unifiedDelay { return Intl.message( 'Unified Delay', name: 'unifiedDelay', desc: '', args: [], ); } /// `Exclude handshake delays from testing` String get unifiedDelayDesc { return Intl.message( 'Exclude handshake delays from testing', name: 'unifiedDelayDesc', desc: '', args: [], ); } /// `TCP Concurrent` String get tcpConcurrent { return Intl.message( 'TCP Concurrent', name: 'tcpConcurrent', desc: '', args: [], ); } /// `Allow concurrent TCP connections` String get tcpConcurrentDesc { return Intl.message( 'Allow concurrent TCP connections', name: 'tcpConcurrentDesc', desc: '', args: [], ); } /// `GEO Low Memory` String get geodataLoader { return Intl.message( 'GEO Low Memory', name: 'geodataLoader', desc: '', args: [], ); } /// `Use GEO low memory loader` String get geodataLoaderDesc { return Intl.message( 'Use GEO low memory loader', name: 'geodataLoaderDesc', desc: '', args: [], ); } /// `Requests` String get requests { return Intl.message('Requests', name: 'requests', desc: '', args: []); } /// `View recent request logs` String get requestsDesc { return Intl.message( 'View recent request logs', name: 'requestsDesc', desc: '', args: [], ); } /// `Find Process` String get findProcessMode { return Intl.message( 'Find Process', name: 'findProcessMode', desc: '', args: [], ); } /// `Init` String get init { return Intl.message('Init', name: 'init', desc: '', args: []); } /// `Never Expires` String get infiniteTime { return Intl.message( 'Never Expires', name: 'infiniteTime', desc: '', args: [], ); } /// `Expiration Time` String get expirationTime { return Intl.message( 'Expiration Time', name: 'expirationTime', desc: '', args: [], ); } /// `Connections` String get connections { return Intl.message('Connections', name: 'connections', desc: '', args: []); } /// `View active connections` String get connectionsDesc { return Intl.message( 'View active connections', name: 'connectionsDesc', desc: '', args: [], ); } /// `Intranet IP` String get intranetIP { return Intl.message('Intranet IP', name: 'intranetIP', desc: '', args: []); } /// `View` String get view { return Intl.message('View', name: 'view', desc: '', args: []); } /// `Cut` String get cut { return Intl.message('Cut', name: 'cut', desc: '', args: []); } /// `Copy` String get copy { return Intl.message('Copy', name: 'copy', desc: '', args: []); } /// `Paste` String get paste { return Intl.message('Paste', name: 'paste', desc: '', args: []); } /// `Test URL` String get testUrl { return Intl.message('Test URL', name: 'testUrl', desc: '', args: []); } /// `Start Test` String get startTest { return Intl.message('Start Test', name: 'startTest', desc: '', args: []); } /// `Add Profile` String get addProfile { return Intl.message('Add Profile', name: 'addProfile', desc: '', args: []); } /// `Custom URL` String get customUrl { return Intl.message('Custom URL', name: 'customUrl', desc: '', args: []); } /// `Sync` String get sync { return Intl.message('Sync', name: 'sync', desc: '', args: []); } /// `Hide from Recents` String get exclude { return Intl.message( 'Hide from Recents', name: 'exclude', desc: '', args: [], ); } /// `Hide app from recent tasks list` String get excludeDesc { return Intl.message( 'Hide app from recent tasks list', name: 'excludeDesc', desc: '', args: [], ); } /// `1 Column` String get oneColumn { return Intl.message('1 Column', name: 'oneColumn', desc: '', args: []); } /// `2 Columns` String get twoColumns { return Intl.message('2 Columns', name: 'twoColumns', desc: '', args: []); } /// `3 Columns` String get threeColumns { return Intl.message('3 Columns', name: 'threeColumns', desc: '', args: []); } /// `4 Columns` String get fourColumns { return Intl.message('4 Columns', name: 'fourColumns', desc: '', args: []); } /// `Standard` String get expand { return Intl.message('Standard', name: 'expand', desc: '', args: []); } /// `Compact` String get shrink { return Intl.message('Compact', name: 'shrink', desc: '', args: []); } /// `Min` String get min { return Intl.message('Min', name: 'min', desc: '', args: []); } /// `Tab` String get tab { return Intl.message('Tab', name: 'tab', desc: '', args: []); } /// `List` String get list { return Intl.message('List', name: 'list', desc: '', args: []); } /// `Delay` String get delay { return Intl.message('Delay', name: 'delay', desc: '', args: []); } /// `Style` String get style { return Intl.message('Style', name: 'style', desc: '', args: []); } /// `Size` String get size { return Intl.message('Size', name: 'size', desc: '', args: []); } /// `Delay Animation` String get delayAnimation { return Intl.message( 'Delay Animation', name: 'delayAnimation', desc: '', args: [], ); } /// `Customize animation during delay testing` String get delayAnimationDesc { return Intl.message( 'Customize animation during delay testing', name: 'delayAnimationDesc', desc: '', args: [], ); } /// `Default` String get noAnimation { return Intl.message('Default', name: 'noAnimation', desc: '', args: []); } /// `Rotating Circle` String get rotatingCircle { return Intl.message( 'Rotating Circle', name: 'rotatingCircle', desc: '', args: [], ); } /// `Pulse` String get pulse { return Intl.message('Pulse', name: 'pulse', desc: '', args: []); } /// `Spinning Lines` String get spinningLines { return Intl.message( 'Spinning Lines', name: 'spinningLines', desc: '', args: [], ); } /// `Three In Out` String get threeInOut { return Intl.message('Three In Out', name: 'threeInOut', desc: '', args: []); } /// `Three Bounce` String get threeBounce { return Intl.message( 'Three Bounce', name: 'threeBounce', desc: '', args: [], ); } /// `Circle` String get circle { return Intl.message('Circle', name: 'circle', desc: '', args: []); } /// `Fading Circle` String get fadingCircle { return Intl.message( 'Fading Circle', name: 'fadingCircle', desc: '', args: [], ); } /// `Fading Four` String get fadingFour { return Intl.message('Fading Four', name: 'fadingFour', desc: '', args: []); } /// `Wave` String get wave { return Intl.message('Wave', name: 'wave', desc: '', args: []); } /// `Double Bounce` String get doubleBounce { return Intl.message( 'Double Bounce', name: 'doubleBounce', desc: '', args: [], ); } /// `Sort` String get sort { return Intl.message('Sort', name: 'sort', desc: '', args: []); } /// `Columns` String get columns { return Intl.message('Columns', name: 'columns', desc: '', args: []); } /// `Proxy Settings` String get proxiesSetting { return Intl.message( 'Proxy Settings', name: 'proxiesSetting', desc: '', args: [], ); } /// `Proxy Group` String get proxyGroup { return Intl.message('Proxy Group', name: 'proxyGroup', desc: '', args: []); } /// `Go` String get go { return Intl.message('Go', name: 'go', desc: '', args: []); } /// `External Link` String get externalLink { return Intl.message( 'External Link', name: 'externalLink', desc: '', args: [], ); } /// `Other Contributors` String get otherContributors { return Intl.message( 'Other Contributors', name: 'otherContributors', desc: '', args: [], ); } /// `Auto-Close Connections` String get autoCloseConnections { return Intl.message( 'Auto-Close Connections', name: 'autoCloseConnections', desc: '', args: [], ); } /// `Close connections when switching nodes` String get autoCloseConnectionsDesc { return Intl.message( 'Close connections when switching nodes', name: 'autoCloseConnectionsDesc', desc: '', args: [], ); } /// `Proxy Traffic Only` String get onlyStatisticsProxy { return Intl.message( 'Proxy Traffic Only', name: 'onlyStatisticsProxy', desc: '', args: [], ); } /// `Only record proxy traffic` String get onlyStatisticsProxyDesc { return Intl.message( 'Only record proxy traffic', name: 'onlyStatisticsProxyDesc', desc: '', args: [], ); } /// `Pure Black Mode` String get pureBlackMode { return Intl.message( 'Pure Black Mode', name: 'pureBlackMode', desc: '', args: [], ); } /// `TCP keep-alive interval` String get keepAliveIntervalDesc { return Intl.message( 'TCP keep-alive interval', name: 'keepAliveIntervalDesc', desc: '', args: [], ); } /// ` entries` String get entries { return Intl.message(' entries', name: 'entries', desc: '', args: []); } /// `Local` String get local { return Intl.message('Local', name: 'local', desc: '', args: []); } /// `Remote` String get remote { return Intl.message('Remote', name: 'remote', desc: '', args: []); } /// `Backup data to WebDAV` String get remoteBackupDesc { return Intl.message( 'Backup data to WebDAV', name: 'remoteBackupDesc', desc: '', args: [], ); } /// `Restore data from WebDAV` String get remoteRecoveryDesc { return Intl.message( 'Restore data from WebDAV', name: 'remoteRecoveryDesc', desc: '', args: [], ); } /// `Backup data locally` String get localBackupDesc { return Intl.message( 'Backup data locally', name: 'localBackupDesc', desc: '', args: [], ); } /// `Restore data from file` String get localRecoveryDesc { return Intl.message( 'Restore data from file', name: 'localRecoveryDesc', desc: '', args: [], ); } /// `Mode` String get mode { return Intl.message('Mode', name: 'mode', desc: '', args: []); } /// `Time` String get time { return Intl.message('Time', name: 'time', desc: '', args: []); } /// `Source` String get source { return Intl.message('Source', name: 'source', desc: '', args: []); } /// `All Apps` String get allApps { return Intl.message('All Apps', name: 'allApps', desc: '', args: []); } /// `Third-Party Apps Only` String get onlyOtherApps { return Intl.message( 'Third-Party Apps Only', name: 'onlyOtherApps', desc: '', args: [], ); } /// `Action` String get action { return Intl.message('Action', name: 'action', desc: '', args: []); } /// `Smart Select` String get intelligentSelected { return Intl.message( 'Smart Select', name: 'intelligentSelected', desc: '', args: [], ); } /// `Import from Clipboard` String get clipboardImport { return Intl.message( 'Import from Clipboard', name: 'clipboardImport', desc: '', args: [], ); } /// `Export to Clipboard` String get clipboardExport { return Intl.message( 'Export to Clipboard', name: 'clipboardExport', desc: '', args: [], ); } /// `Layout` String get layout { return Intl.message('Layout', name: 'layout', desc: '', args: []); } /// `Compact` String get tight { return Intl.message('Compact', name: 'tight', desc: '', args: []); } /// `Standard` String get standard { return Intl.message('Standard', name: 'standard', desc: '', args: []); } /// `Loose` String get loose { return Intl.message('Loose', name: 'loose', desc: '', args: []); } /// `Profile Sorting` String get profilesSort { return Intl.message( 'Profile Sorting', name: 'profilesSort', desc: '', args: [], ); } /// `Start` String get start { return Intl.message('Start', name: 'start', desc: '', args: []); } /// `Stop` String get stop { return Intl.message('Stop', name: 'stop', desc: '', args: []); } /// `Power Switch` String get powerSwitch { return Intl.message( 'Power Switch', name: 'powerSwitch', desc: '', args: [], ); } /// `Uptime` String get runTime { return Intl.message('Uptime', name: 'runTime', desc: '', args: []); } /// `Please add a profile first` String get checkOrAddProfile { return Intl.message( 'Please add a profile first', name: 'checkOrAddProfile', desc: '', args: [], ); } /// `Service Ready` String get serviceReady { return Intl.message( 'Service Ready', name: 'serviceReady', desc: '', args: [], ); } /// `App-related settings` String get appDesc { return Intl.message( 'App-related settings', name: 'appDesc', desc: '', args: [], ); } /// `VPN-related settings` String get vpnDesc { return Intl.message( 'VPN-related settings', name: 'vpnDesc', desc: '', args: [], ); } /// `DNS-related settings` String get dnsDesc { return Intl.message( 'DNS-related settings', name: 'dnsDesc', desc: '', args: [], ); } /// `Key` String get key { return Intl.message('Key', name: 'key', desc: '', args: []); } /// `Value` String get value { return Intl.message('Value', name: 'value', desc: '', args: []); } /// `Append hosts to current config` String get hostsDesc { return Intl.message( 'Append hosts to current config', name: 'hostsDesc', desc: '', args: [], ); } /// `Restart VPN to apply changes` String get vpnTip { return Intl.message( 'Restart VPN to apply changes', name: 'vpnTip', desc: '', args: [], ); } /// `Route all system traffic via VpnService` String get vpnEnableDesc { return Intl.message( 'Route all system traffic via VpnService', name: 'vpnEnableDesc', desc: '', args: [], ); } /// `Options` String get options { return Intl.message('Options', name: 'options', desc: '', args: []); } /// `Loopback Unlock` String get loopback { return Intl.message( 'Loopback Unlock', name: 'loopback', desc: '', args: [], ); } /// `UWP loopback unlocking tool` String get loopbackDesc { return Intl.message( 'UWP loopback unlocking tool', name: 'loopbackDesc', desc: '', args: [], ); } /// `Providers` String get providers { return Intl.message('Providers', name: 'providers', desc: '', args: []); } /// `Proxy Providers` String get proxyProviders { return Intl.message( 'Proxy Providers', name: 'proxyProviders', desc: '', args: [], ); } /// `Rule Providers` String get ruleProviders { return Intl.message( 'Rule Providers', name: 'ruleProviders', desc: '', args: [], ); } /// `Advanced Settings` String get advancedSettings { return Intl.message( 'Advanced Settings', name: 'advancedSettings', desc: '', args: [], ); } /// `Node Exclusion` String get nodeExclusion { return Intl.message( 'Node Exclusion', name: 'nodeExclusion', desc: '', args: [], ); } /// `Exclude all matched nodes` String get nodeExclusionDesc { return Intl.message( 'Exclude all matched nodes', name: 'nodeExclusionDesc', desc: '', args: [], ); } /// `HK|Hong Kong|🇭🇰` String get nodeExclusionPlaceholder { return Intl.message( 'HK|Hong Kong|🇭🇰', name: 'nodeExclusionPlaceholder', desc: '', args: [], ); } /// `Please check the format` String get formatError { return Intl.message( 'Please check the format', name: 'formatError', desc: '', args: [], ); } /// `Timeout` String get healthCheckTimeout { return Intl.message( 'Timeout', name: 'healthCheckTimeout', desc: '', args: [], ); } /// `Node health check timeout` String get healthCheckTimeoutDesc { return Intl.message( 'Node health check timeout', name: 'healthCheckTimeoutDesc', desc: '', args: [], ); } /// `Concurrency Limit` String get concurrencyLimit { return Intl.message( 'Concurrency Limit', name: 'concurrencyLimit', desc: '', args: [], ); } /// `Maximum concurrent delay tests` String get concurrencyLimitDesc { return Intl.message( 'Maximum concurrent delay tests', name: 'concurrencyLimitDesc', desc: '', args: [], ); } /// `Not Recommended` String get notRecommended { return Intl.message( 'Not Recommended', name: 'notRecommended', desc: '', args: [], ); } /// `Override DNS` String get overrideDns { return Intl.message( 'Override DNS', name: 'overrideDns', desc: '', args: [], ); } /// `Override profile's DNS settings` String get overrideDnsDesc { return Intl.message( 'Override profile\'s DNS settings', name: 'overrideDnsDesc', desc: '', args: [], ); } /// `Override Config` String get overrideTestUrl { return Intl.message( 'Override Config', name: 'overrideTestUrl', desc: '', args: [], ); } /// `NTP` String get ntp { return Intl.message('NTP', name: 'ntp', desc: '', args: []); } /// `Use NTP time service` String get ntpDesc { return Intl.message( 'Use NTP time service', name: 'ntpDesc', desc: '', args: [], ); } /// `Override NTP` String get overrideNtp { return Intl.message( 'Override NTP', name: 'overrideNtp', desc: '', args: [], ); } /// `Override profile's NTP settings` String get overrideNtpDesc { return Intl.message( 'Override profile\'s NTP settings', name: 'overrideNtpDesc', desc: '', args: [], ); } /// `Status` String get ntpStatus { return Intl.message('Status', name: 'ntpStatus', desc: '', args: []); } /// `Enable NTP time service` String get ntpStatusDesc { return Intl.message( 'Enable NTP time service', name: 'ntpStatusDesc', desc: '', args: [], ); } /// `Write to System` String get writeToSystem { return Intl.message( 'Write to System', name: 'writeToSystem', desc: '', args: [], ); } /// `Requires administrator privileges` String get writeToSystemDesc { return Intl.message( 'Requires administrator privileges', name: 'writeToSystemDesc', desc: '', args: [], ); } /// `Server` String get ntpServer { return Intl.message('Server', name: 'ntpServer', desc: '', args: []); } /// `Port` String get ntpPort { return Intl.message('Port', name: 'ntpPort', desc: '', args: []); } /// `Update Interval` String get ntpInterval { return Intl.message( 'Update Interval', name: 'ntpInterval', desc: '', args: [], ); } /// `Sniffer` String get sniffer { return Intl.message('Sniffer', name: 'sniffer', desc: '', args: []); } /// `Modify domain sniffer config` String get snifferDesc { return Intl.message( 'Modify domain sniffer config', name: 'snifferDesc', desc: '', args: [], ); } /// `Override Sniffer` String get overrideSniffer { return Intl.message( 'Override Sniffer', name: 'overrideSniffer', desc: '', args: [], ); } /// `Override profile's Sniffer settings` String get overrideSnifferDesc { return Intl.message( 'Override profile\'s Sniffer settings', name: 'overrideSnifferDesc', desc: '', args: [], ); } /// `Status` String get snifferStatus { return Intl.message('Status', name: 'snifferStatus', desc: '', args: []); } /// `Enable Sniffer service` String get snifferStatusDesc { return Intl.message( 'Enable Sniffer service', name: 'snifferStatusDesc', desc: '', args: [], ); } /// `Force DNS Mapping` String get forceDnsMapping { return Intl.message( 'Force DNS Mapping', name: 'forceDnsMapping', desc: '', args: [], ); } /// `Force mapping DNS results to connections` String get forceDnsMappingDesc { return Intl.message( 'Force mapping DNS results to connections', name: 'forceDnsMappingDesc', desc: '', args: [], ); } /// `Parse Pure IP` String get parsePureIp { return Intl.message( 'Parse Pure IP', name: 'parsePureIp', desc: '', args: [], ); } /// `Parse pure IP connections` String get parsePureIpDesc { return Intl.message( 'Parse pure IP connections', name: 'parsePureIpDesc', desc: '', args: [], ); } /// `Override Destination` String get overrideDestination { return Intl.message( 'Override Destination', name: 'overrideDestination', desc: '', args: [], ); } /// `Override destination with sniffed result` String get overrideDestinationDesc { return Intl.message( 'Override destination with sniffed result', name: 'overrideDestinationDesc', desc: '', args: [], ); } /// `HTTP Port Sniffing` String get httpPortSniffer { return Intl.message( 'HTTP Port Sniffing', name: 'httpPortSniffer', desc: '', args: [], ); } /// `TLS Port Sniffing` String get tlsPortSniffer { return Intl.message( 'TLS Port Sniffing', name: 'tlsPortSniffer', desc: '', args: [], ); } /// `QUIC Port Sniffing` String get quicPortSniffer { return Intl.message( 'QUIC Port Sniffing', name: 'quicPortSniffer', desc: '', args: [], ); } /// `Force Sniff Domain` String get forceDomain { return Intl.message( 'Force Sniff Domain', name: 'forceDomain', desc: '', args: [], ); } /// `Skip Domain` String get skipDomain { return Intl.message('Skip Domain', name: 'skipDomain', desc: '', args: []); } /// `Skip Source IP` String get skipSrcAddress { return Intl.message( 'Skip Source IP', name: 'skipSrcAddress', desc: '', args: [], ); } /// `Skip Destination IP` String get skipDstAddress { return Intl.message( 'Skip Destination IP', name: 'skipDstAddress', desc: '', args: [], ); } /// `Ports` String get snifferPorts { return Intl.message('Ports', name: 'snifferPorts', desc: '', args: []); } /// `e.g.: 80, 8080-8880` String get snifferPortsHint { return Intl.message( 'e.g.: 80, 8080-8880', name: 'snifferPortsHint', desc: '', args: [], ); } /// `One domain per line` String get snifferDomainHint { return Intl.message( 'One domain per line', name: 'snifferDomainHint', desc: '', args: [], ); } /// `One address per line` String get snifferAddressHint { return Intl.message( 'One address per line', name: 'snifferAddressHint', desc: '', args: [], ); } /// `Tunnel` String get tunnel { return Intl.message('Tunnel', name: 'tunnel', desc: '', args: []); } /// `Use traffic forwarding tunnel` String get tunnelDesc { return Intl.message( 'Use traffic forwarding tunnel', name: 'tunnelDesc', desc: '', args: [], ); } /// `Override Tunnel` String get overrideTunnel { return Intl.message( 'Override Tunnel', name: 'overrideTunnel', desc: '', args: [], ); } /// `Override profile's Tunnel settings` String get overrideTunnelDesc { return Intl.message( 'Override profile\'s Tunnel settings', name: 'overrideTunnelDesc', desc: '', args: [], ); } /// `Forwarding List` String get tunnelList { return Intl.message( 'Forwarding List', name: 'tunnelList', desc: '', args: [], ); } /// `Add Forwarding` String get addTunnel { return Intl.message( 'Add Forwarding', name: 'addTunnel', desc: '', args: [], ); } /// `Edit Forwarding` String get editTunnel { return Intl.message( 'Edit Forwarding', name: 'editTunnel', desc: '', args: [], ); } /// `Delete Forwarding` String get deleteTunnel { return Intl.message( 'Delete Forwarding', name: 'deleteTunnel', desc: '', args: [], ); } /// `Network Protocol` String get tunnelNetwork { return Intl.message( 'Network Protocol', name: 'tunnelNetwork', desc: '', args: [], ); } /// `e.g.: tcp, udp` String get tunnelNetworkHint { return Intl.message( 'e.g.: tcp, udp', name: 'tunnelNetworkHint', desc: '', args: [], ); } /// `Listen Address` String get tunnelAddress { return Intl.message( 'Listen Address', name: 'tunnelAddress', desc: '', args: [], ); } /// `e.g.: 127.0.0.1:6553` String get tunnelAddressHint { return Intl.message( 'e.g.: 127.0.0.1:6553', name: 'tunnelAddressHint', desc: '', args: [], ); } /// `Target Address` String get tunnelTarget { return Intl.message( 'Target Address', name: 'tunnelTarget', desc: '', args: [], ); } /// `e.g.: 114.114.114.114:53` String get tunnelTargetHint { return Intl.message( 'e.g.: 114.114.114.114:53', name: 'tunnelTargetHint', desc: '', args: [], ); } /// `Proxy Name` String get tunnelProxy { return Intl.message('Proxy Name', name: 'tunnelProxy', desc: '', args: []); } /// `e.g.: proxy (optional)` String get tunnelProxyHint { return Intl.message( 'e.g.: proxy (optional)', name: 'tunnelProxyHint', desc: '', args: [], ); } /// `Experimental` String get experimental { return Intl.message( 'Experimental', name: 'experimental', desc: '', args: [], ); } /// `Use with caution` String get experimentalDesc { return Intl.message( 'Use with caution', name: 'experimentalDesc', desc: '', args: [], ); } /// `Override Experimental` String get overrideExperimental { return Intl.message( 'Override Experimental', name: 'overrideExperimental', desc: '', args: [], ); } /// `Override profile's Experimental settings` String get overrideExperimentalDesc { return Intl.message( 'Override profile\'s Experimental settings', name: 'overrideExperimentalDesc', desc: '', args: [], ); } /// `Disable QUIC GSO` String get quicGoDisableGso { return Intl.message( 'Disable QUIC GSO', name: 'quicGoDisableGso', desc: '', args: [], ); } /// `Disable QUIC Generic Segmentation Offload` String get quicGoDisableGsoDesc { return Intl.message( 'Disable QUIC Generic Segmentation Offload', name: 'quicGoDisableGsoDesc', desc: '', args: [], ); } /// `Disable QUIC ECN` String get quicGoDisableEcn { return Intl.message( 'Disable QUIC ECN', name: 'quicGoDisableEcn', desc: '', args: [], ); } /// `Disable QUIC Explicit Congestion Notification` String get quicGoDisableEcnDesc { return Intl.message( 'Disable QUIC Explicit Congestion Notification', name: 'quicGoDisableEcnDesc', desc: '', args: [], ); } /// `Enable Dialer IP4P Conversion` String get dialerIp4pConvert { return Intl.message( 'Enable Dialer IP4P Conversion', name: 'dialerIp4pConvert', desc: '', args: [], ); } /// `Enable dialer IP4P address conversion feature` String get dialerIp4pConvertDesc { return Intl.message( 'Enable dialer IP4P address conversion feature', name: 'dialerIp4pConvertDesc', desc: '', args: [], ); } /// `Status` String get status { return Intl.message('Status', name: 'status', desc: '', args: []); } /// `Uses system DNS when disabled` String get statusDesc { return Intl.message( 'Uses system DNS when disabled', name: 'statusDesc', desc: '', args: [], ); } /// `Prioritize DoH HTTP/3` String get preferH3Desc { return Intl.message( 'Prioritize DoH HTTP/3', name: 'preferH3Desc', desc: '', args: [], ); } /// `Cache Algorithm` String get cacheAlgorithm { return Intl.message( 'Cache Algorithm', name: 'cacheAlgorithm', desc: '', args: [], ); } /// `Respect Rules` String get respectRules { return Intl.message( 'Respect Rules', name: 'respectRules', desc: '', args: [], ); } /// `DNS connections follow Rules` String get respectRulesDesc { return Intl.message( 'DNS connections follow Rules', name: 'respectRulesDesc', desc: '', args: [], ); } /// `DNS Mode` String get dnsMode { return Intl.message('DNS Mode', name: 'dnsMode', desc: '', args: []); } /// `FakeIP Range` String get fakeipRange { return Intl.message( 'FakeIP Range', name: 'fakeipRange', desc: '', args: [], ); } /// `FakeIPv6 Range` String get fakeipRangeV6 { return Intl.message( 'FakeIPv6 Range', name: 'fakeipRangeV6', desc: '', args: [], ); } /// `FakeIP Filter Mode` String get fakeIpFilterMode { return Intl.message( 'FakeIP Filter Mode', name: 'fakeIpFilterMode', desc: '', args: [], ); } /// `Specify FakeIP filter mode` String get fakeIpFilterModeDesc { return Intl.message( 'Specify FakeIP filter mode', name: 'fakeIpFilterModeDesc', desc: '', args: [], ); } /// `Blacklist` String get blacklist { return Intl.message('Blacklist', name: 'blacklist', desc: '', args: []); } /// `Whitelist` String get whitelist { return Intl.message('Whitelist', name: 'whitelist', desc: '', args: []); } /// `FakeIP Filter` String get fakeipFilter { return Intl.message( 'FakeIP Filter', name: 'fakeipFilter', desc: '', args: [], ); } /// `FakeIP TTL` String get fakeipTtl { return Intl.message('FakeIP TTL', name: 'fakeipTtl', desc: '', args: []); } /// `Default Nameserver` String get defaultNameserver { return Intl.message( 'Default Nameserver', name: 'defaultNameserver', desc: '', args: [], ); } /// `Used to resolve DNS servers` String get defaultNameserverDesc { return Intl.message( 'Used to resolve DNS servers', name: 'defaultNameserverDesc', desc: '', args: [], ); } /// `Nameserver` String get nameserver { return Intl.message('Nameserver', name: 'nameserver', desc: '', args: []); } /// `Used to resolve domains` String get nameserverDesc { return Intl.message( 'Used to resolve domains', name: 'nameserverDesc', desc: '', args: [], ); } /// `Use Hosts` String get useHosts { return Intl.message('Use Hosts', name: 'useHosts', desc: '', args: []); } /// `Use System Hosts` String get useSystemHosts { return Intl.message( 'Use System Hosts', name: 'useSystemHosts', desc: '', args: [], ); } /// `Nameserver Policy` String get nameserverPolicy { return Intl.message( 'Nameserver Policy', name: 'nameserverPolicy', desc: '', args: [], ); } /// `Specify domain-specific nameservers` String get nameserverPolicyDesc { return Intl.message( 'Specify domain-specific nameservers', name: 'nameserverPolicyDesc', desc: '', args: [], ); } /// `Proxy Nameserver` String get proxyNameserver { return Intl.message( 'Proxy Nameserver', name: 'proxyNameserver', desc: '', args: [], ); } /// `Used to resolve proxy nodes` String get proxyNameserverDesc { return Intl.message( 'Used to resolve proxy nodes', name: 'proxyNameserverDesc', desc: '', args: [], ); } /// `Direct Nameserver` String get directNameserver { return Intl.message( 'Direct Nameserver', name: 'directNameserver', desc: '', args: [], ); } /// `Used to resolve direct domains` String get directNameserverDesc { return Intl.message( 'Used to resolve direct domains', name: 'directNameserverDesc', desc: '', args: [], ); } /// `Direct DNS Follows Policy` String get directNameserverFollowPolicy { return Intl.message( 'Direct DNS Follows Policy', name: 'directNameserverFollowPolicy', desc: '', args: [], ); } /// `Fallback` String get fallback { return Intl.message('Fallback', name: 'fallback', desc: '', args: []); } /// `Usually offshore DNS` String get fallbackDesc { return Intl.message( 'Usually offshore DNS', name: 'fallbackDesc', desc: '', args: [], ); } /// `Fallback Filter` String get fallbackFilter { return Intl.message( 'Fallback Filter', name: 'fallbackFilter', desc: '', args: [], ); } /// `GeoIP Code` String get geoipCode { return Intl.message('GeoIP Code', name: 'geoipCode', desc: '', args: []); } /// `IP/CIDR` String get ipcidr { return Intl.message('IP/CIDR', name: 'ipcidr', desc: '', args: []); } /// `Domain` String get domain { return Intl.message('Domain', name: 'domain', desc: '', args: []); } /// `Reset` String get reset { return Intl.message('Reset', name: 'reset', desc: '', args: []); } /// `Show/Hide` String get action_view { return Intl.message('Show/Hide', name: 'action_view', desc: '', args: []); } /// `Start/Stop` String get action_start { return Intl.message('Start/Stop', name: 'action_start', desc: '', args: []); } /// `Switch Mode` String get action_mode { return Intl.message('Switch Mode', name: 'action_mode', desc: '', args: []); } /// `System Proxy` String get action_proxy { return Intl.message( 'System Proxy', name: 'action_proxy', desc: '', args: [], ); } /// `TUN` String get action_tun { return Intl.message('TUN', name: 'action_tun', desc: '', args: []); } /// `Disclaimer` String get disclaimer { return Intl.message('Disclaimer', name: 'disclaimer', desc: '', args: []); } /// `This free open-source software is for non-commercial learning and personal use only. Proxy services are independent of this software. By agreeing, you acknowledge this; otherwise, please exit.` String get disclaimerDesc { return Intl.message( 'This free open-source software is for non-commercial learning and personal use only. Proxy services are independent of this software. By agreeing, you acknowledge this; otherwise, please exit.', name: 'disclaimerDesc', desc: '', args: [], ); } /// `Agree` String get agree { return Intl.message('Agree', name: 'agree', desc: '', args: []); } /// `Hotkey Management` String get hotkeyManagement { return Intl.message( 'Hotkey Management', name: 'hotkeyManagement', desc: '', args: [], ); } /// `Control app via keyboard` String get hotkeyManagementDesc { return Intl.message( 'Control app via keyboard', name: 'hotkeyManagementDesc', desc: '', args: [], ); } /// `Press a key` String get pressKeyboard { return Intl.message( 'Press a key', name: 'pressKeyboard', desc: '', args: [], ); } /// `Enter a valid hotkey` String get inputCorrectHotkey { return Intl.message( 'Enter a valid hotkey', name: 'inputCorrectHotkey', desc: '', args: [], ); } /// `Hotkey Conflict` String get hotkeyConflict { return Intl.message( 'Hotkey Conflict', name: 'hotkeyConflict', desc: '', args: [], ); } /// `Remove` String get remove { return Intl.message('Remove', name: 'remove', desc: '', args: []); } /// `No Hotkeys` String get noHotKey { return Intl.message('No Hotkeys', name: 'noHotKey', desc: '', args: []); } /// `No Network` String get noNetwork { return Intl.message('No Network', name: 'noNetwork', desc: '', args: []); } /// `Allow IPv6 inbound` String get ipv6InboundDesc { return Intl.message( 'Allow IPv6 inbound', name: 'ipv6InboundDesc', desc: '', args: [], ); } /// `Export Logs` String get exportLogs { return Intl.message('Export Logs', name: 'exportLogs', desc: '', args: []); } /// `Export Successful` String get exportSuccess { return Intl.message( 'Export Successful', name: 'exportSuccess', desc: '', args: [], ); } /// `Icon Style` String get iconStyle { return Intl.message('Icon Style', name: 'iconStyle', desc: '', args: []); } /// `Icon Only` String get onlyIcon { return Intl.message('Icon Only', name: 'onlyIcon', desc: '', args: []); } /// `No Icon` String get noIcon { return Intl.message('No Icon', name: 'noIcon', desc: '', args: []); } /// `Stack Mode` String get stackMode { return Intl.message('Stack Mode', name: 'stackMode', desc: '', args: []); } /// `Strict Route` String get strictRoute { return Intl.message( 'Strict Route', name: 'strictRoute', desc: '', args: [], ); } /// `Use TUN strict routing mode` String get strictRouteDesc { return Intl.message( 'Use TUN strict routing mode', name: 'strictRouteDesc', desc: '', args: [], ); } /// `ICMP Forwarding` String get icmpForwarding { return Intl.message( 'ICMP Forwarding', name: 'icmpForwarding', desc: '', args: [], ); } /// `Enable ICMP Ping` String get icmpForwardingDesc { return Intl.message( 'Enable ICMP Ping', name: 'icmpForwardingDesc', desc: '', args: [], ); } /// `DNS Hijack` String get dnsHijack { return Intl.message('DNS Hijack', name: 'dnsHijack', desc: '', args: []); } /// `Redirect DNS queries to internal DNS module` String get dnsHijackDesc { return Intl.message( 'Redirect DNS queries to internal DNS module', name: 'dnsHijackDesc', desc: '', args: [], ); } /// `NAT Enhancement` String get endpointIndependentNat { return Intl.message( 'NAT Enhancement', name: 'endpointIndependentNat', desc: '', args: [], ); } /// `Enable endpoint-independent NAT` String get endpointIndependentNatDesc { return Intl.message( 'Enable endpoint-independent NAT', name: 'endpointIndependentNatDesc', desc: '', args: [], ); } /// `Network` String get network { return Intl.message('Network', name: 'network', desc: '', args: []); } /// `Modify network-related settings` String get networkDesc { return Intl.message( 'Modify network-related settings', name: 'networkDesc', desc: '', args: [], ); } /// `Bypass Domain` String get bypassDomain { return Intl.message( 'Bypass Domain', name: 'bypassDomain', desc: '', args: [], ); } /// `Active only when System Proxy is on` String get bypassDomainDesc { return Intl.message( 'Active only when System Proxy is on', name: 'bypassDomainDesc', desc: '', args: [], ); } /// `Are you sure you want to reset?` String get resetTip { return Intl.message( 'Are you sure you want to reset?', name: 'resetTip', desc: '', args: [], ); } /// `RegExp` String get regExp { return Intl.message('RegExp', name: 'regExp', desc: '', args: []); } /// `Icon` String get icon { return Intl.message('Icon', name: 'icon', desc: '', args: []); } /// `Icon Configuration` String get iconConfiguration { return Intl.message( 'Icon Configuration', name: 'iconConfiguration', desc: '', args: [], ); } /// `No Data` String get noData { return Intl.message('No Data', name: 'noData', desc: '', args: []); } /// `Admin Auto-Launch` String get adminAutoLaunch { return Intl.message( 'Admin Auto-Launch', name: 'adminAutoLaunch', desc: '', args: [], ); } /// `Auto-start with admin privileges` String get adminAutoLaunchDesc { return Intl.message( 'Auto-start with admin privileges', name: 'adminAutoLaunchDesc', desc: '', args: [], ); } /// `Font` String get fontFamily { return Intl.message('Font', name: 'fontFamily', desc: '', args: []); } /// `System Font` String get systemFont { return Intl.message('System Font', name: 'systemFont', desc: '', args: []); } /// `Toggle` String get toggle { return Intl.message('Toggle', name: 'toggle', desc: '', args: []); } /// `System` String get system { return Intl.message('System', name: 'system', desc: '', args: []); } /// `Bypass Private Network` String get bypassPrivateRoute { return Intl.message( 'Bypass Private Network', name: 'bypassPrivateRoute', desc: '', args: [], ); } /// `Automatically bypass private network IP addresses` String get bypassPrivateRouteDesc { return Intl.message( 'Automatically bypass private network IP addresses', name: 'bypassPrivateRouteDesc', desc: '', args: [], ); } /// `Please enter the admin password` String get pleaseInputAdminPassword { return Intl.message( 'Please enter the admin password', name: 'pleaseInputAdminPassword', desc: '', args: [], ); } /// `Copy Environment Variable` String get copyEnvVar { return Intl.message( 'Copy Environment Variable', name: 'copyEnvVar', desc: '', args: [], ); } /// `Memory Info` String get memoryInfo { return Intl.message('Memory Info', name: 'memoryInfo', desc: '', args: []); } /// `Cancel` String get cancel { return Intl.message('Cancel', name: 'cancel', desc: '', args: []); } /// `File modified. Save changes?` String get fileIsUpdate { return Intl.message( 'File modified. Save changes?', name: 'fileIsUpdate', desc: '', args: [], ); } /// `Profile modified. Disable auto-update?` String get profileHasUpdate { return Intl.message( 'Profile modified. Disable auto-update?', name: 'profileHasUpdate', desc: '', args: [], ); } /// `Cache modifications?` String get hasCacheChange { return Intl.message( 'Cache modifications?', name: 'hasCacheChange', desc: '', args: [], ); } /// `Copy Successful` String get copySuccess { return Intl.message( 'Copy Successful', name: 'copySuccess', desc: '', args: [], ); } /// `Success` String get success { return Intl.message('Success', name: 'success', desc: '', args: []); } /// `Copy Link` String get copyLink { return Intl.message('Copy Link', name: 'copyLink', desc: '', args: []); } /// `Export File` String get exportFile { return Intl.message('Export File', name: 'exportFile', desc: '', args: []); } /// `Cache corrupted. Clear it?` String get cacheCorrupt { return Intl.message( 'Cache corrupted. Clear it?', name: 'cacheCorrupt', desc: '', args: [], ); } /// `Third-party API result (for reference only)` String get detectionTip { return Intl.message( 'Third-party API result (for reference only)', name: 'detectionTip', desc: '', args: [], ); } /// `Toggle Display` String get ipClickBehavior { return Intl.message( 'Toggle Display', name: 'ipClickBehavior', desc: '', args: [], ); } /// `Hide IP Display` String get ipPrivacyProtection { return Intl.message( 'Hide IP Display', name: 'ipPrivacyProtection', desc: '', args: [], ); } /// `Refresh IP` String get manualRefreshIp { return Intl.message( 'Refresh IP', name: 'manualRefreshIp', desc: '', args: [], ); } /// `Please try manual refresh` String get tryManualRefresh { return Intl.message( 'Please try manual refresh', name: 'tryManualRefresh', desc: '', args: [], ); } /// `Refresh App List` String get refreshAppList { return Intl.message( 'Refresh App List', name: 'refreshAppList', desc: '', args: [], ); } /// `Refresh the app list?` String get refreshAppListConfirm { return Intl.message( 'Refresh the app list?', name: 'refreshAppListConfirm', desc: '', args: [], ); } /// `Get Domestic IP` String get switchToDomesticIp { return Intl.message( 'Get Domestic IP', name: 'switchToDomesticIp', desc: '', args: [], ); } /// `Listen` String get listen { return Intl.message('Listen', name: 'listen', desc: '', args: []); } /// `Undo` String get undo { return Intl.message('Undo', name: 'undo', desc: '', args: []); } /// `Redo` String get redo { return Intl.message('Redo', name: 'redo', desc: '', args: []); } /// `None` String get none { return Intl.message('None', name: 'none', desc: '', args: []); } /// `Core Configuration` String get basicConfig { return Intl.message( 'Core Configuration', name: 'basicConfig', desc: '', args: [], ); } /// `Global core settings` String get basicConfigDesc { return Intl.message( 'Global core settings', name: 'basicConfigDesc', desc: '', args: [], ); } /// `{count} items selected` String selectedCountTitle(Object count) { return Intl.message( '$count items selected', name: 'selectedCountTitle', desc: '', args: [count], ); } /// `Add Rule` String get addRule { return Intl.message('Add Rule', name: 'addRule', desc: '', args: []); } /// `Rule Name` String get ruleName { return Intl.message('Rule Name', name: 'ruleName', desc: '', args: []); } /// `Content` String get content { return Intl.message('Content', name: 'content', desc: '', args: []); } /// `Sub Rule` String get subRule { return Intl.message('Sub Rule', name: 'subRule', desc: '', args: []); } /// `Rule Target` String get ruleTarget { return Intl.message('Rule Target', name: 'ruleTarget', desc: '', args: []); } /// `Source IP` String get sourceIp { return Intl.message('Source IP', name: 'sourceIp', desc: '', args: []); } /// `No Resolve` String get noResolve { return Intl.message('No Resolve', name: 'noResolve', desc: '', args: []); } /// `Original Rules` String get getOriginRules { return Intl.message( 'Original Rules', name: 'getOriginRules', desc: '', args: [], ); } /// `Override Original Rules` String get overrideOriginRules { return Intl.message( 'Override Original Rules', name: 'overrideOriginRules', desc: '', args: [], ); } /// `Append to Original Rules` String get addedOriginRules { return Intl.message( 'Append to Original Rules', name: 'addedOriginRules', desc: '', args: [], ); } /// `Enable Override` String get enableOverride { return Intl.message( 'Enable Override', name: 'enableOverride', desc: '', args: [], ); } /// `Save changes?` String get saveChanges { return Intl.message( 'Save changes?', name: 'saveChanges', desc: '', args: [], ); } /// `Modify general settings` String get generalDesc { return Intl.message( 'Modify general settings', name: 'generalDesc', desc: '', args: [], ); } /// `Enable process matching` String get findProcessModeDesc { return Intl.message( 'Enable process matching', name: 'findProcessModeDesc', desc: '', args: [], ); } /// `Effective only in mobile view` String get tabAnimationDesc { return Intl.message( 'Effective only in mobile view', name: 'tabAnimationDesc', desc: '', args: [], ); } /// `Haptic Feedback` String get navBarHapticFeedback { return Intl.message( 'Haptic Feedback', name: 'navBarHapticFeedback', desc: '', args: [], ); } /// `Vibrate on navigation tab switch` String get navBarHapticFeedbackDesc { return Intl.message( 'Vibrate on navigation tab switch', name: 'navBarHapticFeedbackDesc', desc: '', args: [], ); } /// `Are you sure you want to save?` String get saveTip { return Intl.message( 'Are you sure you want to save?', name: 'saveTip', desc: '', args: [], ); } /// `Color Schemes` String get colorSchemes { return Intl.message( 'Color Schemes', name: 'colorSchemes', desc: '', args: [], ); } /// `Palette` String get palette { return Intl.message('Palette', name: 'palette', desc: '', args: []); } /// `Tonal Spot` String get tonalSpotScheme { return Intl.message( 'Tonal Spot', name: 'tonalSpotScheme', desc: '', args: [], ); } /// `Fidelity` String get fidelityScheme { return Intl.message('Fidelity', name: 'fidelityScheme', desc: '', args: []); } /// `Monochrome` String get monochromeScheme { return Intl.message( 'Monochrome', name: 'monochromeScheme', desc: '', args: [], ); } /// `Neutral` String get neutralScheme { return Intl.message('Neutral', name: 'neutralScheme', desc: '', args: []); } /// `Vibrant` String get vibrantScheme { return Intl.message('Vibrant', name: 'vibrantScheme', desc: '', args: []); } /// `Expressive` String get expressiveScheme { return Intl.message( 'Expressive', name: 'expressiveScheme', desc: '', args: [], ); } /// `Content` String get contentScheme { return Intl.message('Content', name: 'contentScheme', desc: '', args: []); } /// `Rainbow` String get rainbowScheme { return Intl.message('Rainbow', name: 'rainbowScheme', desc: '', args: []); } /// `Fruit Salad` String get fruitSaladScheme { return Intl.message( 'Fruit Salad', name: 'fruitSaladScheme', desc: '', args: [], ); } /// `Developer Mode` String get developerMode { return Intl.message( 'Developer Mode', name: 'developerMode', desc: '', args: [], ); } /// `Developer mode is enabled.` String get developerModeEnableTip { return Intl.message( 'Developer mode is enabled.', name: 'developerModeEnableTip', desc: '', args: [], ); } /// `Message Test` String get messageTest { return Intl.message( 'Message Test', name: 'messageTest', desc: '', args: [], ); } /// `This is a message.` String get messageTestTip { return Intl.message( 'This is a message.', name: 'messageTestTip', desc: '', args: [], ); } /// `Crash Test` String get crashTest { return Intl.message('Crash Test', name: 'crashTest', desc: '', args: []); } /// `Clear Data` String get clearData { return Intl.message('Clear Data', name: 'clearData', desc: '', args: []); } /// `Text Scaling` String get textScale { return Intl.message('Text Scaling', name: 'textScale', desc: '', args: []); } /// `Light Icon` String get lightIcon { return Intl.message('Light Icon', name: 'lightIcon', desc: '', args: []); } /// `Manually switch light desktop app icon` String get lightIconDesc { return Intl.message( 'Manually switch light desktop app icon', name: 'lightIconDesc', desc: '', args: [], ); } /// `HarmonyOS Font` String get harmonyFont { return Intl.message( 'HarmonyOS Font', name: 'harmonyFont', desc: '', args: [], ); } /// `Use optimized HarmonyOS Sans font` String get harmonyFontDesc { return Intl.message( 'Use optimized HarmonyOS Sans font', name: 'harmonyFontDesc', desc: '', args: [], ); } /// `Internet` String get internet { return Intl.message('Internet', name: 'internet', desc: '', args: []); } /// `System App` String get systemApp { return Intl.message('System App', name: 'systemApp', desc: '', args: []); } /// `No Network App` String get noNetworkApp { return Intl.message( 'No Network App', name: 'noNetworkApp', desc: '', args: [], ); } /// `Contact Me` String get contactMe { return Intl.message('Contact Me', name: 'contactMe', desc: '', args: []); } /// `Recovery Strategy` String get recoveryStrategy { return Intl.message( 'Recovery Strategy', name: 'recoveryStrategy', desc: '', args: [], ); } /// `Override` String get recoveryStrategy_override { return Intl.message( 'Override', name: 'recoveryStrategy_override', desc: '', args: [], ); } /// `Compatible` String get recoveryStrategy_compatible { return Intl.message( 'Compatible', name: 'recoveryStrategy_compatible', desc: '', args: [], ); } /// `Logs Test` String get logsTest { return Intl.message('Logs Test', name: 'logsTest', desc: '', args: []); } /// `{label} cannot be empty` String emptyTip(Object label) { return Intl.message( '$label cannot be empty', name: 'emptyTip', desc: '', args: [label], ); } /// `{label} must be a URL` String urlTip(Object label) { return Intl.message( '$label must be a URL', name: 'urlTip', desc: '', args: [label], ); } /// `{label} must be a number` String numberTip(Object label) { return Intl.message( '$label must be a number', name: 'numberTip', desc: '', args: [label], ); } /// `Interval` String get interval { return Intl.message('Interval', name: 'interval', desc: '', args: []); } /// `{label} already exists` String existsTip(Object label) { return Intl.message( '$label already exists', name: 'existsTip', desc: '', args: [label], ); } /// `Delete current {label}?` String deleteTip(Object label) { return Intl.message( 'Delete current $label?', name: 'deleteTip', desc: '', args: [label], ); } /// `Delete selected {label}?` String deleteMultipTip(Object label) { return Intl.message( 'Delete selected $label?', name: 'deleteMultipTip', desc: '', args: [label], ); } /// `No {label}` String nullTip(Object label) { return Intl.message('No $label', name: 'nullTip', desc: '', args: [label]); } /// `Script` String get script { return Intl.message('Script', name: 'script', desc: '', args: []); } /// `Color` String get color { return Intl.message('Color', name: 'color', desc: '', args: []); } /// `Rename` String get rename { return Intl.message('Rename', name: 'rename', desc: '', args: []); } /// `Unnamed` String get unnamed { return Intl.message('Unnamed', name: 'unnamed', desc: '', args: []); } /// `Please enter a script name` String get pleaseEnterScriptName { return Intl.message( 'Please enter a script name', name: 'pleaseEnterScriptName', desc: '', args: [], ); } /// `Inactive in script mode` String get overrideInvalidTip { return Intl.message( 'Inactive in script mode', name: 'overrideInvalidTip', desc: '', args: [], ); } /// `Mixed Port` String get mixedPort { return Intl.message('Mixed Port', name: 'mixedPort', desc: '', args: []); } /// `Socks Port` String get socksPort { return Intl.message('Socks Port', name: 'socksPort', desc: '', args: []); } /// `Redir Port` String get redirPort { return Intl.message('Redir Port', name: 'redirPort', desc: '', args: []); } /// `Tproxy Port` String get tproxyPort { return Intl.message('Tproxy Port', name: 'tproxyPort', desc: '', args: []); } /// `{label} must be between 1024 and 49151` String portTip(Object label) { return Intl.message( '$label must be between 1024 and 49151', name: 'portTip', desc: '', args: [label], ); } /// `Please enter a different port` String get portConflictTip { return Intl.message( 'Please enter a different port', name: 'portConflictTip', desc: '', args: [], ); } /// `Import` String get import { return Intl.message('Import', name: 'import', desc: '', args: []); } /// `Import from Code` String get importFromCode { return Intl.message( 'Import from Code', name: 'importFromCode', desc: '', args: [], ); } /// `Import failed` String get importFailed { return Intl.message( 'Import failed', name: 'importFailed', desc: '', args: [], ); } /// `Import from File` String get importFile { return Intl.message( 'Import from File', name: 'importFile', desc: '', args: [], ); } /// `Import from URL` String get importUrl { return Intl.message( 'Import from URL', name: 'importUrl', desc: '', args: [], ); } /// `Auto Set System DNS` String get autoSetSystemDns { return Intl.message( 'Auto Set System DNS', name: 'autoSetSystemDns', desc: '', args: [], ); } /// `{label} Details` String details(Object label) { return Intl.message( '$label Details', name: 'details', desc: '', args: [label], ); } /// `Creation Time` String get creationTime { return Intl.message( 'Creation Time', name: 'creationTime', desc: '', args: [], ); } /// `Progress` String get progress { return Intl.message('Progress', name: 'progress', desc: '', args: []); } /// `Host` String get host { return Intl.message('Host', name: 'host', desc: '', args: []); } /// `Destination` String get destination { return Intl.message('Destination', name: 'destination', desc: '', args: []); } /// `Destination GeoIP` String get destinationGeoIP { return Intl.message( 'Destination GeoIP', name: 'destinationGeoIP', desc: '', args: [], ); } /// `Destination IP ASN` String get destinationIPASN { return Intl.message( 'Destination IP ASN', name: 'destinationIPASN', desc: '', args: [], ); } /// `Special Proxy` String get specialProxy { return Intl.message( 'Special Proxy', name: 'specialProxy', desc: '', args: [], ); } /// `Special Rules` String get specialRules { return Intl.message( 'Special Rules', name: 'specialRules', desc: '', args: [], ); } /// `Remote Destination` String get remoteDestination { return Intl.message( 'Remote Destination', name: 'remoteDestination', desc: '', args: [], ); } /// `Network Type` String get networkType { return Intl.message( 'Network Type', name: 'networkType', desc: '', args: [], ); } /// `Proxy Chains` String get proxyChains { return Intl.message( 'Proxy Chains', name: 'proxyChains', desc: '', args: [], ); } /// `Log` String get log { return Intl.message('Log', name: 'log', desc: '', args: []); } /// `Active Connections` String get connection { return Intl.message( 'Active Connections', name: 'connection', desc: '', args: [], ); } /// `Request` String get request { return Intl.message('Request', name: 'request', desc: '', args: []); } /// `Switch` String get switchLabel { return Intl.message('Switch', name: 'switchLabel', desc: '', args: []); } /// `No Status` String get noStatusAvailable { return Intl.message( 'No Status', name: 'noStatusAvailable', desc: '', args: [], ); } /// `Online Panel` String get onlinePanel { return Intl.message( 'Online Panel', name: 'onlinePanel', desc: '', args: [], ); } /// `Open Zashboard` String get openDashboard { return Intl.message( 'Open Zashboard', name: 'openDashboard', desc: '', args: [], ); } /// `Custom` String get custom { return Intl.message('Custom', name: 'custom', desc: '', args: []); } /// `Wakelock` String get wakelock { return Intl.message('Wakelock', name: 'wakelock', desc: '', args: []); } /// `Keeps the screen on and app active in the background without requiring special CPU wakelock permissions.` String get wakelockDescription { return Intl.message( 'Keeps the screen on and app active in the background without requiring special CPU wakelock permissions.', name: 'wakelockDescription', desc: '', args: [], ); } /// `TUN requires admin privileges. Please run as Administrator.` String get tunEnableRequireAdmin { return Intl.message( 'TUN requires admin privileges. Please run as Administrator.', name: 'tunEnableRequireAdmin', desc: '', args: [], ); } /// `Restart TUN for changes to take effect` String get restartTip { return Intl.message( 'Restart TUN for changes to take effect', name: 'restartTip', desc: '', args: [], ); } /// `Restart` String get restart { return Intl.message('Restart', name: 'restart', desc: '', args: []); } /// `Restart Core` String get restartCoreTitle { return Intl.message( 'Restart Core', name: 'restartCoreTitle', desc: '', args: [], ); } /// `Manually restart the core?` String get restartCoreDesc { return Intl.message( 'Manually restart the core?', name: 'restartCoreDesc', desc: '', args: [], ); } /// `High Refresh Rate` String get highRefreshRate { return Intl.message( 'High Refresh Rate', name: 'highRefreshRate', desc: '', args: [], ); } /// `Enable highest refresh rate support` String get highRefreshRateDesc { return Intl.message( 'Enable highest refresh rate support', name: 'highRefreshRateDesc', desc: '', args: [], ); } } class AppLocalizationDelegate extends LocalizationsDelegate { const AppLocalizationDelegate(); List get supportedLocales { return const [ Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'ru'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'TC'), ]; } @override bool isSupported(Locale locale) => _isSupported(locale); @override Future load(Locale locale) => AppLocalizations.load(locale); @override bool shouldReload(AppLocalizationDelegate old) => false; bool _isSupported(Locale locale) { for (var supportedLocale in supportedLocales) { if (supportedLocale.languageCode == locale.languageCode) { return true; } } return false; } } ================================================ FILE: lib/main.dart ================================================ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; import 'package:bett_box/plugins/app.dart'; import 'package:bett_box/plugins/tile.dart'; import 'package:bett_box/plugins/vpn.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'application.dart'; import 'clash/core.dart'; import 'clash/lib.dart'; import 'common/common.dart'; import 'models/models.dart'; ReceivePort? _serviceReceiverPort; ReceivePort? _messageReceiverPort; Future main() async { globalState.isService = false; WidgetsFlutterBinding.ensureInitialized(); PaintingBinding.instance.imageCache.maximumSizeBytes = 50 * 1024 * 1024; final version = await system.version; await clashCore.preload(); await globalState.initApp(version); try { await uiManager.initializeUI(); } catch (e) { commonPrint.log('Failed to initialize UI: $e'); } await _runApp(version); } Future _runApp(int version) async { if (system.isAndroid && globalState.config.appSetting.enableHighRefreshRate) { try { await FlutterDisplayMode.setHighRefreshRate(); } catch (e) { commonPrint.log('Failed to set high refresh rate: $e'); } } await android?.init(); await window?.init(version); HttpOverrides.global = BettboxHttpOverrides(); runApp(ProviderScope(child: const Application())); } @pragma('vm:entry-point') Future _service(List flags) async { globalState.isService = true; WidgetsFlutterBinding.ensureInitialized(); await globalState.init(); { final quickStart = flags.contains('quick'); final bootStart = flags.contains('boot'); final clashLibHandler = ClashLibHandler(); tile?.addListener( _TileListenerWithService( onStart: () async { await app.tip(appLocalizations.startVpn); await globalState.handleStart(); }, onStop: () async { await app.tip(appLocalizations.stopVpn); clashLibHandler.stopListener(); await vpn?.stop(); }, onReconnectIpc: () { commonPrint.log( 'Service: reconnectIpc requested, re-establishing IPC', ); _handleMainIpc(clashLibHandler); }, ), ); vpn?.addListener( _VpnListenerWithService( onDnsChanged: (String dns) { clashLibHandler.updateDns(dns); }, ), ); if (!quickStart && !bootStart) { _handleMainIpc(clashLibHandler); return; } if (bootStart && !globalState.config.appSetting.autoRun) { commonPrint.log('Silent boot detected, but autoRun is disabled. Staying idle.'); _handleMainIpc(clashLibHandler); return; } commonPrint.log('Executing ${bootStart ? "boot" : "quick"} start sequence'); await ClashCore.initGeo(); app.tip(appLocalizations.startVpn); final homeDirPath = await appPath.homeDirPath; final version = await system.version; final clashConfig = globalState.config.patchClashConfig.copyWith.tun( enable: false, ); final params = await globalState.getSetupParams(pathConfig: clashConfig); Future(() async { try { final profileId = globalState.config.currentProfileId; if (profileId == null) { return; } final res = await clashLibHandler.quickStart( InitParams(homeDir: homeDirPath, version: version), params, globalState.getCoreState(), ); debugPrint(res); if (res.isNotEmpty) { commonPrint.log('QuickStart failed with error: $res'); await vpn?.stop(); return; } await vpn?.start(clashLibHandler.getAndroidVpnOptions()); if (globalState.config.appSetting.openLogs) { await clashLibHandler.invokeAction('{"id": "quickStartLog", "method": "startLog"}'); } else { await clashLibHandler.invokeAction('{"id": "quickStopLog", "method": "stopLog"}'); } clashLibHandler.startListener(); } catch (e) { commonPrint.log('Fatal error during service background start: $e'); await vpn?.stop(); } }); } } void _handleMainIpc(ClashLibHandler clashLibHandler) { final sendPort = IsolateNameServer.lookupPortByName(mainIsolate); if (sendPort == null) { commonPrint.log('Service: mainIsolate sendPort not found, IPC unavailable'); return; } _serviceReceiverPort?.close(); _messageReceiverPort?.close(); _serviceReceiverPort = ReceivePort(); _serviceReceiverPort!.listen((message) async { final res = await clashLibHandler.invokeAction(message); _safeSend(sendPort, res); }); _safeSend(sendPort, _serviceReceiverPort!.sendPort); _messageReceiverPort = ReceivePort(); clashLibHandler.attachMessagePort(_messageReceiverPort!.sendPort.nativePort); _messageReceiverPort!.listen((message) { _safeSend(sendPort, message); }); clashLibHandler.startListener(); } void _safeSend(SendPort sendPort, dynamic message) { try { sendPort.send(message); } catch (e) { commonPrint.log('Service: IPC send failed: $e'); final retryPort = IsolateNameServer.lookupPortByName(mainIsolate); if (retryPort != null) { try { retryPort.send(message); } catch (_) {} } } } @immutable class _TileListenerWithService with TileListener { final Function() _onStart; final Function() _onStop; final Function() _onReconnectIpc; const _TileListenerWithService({ required Function() onStart, required Function() onStop, required Function() onReconnectIpc, }) : _onStart = onStart, _onStop = onStop, _onReconnectIpc = onReconnectIpc; @override void onStart() => _onStart(); @override void onStop() => _onStop(); @override void onReconnectIpc() => _onReconnectIpc(); } @immutable class _VpnListenerWithService with VpnListener { final Function(String dns) _onDnsChanged; const _VpnListenerWithService({required Function(String dns) onDnsChanged}) : _onDnsChanged = onDnsChanged; @override void onDnsChanged(String dns) { super.onDnsChanged(dns); _onDnsChanged(dns); } } ================================================ FILE: lib/manager/android_manager.dart ================================================ import 'package:bett_box/plugins/app.dart'; import 'package:bett_box/providers/providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class AndroidManager extends ConsumerStatefulWidget { final Widget child; const AndroidManager({super.key, required this.child}); @override ConsumerState createState() => _AndroidContainerState(); } class _AndroidContainerState extends ConsumerState { @override void initState() { super.initState(); ref.listenManual(appSettingProvider.select((state) => state.hidden), ( prev, next, ) { app.updateExcludeFromRecents(next); }, fireImmediately: true); } @override Widget build(BuildContext context) { return widget.child; } } ================================================ FILE: lib/manager/app_manager.dart ================================================ import 'dart:async'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/manager/window_manager.dart'; import 'package:bett_box/plugins/app.dart'; import 'package:bett_box/providers/providers.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_acrylic/widgets/transparent_macos_sidebar.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:window_manager/window_manager.dart'; class AppStateManager extends ConsumerStatefulWidget { final Widget child; const AppStateManager({super.key, required this.child}); @override ConsumerState createState() => _AppStateManagerState(); } class _AppStateManagerState extends ConsumerState with WidgetsBindingObserver { bool _isRefreshActive = false; Timer? _dashboardRefreshDebounceTimer; Timer? _missedUpdateCheckTimer; DateTime? _lastMissedUpdateCheck; late final VoidCallback _dashboardTickListener; static const _missedUpdateCheckDelay = Duration(seconds: 5); static const _missedUpdateCheckThrottle = Duration(seconds: 60); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _dashboardTickListener = () { if (!globalState.isStart) { return; } unawaited(globalState.appController.updateRunTime()); }; dashboardRefreshManager.tick1s.addListener(_dashboardTickListener); ref.listenManual(layoutChangeProvider, (prev, next) { WidgetsBinding.instance.addPostFrameCallback((_) { if (prev != next) { globalState.computeHeightMapCache = {}; } }); }); ref.listenManual(checkIpProvider, (prev, next) { if (next.b && (prev?.a != next.a)) { detectionState.startCheck(); } }); ref.listenManual(configStateProvider, (prev, next) { if (prev != next) { globalState.appController.savePreferencesDebounce(); } }); WidgetsBinding.instance.addPostFrameCallback((_) { _updateDashboardRefreshState(); detectionState.tryStartCheck(); }); if (window == null) { return; } ref.listenManual(autoSetSystemDnsStateProvider, (prev, next) async { if (prev == next) { return; } if (next.a == true && next.b == true) { macOS?.updateDns(false); } else { macOS?.updateDns(true); } }); ref.listenManual(currentBrightnessProvider, (prev, next) { if (prev == next) { return; } window?.updateMacOSBrightness(next); }, fireImmediately: true); } @override void dispose() { _dashboardRefreshDebounceTimer?.cancel(); _missedUpdateCheckTimer?.cancel(); dashboardRefreshManager.tick1s.removeListener(_dashboardTickListener); WidgetsBinding.instance.removeObserver(this); super.dispose(); } Future _updateDashboardRefreshState() async { final lifecycleState = WidgetsBinding.instance.lifecycleState; final isForeground = lifecycleState == null || lifecycleState == AppLifecycleState.resumed; var isVisible = true; var isMinimized = false; if (system.isDesktop) { final visible = await window?.isVisible; if (visible == false) { isVisible = false; } isMinimized = await window?.isMinimized ?? false; } final shouldRun = isForeground && isVisible && !isMinimized; if (!shouldRun) { _dashboardRefreshDebounceTimer?.cancel(); _dashboardRefreshDebounceTimer = null; if (_isRefreshActive) { dashboardRefreshManager.stop(); _isRefreshActive = false; } return; } if (_isRefreshActive) { return; } _dashboardRefreshDebounceTimer?.cancel(); _dashboardRefreshDebounceTimer = Timer( const Duration(milliseconds: 1000), () { if (!mounted) return; if (_isRefreshActive) return; dashboardRefreshManager.start(); _isRefreshActive = true; }, ); } bool get _shouldCheckMissedUpdates { if (_lastMissedUpdateCheck == null) return true; return DateTime.now().difference(_lastMissedUpdateCheck!) > _missedUpdateCheckThrottle; } void _scheduleMissedUpdateCheck() { if (!_shouldCheckMissedUpdates) return; _missedUpdateCheckTimer?.cancel(); _missedUpdateCheckTimer = Timer(_missedUpdateCheckDelay, () { _lastMissedUpdateCheck = DateTime.now(); globalState.appController.checkAndUpdateMissedProfiles(); }); } @override Future didChangeAppLifecycleState(AppLifecycleState state) async { final isBackgroundState = state == AppLifecycleState.paused || state == AppLifecycleState.hidden || (state == AppLifecycleState.inactive && !system.isDesktop); if (isBackgroundState) { _missedUpdateCheckTimer?.cancel(); globalState.appController.savePreferences(); await globalState.handleBackground(); } else if (state == AppLifecycleState.resumed) { globalState.handleForeground(); render?.resume(); await globalState.resumeForegroundUpdates(); await globalState.appController.syncWakelockIfNeeded(); _scheduleMissedUpdateCheck(); final hasDetection = ref .read(dashboardStateProvider) .dashboardWidgets .contains(DashboardWidget.networkDetection); if (hasDetection) { detectionState.startCheck(immediate: true); } } if (state == AppLifecycleState.resumed && system.isAndroid) { final hidden = ref.read(appSettingProvider.select((s) => s.hidden)); app.updateExcludeFromRecents(hidden); SystemChrome.setSystemUIOverlayStyle(globalState.appState.systemUiOverlayStyle); } if (state == AppLifecycleState.inactive) { WidgetsBinding.instance.addPostFrameCallback((_) { detectionState.tryStartCheck(); }); } _updateDashboardRefreshState(); } @override void didChangePlatformBrightness() { globalState.appController.updateBrightness(); globalState.appController.updateTray(); } @override Widget build(BuildContext context) { return widget.child; } } class AppEnvManager extends StatelessWidget { final Widget child; const AppEnvManager({super.key, required this.child}); @override Widget build(BuildContext context) { if (kDebugMode) { if (globalState.isPre) { return Banner( message: 'DEBUG', location: BannerLocation.topEnd, child: child, ); } } if (globalState.isPre) { return Banner( message: 'PRE', location: BannerLocation.topEnd, child: child, ); } return child; } } class AppSidebarContainer extends ConsumerWidget { final Widget child; const AppSidebarContainer({super.key, required this.child}); Widget _buildLoading() { return Consumer( builder: (_, ref, _) { final loading = ref.watch(loadingProvider); final isMobileView = ref.watch(isMobileViewProvider); return loading && !isMobileView ? RotatedBox( quarterTurns: 1, child: const LinearProgressIndicator(), ) : Container(); }, ); } Widget _buildBackground({ required BuildContext context, required Widget child, }) { if (!system.isMacOS) { return Material( color: context.colorScheme.surfaceContainer, child: child, ); } return TransparentMacOSSidebar( child: Material(color: Colors.transparent, child: child), ); } @override Widget build(BuildContext context, WidgetRef ref) { final navigationState = ref.watch(navigationStateProvider); final navigationItems = navigationState.navigationItems; final isMobileView = navigationState.viewMode == ViewMode.mobile; if (isMobileView) { return child; } final currentIndex = navigationState.currentIndex; final showLabel = ref.watch(appSettingProvider).showLabel; return Row( children: [ Stack( alignment: Alignment.topRight, children: [ _buildBackground( context: context, child: SafeArea( left: !system.isAndroid, top: true, right: false, bottom: false, child: Column( children: [ if (system.isMacOS) const SizedBox(height: 22), const SizedBox(height: 16), if (!system.isMacOS) ...[const AppIcon(), const SizedBox(height: 12)], Expanded( child: ScrollConfiguration( behavior: HiddenBarScrollBehavior(), child: CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.arrowUp): () { if (currentIndex > 0) { globalState.appController.toPage( navigationItems[currentIndex - 1].label, ); } }, const SingleActivator(LogicalKeyboardKey.arrowDown): () { if (currentIndex < navigationItems.length - 1) { globalState.appController.toPage( navigationItems[currentIndex + 1].label, ); } }, const SingleActivator(LogicalKeyboardKey.select): () {}, const SingleActivator(LogicalKeyboardKey.enter): () {}, }, child: Focus( autofocus: true, child: NavigationRail( backgroundColor: Colors.transparent, selectedLabelTextStyle: context .textTheme .labelLarge! .copyWith( color: context.colorScheme.onSurface, ), unselectedLabelTextStyle: context .textTheme .labelLarge! .copyWith( color: context.colorScheme.onSurface, ), destinations: navigationItems .map( (e) => NavigationRailDestination( icon: e.icon, label: Text(Intl.message(e.label.name)), ), ) .toList(), onDestinationSelected: (index) { globalState.appController.toPage( navigationItems[index].label, ); }, extended: showLabel, selectedIndex: currentIndex, labelType: showLabel ? NavigationRailLabelType.none : NavigationRailLabelType.all, ), ), ), ), ), const SizedBox(height: 16), if (window != null) const WindowLockButton(), const SizedBox(height: 16), ], ), ), ), _buildLoading(), ], ), Expanded(flex: 1, child: ClipRect(child: child)), ], ); } } class WindowLockButton extends ConsumerWidget { const WindowLockButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final isLocked = ref.watch( windowSettingProvider.select((state) => state.isLocked), ); return IconButton( onPressed: () async { try { final currentLocked = ref.read( windowSettingProvider.select((state) => state.isLocked), ); final newLocked = !currentLocked; await windowManager.setResizable(!newLocked); ref .read(windowSettingProvider.notifier) .updateState((state) => state.copyWith(isLocked: newLocked)); } catch (e) { commonPrint.log('Window Lock Failed: $e'); } }, icon: Icon( isLocked ? Icons.lock : Icons.lock_open, color: context.colorScheme.onSurfaceVariant, ), ); } } ================================================ FILE: lib/manager/clash_manager.dart ================================================ import 'package:bett_box/clash/clash.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/providers/app.dart'; import 'package:bett_box/providers/config.dart'; import 'package:bett_box/providers/state.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class ClashManager extends ConsumerStatefulWidget { final Widget child; const ClashManager({super.key, required this.child}); @override ConsumerState createState() => _ClashContainerState(); } class _ClashContainerState extends ConsumerState with AppMessageListener { @override Widget build(BuildContext context) { return widget.child; } @override void initState() { super.initState(); clashMessage.addListener(this); ref.listenManual(needSetupProvider, (prev, next) { if (prev != next) { globalState.appController.handleChangeProfile(); } }); ref.listenManual(coreStateProvider, (prev, next) async { if (prev != next) { await clashCore.setState(next); } }); ref.listenManual(updateParamsProvider, (prev, next) { if (prev != next) { globalState.appController.updateClashConfigDebounce(); } }); ref.listenManual(appSettingProvider.select((state) => state.openLogs), ( prev, next, ) { if (next) { clashCore.startLog(); } else { clashCore.stopLog(); } }); } @override Future dispose() async { clashMessage.removeListener(this); super.dispose(); } @override Future onDelay(Delay delay) async { super.onDelay(delay); final appController = globalState.appController; appController.setDelay(delay); debouncer.call(FunctionTag.updateDelay, () async { appController.updateGroupsDebounce(); }, duration: const Duration(milliseconds: 5000)); } @override void onLog(Log log) { ref.read(logsProvider.notifier).addLog(log); if (log.logLevel == LogLevel.error) { globalState.showNotifier(log.payload); } super.onLog(log); } @override void onRequest(TrackerInfo trackerInfo) async { ref.read(requestsProvider.notifier).addRequest(trackerInfo); super.onRequest(trackerInfo); } @override Future onLoaded(String providerName) async { ref .read(providersProvider.notifier) .setProvider(await clashCore.getExternalProvider(providerName)); globalState.appController.updateGroupsDebounce(); super.onLoaded(providerName); } } ================================================ FILE: lib/manager/connectivity_manager.dart ================================================ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; class ConnectivityManager extends StatefulWidget { final Function(List results)? onConnectivityChanged; final Widget child; const ConnectivityManager({ super.key, this.onConnectivityChanged, required this.child, }); @override State createState() => _ConnectivityManagerState(); } class _ConnectivityManagerState extends State { late StreamSubscription subscription; @override void initState() { super.initState(); subscription = Connectivity().onConnectivityChanged.listen((results) async { widget.onConnectivityChanged?.call(results); }); } @override void dispose() { subscription.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return widget.child; } } ================================================ FILE: lib/manager/hotkey_manager.dart ================================================ import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/models/common.dart'; import 'package:bett_box/providers/config.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; class HotKeyManager extends ConsumerStatefulWidget { final Widget child; const HotKeyManager({super.key, required this.child}); @override ConsumerState createState() => _HotKeyManagerState(); } class _HotKeyManagerState extends ConsumerState { @override void initState() { super.initState(); ref.listenManual(hotKeyActionsProvider, (prev, next) { if (!hotKeyActionListEquality.equals(prev, next)) { _updateHotKeys(hotKeyActions: next); } }, fireImmediately: true); } Future _handleHotKeyAction(HotAction action) async { switch (action) { case HotAction.mode: globalState.appController.updateMode(); case HotAction.start: globalState.appController.updateStart(); case HotAction.view: globalState.appController.updateVisible(); case HotAction.proxy: globalState.appController.updateSystemProxy(); case HotAction.tun: globalState.appController.updateTun(); } } Future _updateHotKeys({ required List hotKeyActions, }) async { await hotKeyManager.unregisterAll(); final hotkeyActionHandles = hotKeyActions .where((hotKeyAction) { return hotKeyAction.key != null && hotKeyAction.modifiers.isNotEmpty; }) .map((hotKeyAction) async { final modifiers = hotKeyAction.modifiers .map((item) => item.toHotKeyModifier()) .toList(); final hotKey = HotKey( key: PhysicalKeyboardKey(hotKeyAction.key!), modifiers: modifiers, ); return await hotKeyManager.register( hotKey, keyDownHandler: (_) { _handleHotKeyAction(hotKeyAction.action); }, ); }); await Future.wait(hotkeyActionHandles); } Shortcuts _buildShortcuts(Widget child) { return Shortcuts( shortcuts: { utils.controlSingleActivator(LogicalKeyboardKey.keyW): CloseWindowIntent(), }, child: Actions( actions: { CloseWindowIntent: CallbackAction( onInvoke: (_) => globalState.appController.handleBackOrExit(), ), DoNothingIntent: CallbackAction( onInvoke: (_) => null, ), }, child: child, ), ); } @override Widget build(BuildContext context) { return _buildShortcuts(widget.child); } } ================================================ FILE: lib/manager/manager.dart ================================================ export 'android_manager.dart'; export 'app_manager.dart'; export 'clash_manager.dart'; export 'connectivity_manager.dart'; export 'message_manager.dart'; export 'proxy_manager.dart'; export 'smart_auto_stop_manager.dart'; export 'theme_manager.dart'; export 'tile_manager.dart'; export 'tray_manager.dart'; export 'vpn_manager.dart'; export 'window_manager.dart'; ================================================ FILE: lib/manager/message_manager.dart ================================================ import 'dart:async'; import 'dart:math'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/widgets/fade_box.dart'; import 'package:flutter/material.dart'; class MessageManager extends StatefulWidget { final Widget child; const MessageManager({super.key, required this.child}); @override State createState() => MessageManagerState(); } class MessageManagerState extends State { final _messagesNotifier = ValueNotifier>([]); final List _bufferMessages = []; bool _pushing = false; @override void initState() { super.initState(); } @override void dispose() { _messagesNotifier.dispose(); super.dispose(); } Future message( String text, { VoidCallback? onAction, String? actionLabel, bool showCountdown = false, }) async { if (_messagesNotifier.value.any((m) => m.text == text) || _bufferMessages.any((m) => m.text == text)) { return; } final commonMessage = CommonMessage( id: utils.uuidV4, text: text, onAction: onAction, actionLabel: actionLabel, showCountdown: showCountdown, ); _bufferMessages.add(commonMessage); await _showMessage(); } Future _showMessage() async { if (_pushing) return; _pushing = true; while (_bufferMessages.isNotEmpty) { final commonMessage = _bufferMessages.removeAt(0); _messagesNotifier.value = List.from(_messagesNotifier.value)..add(commonMessage); await Future.delayed(const Duration(seconds: 1)); Future.delayed(commonMessage.duration, () => _handleRemove(commonMessage)); if (_bufferMessages.isEmpty) _pushing = false; } } void _handleRemove(CommonMessage commonMessage) { _messagesNotifier.value = List.from(_messagesNotifier.value) ..remove(commonMessage); } @override Widget build(BuildContext context) { return Stack( children: [ widget.child, ValueListenableBuilder( valueListenable: _messagesNotifier, builder: (_, messages, _) { return FadeThroughBox( margin: EdgeInsets.only( top: kToolbarHeight + 8, left: 12, right: 12, ), alignment: Alignment.topCenter, child: messages.isEmpty ? SizedBox() : LayoutBuilder( key: Key(messages.last.id), builder: (_, constraints) { final message = messages.last; return Card( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(12.0), ), ), elevation: 10, color: context.colorScheme.surfaceContainerHigh, child: Container( width: min(constraints.maxWidth, 500), padding: EdgeInsets.symmetric( horizontal: 12, vertical: 12, ), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (message.showCountdown) _CountdownWidget( duration: message.duration, ), Expanded( child: Text( message.text, textAlign: TextAlign.center, ), ), if (message.actionLabel != null && message.onAction != null) ...[ const SizedBox(width: 8), TextButton( onPressed: () { _handleRemove(message); message.onAction?.call(); }, style: TextButton.styleFrom( minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), foregroundColor: context.colorScheme.primary, ), child: Text(message.actionLabel!), ), ], ], ), ), ); }, ), ); }, ), ], ); } } class _CountdownWidget extends StatefulWidget { final Duration duration; const _CountdownWidget({required this.duration}); @override State<_CountdownWidget> createState() => _CountdownWidgetState(); } class _CountdownWidgetState extends State<_CountdownWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; late int _totalSeconds; @override void initState() { super.initState(); _totalSeconds = widget.duration.inSeconds; _controller = AnimationController( vsync: this, duration: widget.duration, ); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final primaryColor = context.colorScheme.primary; return Padding( padding: const EdgeInsets.only(right: 8), child: SizedBox( width: 24, height: 24, child: AnimatedBuilder( animation: _controller, builder: (context, child) { final remaining = (_totalSeconds * (1 - _controller.value)).ceil(); final currentSecond = remaining > 0 ? remaining : 1; return Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: 1 - _controller.value, strokeWidth: 2, backgroundColor: primaryColor.withAlpha(26), valueColor: AlwaysStoppedAnimation(primaryColor), ), Text( '$currentSecond', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: primaryColor, ), ), ], ); }, ), ), ); } } ================================================ FILE: lib/manager/proxy_manager.dart ================================================ import 'package:bett_box/common/proxy.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/providers/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class ProxyManager extends ConsumerStatefulWidget { final Widget child; const ProxyManager({super.key, required this.child}); @override ConsumerState createState() => _ProxyManagerState(); } class _ProxyManagerState extends ConsumerState { Future _updateProxy(ProxyState proxyState) async { final isStart = proxyState.isStart; final systemProxy = proxyState.systemProxy; final port = proxyState.port; if (isStart && systemProxy) { proxy?.startProxy(port, proxyState.bassDomain); } else { proxy?.stopProxy(); } } @override void initState() { super.initState(); ref.listenManual(proxyStateProvider, (prev, next) { if (prev != next) { _updateProxy(next); } }, fireImmediately: true); } @override Widget build(BuildContext context) { return widget.child; } } ================================================ FILE: lib/manager/smart_auto_stop_manager.dart ================================================ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:bett_box/clash/clash.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/common/network_matcher.dart'; import 'package:bett_box/models/models.dart'; import 'package:bett_box/plugins/service.dart'; import 'package:bett_box/providers/providers.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:synchronized/synchronized.dart'; /// Smart Auto Stop Manager class SmartAutoStopManager extends ConsumerStatefulWidget { final Widget child; const SmartAutoStopManager({super.key, required this.child}); @override ConsumerState createState() => _SmartAutoStopManagerState(); } class _SmartAutoStopManagerState extends ConsumerState { StreamSubscription>? _connectivitySubscription; final _checkLock = Lock(); int _checkSequence = 0; late final NativeEventCallback _nativeEventCallback; @override void initState() { super.initState(); _initConnectivityListener(); _initNativeNetworkListener(); } void _initNativeNetworkListener() { _nativeEventCallback = (String method, dynamic arguments) async { if (method == 'networkChanged') { _onNativeNetworkChanged(); } else if (method == 'quickResponse') { final vpnProps = ref.read(vpnSettingProvider); if (vpnProps.quickResponse) { commonPrint.log( 'Network change: Closing connections.', ); clashCore.closeConnections(); } } }; service?.addNativeEventCallback(_nativeEventCallback); } void _onNativeNetworkChanged() { final vpnProps = ref.read(vpnSettingProvider); if (!vpnProps.smartAutoStop) return; _debouncedCheckCurrentNetwork(); } @override void didChangeDependencies() { super.didChangeDependencies(); ref.listenManual(vpnSettingProvider, (prev, next) { if (prev?.smartAutoStop != next.smartAutoStop || prev?.smartAutoStopNetworks != next.smartAutoStopNetworks) { _onSettingsChanged(); } }); } void _initConnectivityListener() { _connectivitySubscription = Connectivity().onConnectivityChanged.listen(( results, ) { _onConnectivityChanged(results); }); } void _onSettingsChanged() { final vpnProps = ref.read(vpnSettingProvider); if (!vpnProps.smartAutoStop) { // Feature disabled, if we were smart-stopped, resume. final isSmartStopped = ref.read(isSmartStoppedProvider); if (isSmartStopped) { ref.read(isSmartStoppedProvider.notifier).set(false); _restartVpn(); } return; } // Re-check current network _checkCurrentNetwork(); } Future _onConnectivityChanged(List results) async { final vpnProps = ref.read(vpnSettingProvider); if (!vpnProps.smartAutoStop) return; _debouncedCheckCurrentNetwork(); } void _debouncedCheckCurrentNetwork() { final currentSequence = ++_checkSequence; Future.delayed(const Duration(milliseconds: 1000), () async { if (currentSequence != _checkSequence) { commonPrint.log('Smart Auto Stop: Skipping outdated network check'); return; } await _checkCurrentNetwork(); }); } Future _checkCurrentNetwork() async { await _checkLock.synchronized(() async { final vpnProps = ref.read(vpnSettingProvider); if (!vpnProps.smartAutoStop) return; final networks = vpnProps.smartAutoStopNetworks; if (networks.isEmpty) return; final isSmartStopped = ref.read(isSmartStoppedProvider); // Get current IP(s) — always from native on Android for consistency List candidateIps; if (system.isAndroid) { candidateIps = await _getNativeLocalIpAddresses(); } else { final ip = await _getLocalIpAddress(); candidateIps = ip != null ? [ip] : []; } if (candidateIps.isEmpty) { commonPrint.log('Smart Auto Stop: No IP found. Skipping.'); return; } // Match: any IP matches any rule = should stop final shouldStop = candidateIps.any( (ip) => NetworkMatcher.matchAny(ip, networks), ); commonPrint.log( 'SmartAutoStop: IPs=${candidateIps.join(",")}, RuleMatch=$shouldStop, SmartStopped=$isSmartStopped', ); // Dedup: only act on state transitions if (shouldStop && !isSmartStopped) { // Need to stop, but only if VPN is actually running final isRunning = ref.read(runTimeProvider) != null || globalState.isStart; if (isRunning) { ref.read(isSmartStoppedProvider.notifier).set(true); commonPrint.log('Smart Auto Stop: Stopping ...'); await _stopVpn(); } } else if (!shouldStop && isSmartStopped) { // Need to resume ref.read(isSmartStoppedProvider.notifier).set(false); commonPrint.log('Smart Auto Stop: Restarting ...'); await _restartVpn(); } }); } Future> _getNativeLocalIpAddresses() async { try { final serviceInstance = service; if (serviceInstance != null) { final ips = await serviceInstance.getLocalIpAddresses(); if (ips.isNotEmpty) return ips; } } catch (e) { commonPrint.log('Smart Auto Stop: Native IP error: $e'); } // Fallback to Flutter layer final ip = await _getLocalIpAddress(); return ip != null ? [ip] : []; } Future _getLocalIpAddress() async { return await utils.getLocalIpAddress(); } Future _stopVpn() async { if (system.isAndroid) { // Android: Enable smart-stop mode (Blank notification) // This keeps the service alive but stops the VPN logic await service?.setSmartStopped(true); await service?.smartStop(); // Update Dart state to look "stopped" globalState.startTime = null; clashCore.resetTraffic(); ref.read(trafficsProvider.notifier).clear(); ref.read(totalTrafficProvider.notifier).value = Traffic(); ref.read(runTimeProvider.notifier).value = null; } else { // Desktop: Full stop await globalState.appController.updateStatus(false); } } Future _restartVpn() async { if (system.isAndroid) { // Android: Resume from smart-stop mode await service?.setSmartStopped(false); await service?.smartResume(); globalState.startTime = DateTime.now(); ref.read(runTimeProvider.notifier).value = 0; globalState.appController.addCheckIpNumDebounce(); } else { // Desktop: Full start await globalState.appController.updateStatus(true); } } @override void dispose() { _connectivitySubscription?.cancel(); service?.removeNativeEventCallback(_nativeEventCallback); super.dispose(); } @override Widget build(BuildContext context) { return widget.child; } } ================================================ FILE: lib/manager/theme_manager.dart ================================================ import 'dart:math'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/common/theme.dart'; import 'package:bett_box/providers/config.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/state.dart'; class ThemeManager extends ConsumerWidget { final Widget child; const ThemeManager({super.key, required this.child}); Widget _buildSystemUi(Widget child) { if (!system.isAndroid) { return child; } return AnnotatedRegion( sized: false, value: SystemUiMode.edgeToEdge, child: Consumer( builder: (context, ref, _) { final brightness = ref.watch(currentBrightnessProvider); final iconBrightness = brightness == Brightness.light ? Brightness.dark : Brightness.light; globalState.appState = globalState.appState.copyWith( systemUiOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: iconBrightness, systemNavigationBarIconBrightness: iconBrightness, systemNavigationBarColor: context.colorScheme.surface, systemNavigationBarDividerColor: Colors.transparent, ), ); return AnnotatedRegion( value: globalState.appState.systemUiOverlayStyle, sized: false, child: child, ); }, ), ); } // _buildScrollbar(Widget child) { // return Consumer( // builder: (_, ref, child) { // final isMobileView = ref.read(isMobileViewProvider); // if (isMobileView) { // return ScrollConfiguration( // behavior: HiddenBarScrollBehavior(), // child: child!, // ); // } // return child!; // }, // child: child, // ); // } @override Widget build(BuildContext context, ref) { final textScale = ref.read( themeSettingProvider.select((state) => state.textScale), ); final double textScaleFactor = max( min( textScale.enable ? textScale.scale : defaultTextScaleFactor, maxTextScale, ), minTextScale, ); globalState.measure = Measure.of(context, textScaleFactor); globalState.theme = CommonTheme.of(context, textScaleFactor); final padding = MediaQuery.of(context).padding; final height = MediaQuery.of(context).size.height; return MediaQuery( data: MediaQuery.of(context).copyWith( textScaler: TextScaler.linear(textScaleFactor), padding: padding.copyWith( top: padding.top > height * 0.3 ? 20.0 : padding.top, ), ), child: LayoutBuilder( builder: (_, container) { globalState.appController.updateViewSize( Size(container.maxWidth, container.maxHeight), ); return _buildSystemUi(child); }, ), ); } } ================================================ FILE: lib/manager/tile_manager.dart ================================================ import 'package:bett_box/plugins/tile.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; class TileManager extends StatefulWidget { final Widget child; const TileManager({super.key, required this.child}); @override State createState() => _TileContainerState(); } class _TileContainerState extends State with TileListener { @override Widget build(BuildContext context) { return widget.child; } @override void onStart() { globalState.appController.updateStatus(true); super.onStart(); } @override Future onStop() async { globalState.appController.updateStatus(false); super.onStop(); } @override void initState() { super.initState(); tile?.addListener(this); } @override void dispose() { tile?.removeListener(this); super.dispose(); } } ================================================ FILE: lib/manager/tray_manager.dart ================================================ import 'package:bett_box/common/common.dart'; import 'package:bett_box/providers/state.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tray_manager/tray_manager.dart'; class TrayManager extends ConsumerStatefulWidget { final Widget child; const TrayManager({super.key, required this.child}); @override ConsumerState createState() => _TrayContainerState(); } class _TrayContainerState extends ConsumerState with TrayListener { @override void initState() { super.initState(); trayManager.addListener(this); ref.listenManual(trayStateProvider, (prev, next) { if (prev != next) { globalState.appController.updateTray(); } }); } @override Widget build(BuildContext context) { return widget.child; } @override void onTrayIconRightMouseDown() { // ignore: deprecated_member_use trayManager.popUpContextMenu(bringAppToFront: true); } @override onTrayIconMouseDown() { window?.show(); } @override dispose() { trayManager.removeListener(this); super.dispose(); } } ================================================ FILE: lib/manager/vpn_manager.dart ================================================ import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/providers/app.dart'; import 'package:bett_box/providers/state.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class VpnManager extends ConsumerStatefulWidget { final Widget child; const VpnManager({super.key, required this.child}); @override ConsumerState createState() => _VpnContainerState(); } class _VpnContainerState extends ConsumerState { @override void initState() { super.initState(); ref.listenManual(vpnStateProvider, (prev, next) { // Skip tip if (prev == null || prev == next) return; final prevProps = prev.vpnProps; final nextProps = next.vpnProps; // Check final onlySmartAutoStopChanged = prevProps.copyWith( smartAutoStop: nextProps.smartAutoStop, smartAutoStopNetworks: nextProps.smartAutoStopNetworks, ) == nextProps; final onlyQuickResponseChanged = prevProps.copyWith( quickResponse: nextProps.quickResponse, ) == nextProps; if (onlySmartAutoStopChanged || onlyQuickResponseChanged) { return; // No tip needed } showTip(); }); } void showTip() { debouncer.call(FunctionTag.vpnTip, () { if (ref.read(runTimeProvider.notifier).isStart) { globalState.showNotifier( appLocalizations.vpnTip, onAction: () async { await globalState.appController.updateStatus(false); await Future.delayed(const Duration(milliseconds: 500)); await globalState.appController.updateStatus(true); }, actionLabel: appLocalizations.restart, showCountdown: true, ); } }); } @override Widget build(BuildContext context) { return widget.child; } } ================================================ FILE: lib/manager/window_manager.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:bett_box/providers/providers.dart'; import 'package:bett_box/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_ext/window_ext.dart'; import 'package:window_manager/window_manager.dart'; class WindowManager extends ConsumerStatefulWidget { final Widget child; const WindowManager({super.key, required this.child}); @override ConsumerState createState() => _WindowContainerState(); } class _WindowContainerState extends ConsumerState with WindowListener, WindowExtListener { Timer? _renderToggleTimer; bool? _pendingRenderResume; void _scheduleRenderToggle(bool resume) { _pendingRenderResume = resume; _renderToggleTimer?.cancel(); _renderToggleTimer = Timer(const Duration(milliseconds: 500), () { if (_pendingRenderResume == true) { render?.resume(); } else { render?.pause(); } }); } @override Widget build(BuildContext context) { return widget.child; } ProviderSubscription? _autoLaunchSub; ProviderSubscription? _smartDelaySub; @override void initState() { super.initState(); _autoLaunchSub = ref.listenManual(appSettingProvider.select((state) => state.autoLaunch), ( prev, next, ) { if (prev != next) { final smartDelayLaunch = ref.read(appSettingProvider).smartDelayLaunch; debouncer.call(FunctionTag.autoLaunch, () { autoLaunch?.updateStatus(next, requireNetwork: smartDelayLaunch); }); } }); _smartDelaySub = ref.listenManual( appSettingProvider.select((state) => state.smartDelayLaunch), (prev, next) { if (prev != next) { final autoLaunchEnabled = ref.read(appSettingProvider).autoLaunch; if (autoLaunchEnabled) { autoLaunch?.updateStatus(true, requireNetwork: next); } } }, ); windowExtManager.addListener(this); windowManager.addListener(this); } @override void onWindowClose() async { globalState.appController.unBackBlock(); await globalState.appController.handleBackOrExit(); } @override Future onShouldTerminate() async { await globalState.appController.handleExit(); super.onShouldTerminate(); } @override Future onWindowMoved() async { super.onWindowMoved(); final offset = await windowManager.getPosition(); ref .read(windowSettingProvider.notifier) .updateState( (state) => state.copyWith(top: offset.dy, left: offset.dx), ); } @override Future onWindowResized() async { super.onWindowResized(); final size = await windowManager.getSize(); ref .read(windowSettingProvider.notifier) .updateState( (state) => state.copyWith(width: size.width, height: size.height), ); } @override void onWindowMinimize() async { globalState.appController.savePreferencesDebounce(); _renderToggleTimer?.cancel(); await globalState.handleBackground(); super.onWindowMinimize(); } @override void onWindowRestore() { globalState.handleForeground(); _scheduleRenderToggle(true); unawaited(globalState.resumeForegroundUpdates()); unawaited(globalState.appController.syncWakelockIfNeeded()); super.onWindowRestore(); } @override Future dispose() async { _autoLaunchSub?.close(); _smartDelaySub?.close(); windowManager.removeListener(this); windowExtManager.removeListener(this); _renderToggleTimer?.cancel(); super.dispose(); } } class WindowHeaderContainer extends StatelessWidget { final Widget child; const WindowHeaderContainer({super.key, required this.child}); @override Widget build(BuildContext context) { return Consumer( builder: (_, ref, child) { final isMobileView = ref.watch(isMobileViewProvider); final version = ref.watch(versionProvider); if ((version <= 10 || !isMobileView) && system.isMacOS) { return child!; } return Stack( children: [ Column( children: [ SizedBox(height: kHeaderHeight), Expanded(flex: 1, child: child!), ], ), const WindowHeader(), ], ); }, child: child, ); } } class WindowHeader extends StatefulWidget { const WindowHeader({super.key}); @override State createState() => _WindowHeaderState(); } class _WindowHeaderState extends State { final isMaximizedNotifier = ValueNotifier(false); final isPinNotifier = ValueNotifier(false); final isHoveringNotifier = ValueNotifier(false); // 新增:鼠标悬停状态 @override void initState() { super.initState(); _initNotifier(); } Future _initNotifier() async { isMaximizedNotifier.value = await windowManager.isMaximized(); isPinNotifier.value = await windowManager.isAlwaysOnTop(); } @override void dispose() { isMaximizedNotifier.dispose(); isPinNotifier.dispose(); isHoveringNotifier.dispose(); // 新增:释放资源 super.dispose(); } Future _updateMaximized() async { final isMaximized = await windowManager.isMaximized(); switch (isMaximized) { case true: await windowManager.unmaximize(); break; case false: await windowManager.maximize(); break; } isMaximizedNotifier.value = await windowManager.isMaximized(); } Future _updatePin() async { final isAlwaysOnTop = await windowManager.isAlwaysOnTop(); await windowManager.setAlwaysOnTop(!isAlwaysOnTop); isPinNotifier.value = await windowManager.isAlwaysOnTop(); } Widget _buildActions() { final shouldUseHoverEffect = system.isWindows || system.isLinux; return MouseRegion( onEnter: shouldUseHoverEffect ? (_) => isHoveringNotifier.value = true : null, onExit: shouldUseHoverEffect ? (_) { Future.delayed(const Duration(milliseconds: 100), () { if (mounted) { isHoveringNotifier.value = false; } }); } : null, child: ValueListenableBuilder( valueListenable: isHoveringNotifier, builder: (_, isHovering, _) { final showButtons = !shouldUseHoverEffect || isHovering; return Opacity( opacity: showButtons ? 1.0 : 0.0, child: IgnorePointer( ignoring: !showButtons, child: Row( children: [ IconButton( onPressed: () async { _updatePin(); }, icon: ValueListenableBuilder( valueListenable: isPinNotifier, builder: (_, value, _) { return value ? const Icon(Icons.push_pin) : const Icon(Icons.push_pin_outlined); }, ), ), IconButton( onPressed: () { windowManager.minimize(); }, icon: const Icon(Icons.remove), ), IconButton( onPressed: () async { _updateMaximized(); }, icon: ValueListenableBuilder( valueListenable: isMaximizedNotifier, builder: (_, value, _) { return value ? const Icon(Icons.filter_none, size: 20) : const Icon(Icons.crop_square); }, ), ), IconButton( onPressed: () { FocusManager.instance.primaryFocus?.unfocus(); globalState.appController.unBackBlock(); isHoveringNotifier.value = true; globalState.appController.handleBackOrExit(); }, icon: const Icon(Icons.close), ), ], ), ), ); }, ), ); } @override Widget build(BuildContext context) { return Material( child: Stack( alignment: AlignmentDirectional.center, children: [ Positioned( child: GestureDetector( onPanStart: (_) { windowManager.startDragging(); }, onDoubleTap: () { _updateMaximized(); }, child: Container( color: context.colorScheme.secondary.opacity15, alignment: Alignment.centerLeft, height: kHeaderHeight, ), ), ), if (system.isMacOS) const Text(appName) else ...[ Positioned(right: 0, child: _buildActions()), ], ], ), ); } } final sidebarIconPathProvider = StateNotifierProvider((ref) { return SidebarIconPathNotifier(); }); class SidebarIconPathNotifier extends StateNotifier { SidebarIconPathNotifier() : super(null) { _init(); } Future _init() async { final prefs = await preferences.sharedPreferencesCompleter.future; state = prefs?.getString(customSidebarIconKey); } Future updatePath(String? path) async { state = path; final prefs = await preferences.sharedPreferencesCompleter.future; if (path == null) { prefs?.remove(customSidebarIconKey); } else { prefs?.setString(customSidebarIconKey, path); } } } class AppIcon extends ConsumerWidget { const AppIcon({super.key}); Future _handlePickImage(BuildContext context, WidgetRef ref) async { final result = await FilePicker.platform.pickFiles(type: FileType.image); if (result != null && result.files.single.path != null) { final path = result.files.single.path!; final file = File(path); final size = await file.length(); if (size > 1024 * 1024) { if (context.mounted) { globalState.showNotifier('Image size exceeds 1MB'); } return; } ref.read(sidebarIconPathProvider.notifier).updatePath(path); } } @override Widget build(BuildContext context, WidgetRef ref) { final isDark = Theme.of(context).brightness == Brightness.dark; final customIconPath = ref.watch(sidebarIconPathProvider); Widget icon; if (customIconPath != null && customIconPath.isNotEmpty) { icon = ClipOval( child: Image.file( File(customIconPath), width: 32, height: 32, fit: BoxFit.cover, cacheWidth: 64, cacheHeight: 64, errorBuilder: (_, _, _) { // Fallback if file load fails return Image.asset( isDark ? 'assets/images/icon.png' : 'assets/images/icon_light.png', fit: BoxFit.contain, ); }, ), ); } else { icon = Image.asset( isDark ? 'assets/images/icon.png' : 'assets/images/icon_light.png', fit: BoxFit.contain, ); } return GestureDetector( onLongPress: () => _handlePickImage(context, ref), child: SizedBox(width: 40, height: 40, child: icon), ); } } ================================================ FILE: lib/models/app.dart ================================================ import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'common.dart'; import 'core.dart'; part 'generated/app.freezed.dart'; typedef DelayMap = Map>; @freezed abstract class AppState with _$AppState { const factory AppState({ @Default(false) bool isInit, @Default(false) bool backBlock, @Default(PageLabel.dashboard) PageLabel pageLabel, @Default([]) List packages, @Default(0) int sortNum, required Size viewSize, @Default({}) DelayMap delayMap, @Default([]) List groups, @Default(0) int checkIpNum, required Brightness brightness, int? runTime, @Default([]) List providers, String? localIp, required FixedList requests, required int version, required FixedList logs, required FixedList traffics, required Traffic totalTraffic, @Default(false) bool realTunEnable, @Default(false) bool loading, required SystemUiOverlayStyle systemUiOverlayStyle, }) = _AppState; } extension AppStateExt on AppState { ViewMode get viewMode => utils.getViewMode(viewSize.width); bool get isStart => runTime != null; } ================================================ FILE: lib/models/clash_config.dart ================================================ // ignore_for_file: invalid_annotation_target import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'generated/clash_config.freezed.dart'; part 'generated/clash_config.g.dart'; typedef HostsMap = Map; const defaultClashConfig = ClashConfig(); const defaultTun = Tun(); const defaultDns = Dns(); const defaultNtp = Ntp(); const defaultSniffer = Sniffer(); const defaultTunnel = []; const defaultExperimental = Experimental(); const defaultGeoXUrl = GeoXUrl(); const defaultMixedPort = 7890; const defaultKeepAliveInterval = 30; const defaultBypassPrivateRouteAddress = [ '198.51.100.0/30', '1.0.0.0/8', '2.0.0.0/7', '4.0.0.0/6', '8.0.0.0/7', '11.0.0.0/8', '12.0.0.0/6', '16.0.0.0/4', '32.0.0.0/3', '64.0.0.0/3', '96.0.0.0/4', '112.0.0.0/5', '120.0.0.0/6', '124.0.0.0/7', '126.0.0.0/8', '128.0.0.0/3', '160.0.0.0/5', '168.0.0.0/8', '169.0.0.0/9', '169.128.0.0/10', '169.192.0.0/11', '169.224.0.0/12', '169.240.0.0/13', '169.248.0.0/14', '169.252.0.0/15', '169.255.0.0/16', '170.0.0.0/7', '172.0.0.0/12', '172.32.0.0/11', '172.64.0.0/10', '172.128.0.0/9', '173.0.0.0/8', '174.0.0.0/7', '176.0.0.0/4', '192.0.0.0/9', '192.128.0.0/11', '192.160.0.0/13', '192.169.0.0/16', '192.170.0.0/15', '192.172.0.0/14', '192.176.0.0/12', '192.192.0.0/10', '193.0.0.0/8', '194.0.0.0/7', '196.0.0.0/6', '200.0.0.0/5', '208.0.0.0/4', '2000::/3', ]; int? _parseInt(dynamic value) { if (value == null) return null; if (value is num) return value.toInt(); if (value is String) return int.tryParse(value); return null; } bool? _parseBool(dynamic value) { if (value == null) return null; if (value is bool) return value; if (value is String) { if (value.toLowerCase() == 'true') return true; if (value.toLowerCase() == 'false') return false; } return null; } List? _parseStringList(dynamic value) { if (value == null) return null; if (value is List) { return value.whereType().toList(); } return null; } @freezed abstract class ProxyGroup with _$ProxyGroup { const factory ProxyGroup({ required String name, @JsonKey(fromJson: GroupType.parseProfileType) required GroupType type, @JsonKey(fromJson: _parseStringList) List? proxies, @JsonKey(fromJson: _parseStringList) List? use, @JsonKey(fromJson: _parseInt) int? interval, @JsonKey(fromJson: _parseBool) bool? lazy, String? url, @JsonKey(fromJson: _parseInt) int? timeout, @JsonKey(name: 'max-failed-times', fromJson: _parseInt) int? maxFailedTimes, String? filter, @JsonKey(name: 'expected-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') dynamic expectedStatus, @JsonKey(fromJson: _parseBool) bool? hidden, String? icon, @JsonKey(fromJson: _parseInt) int? tolerance, }) = _ProxyGroup; factory ProxyGroup.fromJson(Map json) => _$ProxyGroupFromJson(json); } @freezed abstract class RuleProvider with _$RuleProvider { const factory RuleProvider({required String name}) = _RuleProvider; factory RuleProvider.fromJson(Map json) => _$RuleProviderFromJson(json); } @freezed abstract class Sniffer with _$Sniffer { const factory Sniffer({ @Default(true) bool enable, @Default(false) @JsonKey(name: 'override-destination') bool overrideDest, @Default([]) List sniffing, @Default(['+.v2ex.com']) @JsonKey(name: 'force-domain') List forceDomain, @Default(['192.168.0.3/32']) @JsonKey(name: 'skip-src-address') List skipSrcAddress, @Default([ '91.108.56.0/22', '91.108.4.0/22', '91.108.8.0/22', '91.108.16.0/22', '91.108.12.0/22', '149.154.160.0/20', '91.105.192.0/23', '91.108.20.0/22', '185.76.151.0/24', '2001:b28:f23d::/48', '2001:b28:f23f::/48', '2001:67c:4e8::/48', '2001:b28:f23c::/48', '2a0a:f280::/32', ]) @JsonKey(name: 'skip-dst-address') List skipDstAddress, @Default(['Mijia Cloud', '+.push.apple.com']) @JsonKey(name: 'skip-domain') List skipDomain, @Default([]) @JsonKey(name: 'port-whitelist') List port, @Default(true) @JsonKey(name: 'force-dns-mapping') bool forceDnsMapping, @Default(true) @JsonKey(name: 'parse-pure-ip') bool parsePureIp, @Default({ 'HTTP': SnifferConfig(ports: ['80', '8080-8880'], overrideDest: true), 'TLS': SnifferConfig(ports: ['443', '8443']), 'QUIC': SnifferConfig(ports: ['443', '8443']), }) Map sniff, }) = _Sniffer; factory Sniffer.fromJson(Map json) => _$SnifferFromJson(json); factory Sniffer.safeSnifferFromJson(Map json) { try { return Sniffer.fromJson(json); } catch (_) { return const Sniffer(); } } } List _formJsonPorts(List? ports) { return ports?.map((item) => item.toString()).toList() ?? []; } @freezed abstract class TunnelEntry with _$TunnelEntry { const factory TunnelEntry({ required String id, List? network, String? address, String? target, String? proxyName, }) = _TunnelEntry; factory TunnelEntry.fromJson(Map json) => _$TunnelEntryFromJson(json); factory TunnelEntry.fromString(String value) { final id = utils.uuidV4; // Parse simple format: tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy final parts = value.split(',').map((e) => e.trim()).toList(); if (parts.length >= 3) { return TunnelEntry( id: id, network: parts[0].split('/').map((e) => e.trim()).toList(), address: parts[1], target: parts[2], proxyName: parts.length > 3 ? parts[3] : null, ); } return TunnelEntry(id: id); } } extension TunnelEntryExt on TunnelEntry { String get displayValue { final parts = []; if (network != null && network!.isNotEmpty) { parts.add(network!.join('/')); } if (address != null && address!.isNotEmpty) { parts.add(address!); } if (target != null && target!.isNotEmpty) { parts.add(target!); } if (proxyName != null && proxyName!.isNotEmpty) { parts.add(proxyName!); } return parts.join(', '); } Map toClashJson() { final map = {}; if (network != null && network!.isNotEmpty) { map['network'] = network; } if (address != null && address!.isNotEmpty) { map['address'] = address; } if (target != null && target!.isNotEmpty) { map['target'] = target; } if (proxyName != null && proxyName!.isNotEmpty) { map['proxy'] = proxyName; } return map; } } @freezed abstract class SnifferConfig with _$SnifferConfig { const factory SnifferConfig({ @Default([]) @JsonKey(fromJson: _formJsonPorts) List ports, @JsonKey(name: 'override-destination') bool? overrideDest, }) = _SnifferConfig; factory SnifferConfig.fromJson(Map json) => _$SnifferConfigFromJson(json); } @freezed abstract class Tun with _$Tun { const factory Tun({ @Default(false) bool enable, @Default(tunDeviceName) String device, @JsonKey(name: 'auto-route') @Default(false) bool autoRoute, @Default(TunStack.system) TunStack stack, @JsonKey(name: 'dns-hijack') @Default(['any:53']) List dnsHijack, @JsonKey(name: 'route-address') @Default([]) List routeAddress, @JsonKey(name: 'route-exclude-address') @Default([]) List routeExcludeAddress, @JsonKey(name: 'strict-route') @Default(false) bool strictRoute, @JsonKey(name: 'disable-icmp-forwarding') @Default(true) bool disableIcmpForwarding, @Default(4064) int mtu, @JsonKey(name: 'endpoint-independent-nat') @Default(false) bool endpointIndependentNat, }) = _Tun; factory Tun.fromJson(Map json) => _$TunFromJson(json); factory Tun.safeFormJson(Map? json) { if (json == null) { return defaultTun; } try { return Tun.fromJson(json); } catch (_) { return defaultTun; } } } extension TunExt on Tun { Tun getRealTun( bool bypassPrivateRoute, { String? fakeIpRange, String? fakeIpRangeV6, }) { if (system.isDesktop) { if (bypassPrivateRoute) { return copyWith( autoRoute: true, routeAddress: [], routeExcludeAddress: [ '127.0.0.0/8', '::1/128', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '169.254.0.0/16', 'fd00::/8', 'fe80::/10', ], ); } return copyWith( autoRoute: true, routeAddress: [], routeExcludeAddress: [], ); } if (bypassPrivateRoute) { return copyWith( autoRoute: true, routeAddress: List.from(defaultBypassPrivateRouteAddress), ); } return copyWith( autoRoute: true, routeAddress: [], ); } } @freezed abstract class FallbackFilter with _$FallbackFilter { const factory FallbackFilter({ @Default(false) bool geoip, @Default('CN') @JsonKey(name: 'geoip-code') String geoipCode, @Default([]) List geosite, @Default([]) List ipcidr, @Default([]) List domain, }) = _FallbackFilter; factory FallbackFilter.fromJson(Map json) => _$FallbackFilterFromJson(json); } @freezed abstract class Dns with _$Dns { const factory Dns({ @Default(true) bool enable, @Default('0.0.0.0:1053') String listen, @Default(false) @JsonKey(name: 'prefer-h3') bool preferH3, @Default(CacheAlgorithm.arc) @JsonKey(name: 'cache-algorithm') CacheAlgorithm cacheAlgorithm, @Default(true) @JsonKey(name: 'use-hosts') bool useHosts, @Default(true) @JsonKey(name: 'use-system-hosts') bool useSystemHosts, @Default(false) @JsonKey(name: 'respect-rules') bool respectRules, @Default(false) bool ipv6, @Default(['114.114.114.114']) @JsonKey(name: 'default-nameserver') List defaultNameserver, @Default(DnsMode.fakeIp) @JsonKey(name: 'enhanced-mode') DnsMode enhancedMode, @Default('198.18.0.1/15') @JsonKey(name: 'fake-ip-range') String fakeIpRange, @Default('fc00::/18') @JsonKey(name: 'fake-ip-range-v6') String fakeIpRangeV6, @Default(FilterMode.blacklist) @JsonKey(name: 'fake-ip-filter-mode') FilterMode fakeIpFilterMode, @Default([ '*', 'geosite:private', 'geosite:category-ntp', 'geosite:geolocation-cn', 'geosite:connectivity-check', ]) @JsonKey(name: 'fake-ip-filter') List fakeIpFilter, @Default(1) @JsonKey(name: 'fake-ip-ttl') int fakeIpTtl, @Default({ '+.internal.crop.com': '10.0.0.1', 'geosite:cn': '119.29.29.29', 'geosite:private': 'system', '*': 'system', }) @JsonKey(name: 'nameserver-policy') Map nameserverPolicy, @Default(['1.1.1.1']) List nameserver, @Default([]) List fallback, @Default(['https://doh.pub/dns-query#DIRECT']) @JsonKey(name: 'proxy-server-nameserver') List proxyServerNameserver, @Default([]) @JsonKey(name: 'direct-nameserver') List directNameserver, @Default(false) @JsonKey(name: 'direct-nameserver-follow-policy') bool directNameserverFollowPolicy, @Default(FallbackFilter()) @JsonKey(name: 'fallback-filter') FallbackFilter fallbackFilter, }) = _Dns; factory Dns.fromJson(Map json) => _$DnsFromJson(json); factory Dns.safeDnsFromJson(Map json) { try { return Dns.fromJson(json); } catch (_) { return const Dns(); } } } @freezed abstract class Ntp with _$Ntp { const factory Ntp({ @Default(true) bool enable, @Default(false) @JsonKey(name: 'write-to-system') bool writeToSystem, @Default('ntp.aliyun.com') String server, @Default(123) int port, @Default(60) int interval, }) = _Ntp; factory Ntp.fromJson(Map json) => _$NtpFromJson(json); factory Ntp.safeNtpFromJson(Map json) { try { return Ntp.fromJson(json); } catch (_) { return const Ntp(); } } } @freezed abstract class Experimental with _$Experimental { const factory Experimental({ @Default(true) @JsonKey(name: 'quic-go-disable-gso') bool quicGoDisableGso, @Default(true) @JsonKey(name: 'quic-go-disable-ecn') bool quicGoDisableEcn, @Default(false) @JsonKey(name: 'dialer-ip4p-convert') bool dialerIp4pConvert, }) = _Experimental; factory Experimental.fromJson(Map json) => _$ExperimentalFromJson(json); factory Experimental.safeExperimentalFromJson(Map json) { try { return Experimental.fromJson(json); } catch (_) { return const Experimental(); } } } @freezed abstract class GeoXUrl with _$GeoXUrl { const factory GeoXUrl({ @Default( 'https://fastly.jsdelivr.net/gh/appshubcc/bett-rules@release/geoip.metadb', ) String mmdb, @Default( 'https://fastly.jsdelivr.net/gh/appshubcc/bett-rules@release/GeoLite2-ASN.mmdb', ) String asn, @Default( 'https://fastly.jsdelivr.net/gh/appshubcc/bett-rules@release/geoip.dat', ) String geoip, @Default( 'https://fastly.jsdelivr.net/gh/appshubcc/bett-rules@release/geosite.dat', ) String geosite, }) = _GeoXUrl; factory GeoXUrl.fromJson(Map json) => _$GeoXUrlFromJson(json); factory GeoXUrl.safeFormJson(Map? json) { if (json == null) { return defaultGeoXUrl; } try { return GeoXUrl.fromJson(json); } catch (_) { return defaultGeoXUrl; } } } @freezed abstract class ParsedRule with _$ParsedRule { const factory ParsedRule({ required RuleAction ruleAction, String? content, String? ruleTarget, String? ruleProvider, String? subRule, @Default(false) bool noResolve, @Default(false) bool src, }) = _ParsedRule; factory ParsedRule.parseString(String value) { final splits = value.split(','); final shortSplits = splits .where((item) => !item.contains('src') && !item.contains('no-resolve')) .toList(); final ruleAction = RuleAction.values.firstWhere( (item) => item.value == shortSplits.first, orElse: () => RuleAction.DOMAIN, ); String? subRule; String? ruleTarget; if (ruleAction == RuleAction.SUB_RULE) { subRule = shortSplits.last; } else { ruleTarget = shortSplits.last; } String? content; String? ruleProvider; if (ruleAction == RuleAction.RULE_SET) { ruleProvider = shortSplits.sublist(1, shortSplits.length - 1).join(','); } else { content = shortSplits.sublist(1, shortSplits.length - 1).join(','); } return ParsedRule( ruleAction: ruleAction, content: content, src: splits.contains('src'), ruleProvider: ruleProvider, noResolve: splits.contains('no-resolve'), subRule: subRule, ruleTarget: ruleTarget, ); } } extension ParsedRuleExt on ParsedRule { String get value { if (ruleAction == RuleAction.MATCH) { return [ ruleAction.value, ruleTarget, ].join(','); } return [ ruleAction.value, ruleAction == RuleAction.RULE_SET ? ruleProvider : content, ruleAction == RuleAction.SUB_RULE ? subRule : ruleTarget, if (ruleAction.hasParams) ...[ if (src) 'src', if (noResolve) 'no-resolve', ], ].join(','); } } @freezed abstract class Rule with _$Rule { const factory Rule({required String id, required String value}) = _Rule; factory Rule.value(String value) { return Rule(value: value, id: utils.uuidV4); } factory Rule.fromJson(Map json) => _$RuleFromJson(json); } @freezed abstract class SubRule with _$SubRule { const factory SubRule({required String name}) = _SubRule; factory SubRule.fromJson(Map json) => _$SubRuleFromJson(json); } List _genRule(List? rules) { if (rules == null) { return []; } return rules.map((item) => Rule.value(item)).toList(); } List _genRuleProviders(Map json) { return json.entries.map((entry) => RuleProvider(name: entry.key)).toList(); } List _genSubRules(Map json) { return json.entries.map((entry) => SubRule(name: entry.key)).toList(); } @freezed abstract class ClashConfigSnippet with _$ClashConfigSnippet { const factory ClashConfigSnippet({ @Default([]) @JsonKey(name: 'proxy-groups') List proxyGroups, @JsonKey(fromJson: _genRule, name: 'rules') @Default([]) List rule, @JsonKey(name: 'rule-providers', fromJson: _genRuleProviders) @Default([]) List ruleProvider, @JsonKey(name: 'sub-rules', fromJson: _genSubRules) @Default([]) List subRules, }) = _ClashConfigSnippet; factory ClashConfigSnippet.fromJson(Map json) => _$ClashConfigSnippetFromJson(json); } @freezed abstract class ClashConfig with _$ClashConfig { const factory ClashConfig({ @Default(defaultMixedPort) @JsonKey(name: 'mixed-port') int mixedPort, @Default(0) @JsonKey(name: 'socks-port') int socksPort, @Default(0) @JsonKey(name: 'port') int port, @Default(0) @JsonKey(name: 'redir-port') int redirPort, @Default(0) @JsonKey(name: 'tproxy-port') int tproxyPort, @Default(Mode.rule) Mode mode, @Default(false) @JsonKey(name: 'allow-lan') bool allowLan, @Default(LogLevel.error) @JsonKey(name: 'log-level') LogLevel logLevel, @Default(false) bool ipv6, @Default(FindProcessMode.off) @JsonKey( name: 'find-process-mode', unknownEnumValue: FindProcessMode.always, ) FindProcessMode findProcessMode, @Default(defaultKeepAliveInterval) @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @Default(true) @JsonKey(name: 'unified-delay') bool unifiedDelay, @Default(true) @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @Default(defaultTun) @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @Default(defaultDns) @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @Default(defaultNtp) @JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp, @Default(defaultSniffer) @JsonKey(fromJson: Sniffer.safeSnifferFromJson) Sniffer sniffer, @Default(defaultTunnel) List tunnels, @Default(defaultExperimental) @JsonKey(fromJson: Experimental.safeExperimentalFromJson) Experimental experimental, @Default(defaultGeoXUrl) @JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) GeoXUrl geoXUrl, @Default(GeodataLoader.memconservative) @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @Default([]) @JsonKey(name: 'proxy-groups') List proxyGroups, @Default([]) List rule, @JsonKey(name: 'global-ua') String? globalUa, @Default(ExternalControllerStatus.close) @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String? secret, @JsonKey(name: 'external-ui-name') String? externalUiName, @JsonKey(name: 'external-ui-url') String? externalUiUrl, @Default({}) HostsMap hosts, }) = _ClashConfig; factory ClashConfig.fromJson(Map json) => _$ClashConfigFromJson(json); factory ClashConfig.safeFormJson(Map? json) { if (json == null) { return defaultClashConfig; } try { return ClashConfig.fromJson(json); } catch (_) { return defaultClashConfig; } } } ================================================ FILE: lib/models/common.dart ================================================ // ignore_for_file: invalid_annotation_target import 'dart:math'; import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'generated/common.freezed.dart'; part 'generated/common.g.dart'; @freezed abstract class NavigationItem with _$NavigationItem { const factory NavigationItem({ required Icon icon, required PageLabel label, final String? description, required WidgetBuilder builder, @Default(true) bool keep, String? path, @Default([NavigationItemMode.mobile, NavigationItemMode.desktop]) List modes, }) = _NavigationItem; } @freezed abstract class Package with _$Package { const factory Package({ required String packageName, required String label, required bool system, required bool internet, required int lastUpdateTime, }) = _Package; factory Package.fromJson(Map json) => _$PackageFromJson(json); } @freezed abstract class Metadata with _$Metadata { const factory Metadata({ @Default(0) int uid, @Default('') String network, @Default('') String sourceIP, @Default('') String sourcePort, @Default('') String destinationIP, @Default('') String destinationPort, @Default('') String host, DnsMode? dnsMode, @Default('') String process, @Default('') String processPath, @Default('') String remoteDestination, @Default([]) List sourceGeoIP, @Default([]) List destinationGeoIP, @Default('') String destinationIPASN, @Default('') String sourceIPASN, @Default('') String specialRules, @Default('') String specialProxy, }) = _Metadata; factory Metadata.fromJson(Map json) => _$MetadataFromJson(json); } @freezed abstract class TrackerInfo with _$TrackerInfo { const factory TrackerInfo({ required String id, @Default(0) int upload, @Default(0) int download, required DateTime start, required Metadata metadata, required List chains, required String rule, required String rulePayload, int? downloadSpeed, int? uploadSpeed, }) = _TrackerInfo; factory TrackerInfo.fromJson(Map json) => _$TrackerInfoFromJson(json); } extension TrackerInfoExt on TrackerInfo { String get desc { var text = '${metadata.network}://'; final ips = [ metadata.host, metadata.destinationIP, ].where((ip) => ip.isNotEmpty); text += ips.join('/'); text += ':${metadata.destinationPort}'; return text; } String get progressText { final process = metadata.process; final uid = metadata.uid; if (uid != 0) { return '$process($uid)'; } return process; } } String _logDateTime(dynamic _) { return DateTime.now().showFull; } // String _logId(_) { // return utils.id; // } @freezed abstract class Log with _$Log { const factory Log({ // @JsonKey(fromJson: _logId) required String id, @JsonKey(name: 'LogLevel') @Default(LogLevel.error) LogLevel logLevel, @JsonKey(name: 'Payload') @Default('') String payload, @JsonKey(fromJson: _logDateTime) required String dateTime, }) = _Log; factory Log.app(String payload) { return Log( logLevel: LogLevel.info, payload: payload, dateTime: _logDateTime(null), // id: _logId(null), ); } factory Log.fromJson(Map json) => _$LogFromJson(json); } @freezed abstract class LogsState with _$LogsState { const factory LogsState({ @Default([]) List logs, @Default([]) List keywords, @Default('') String query, @Default(false) bool autoScrollToEnd, }) = _LogsState; } extension LogsStateExt on LogsState { List get list { final lowQuery = query.toLowerCase(); return logs.where((log) { final logLevelName = log.logLevel.name; return {logLevelName}.containsAll(keywords) && ((log.payload.toLowerCase().contains(lowQuery)) || logLevelName.contains(lowQuery)); }).toList(); } } @freezed abstract class TrackerInfosState with _$TrackerInfosState { const factory TrackerInfosState({ @Default([]) List trackerInfos, @Default([]) List keywords, @Default('') String query, @Default(false) bool autoScrollToEnd, }) = _TrackerInfosState; } extension TrackerInfosStateExt on TrackerInfosState { List get list { final lowerQuery = query.toLowerCase().trim(); final lowQuery = query.toLowerCase(); return trackerInfos.where((trackerInfo) { final chains = trackerInfo.chains; final process = trackerInfo.metadata.process; final networkText = trackerInfo.metadata.network.toLowerCase(); final hostText = trackerInfo.metadata.host.toLowerCase(); final destinationIPText = trackerInfo.metadata.destinationIP .toLowerCase(); final processText = trackerInfo.metadata.process.toLowerCase(); final chainsText = chains.join('').toLowerCase(); return {...chains, process}.containsAll(keywords) && (networkText.contains(lowerQuery) || hostText.contains(lowerQuery) || destinationIPText.contains(lowQuery) || processText.contains(lowerQuery) || chainsText.contains(lowerQuery)); }).toList(); } } const defaultDavFileName = 'backup.zip'; @freezed abstract class DAV with _$DAV { const factory DAV({ required String uri, required String user, required String password, @Default(defaultDavFileName) String fileName, }) = _DAV; factory DAV.fromJson(Map json) => _$DAVFromJson(json); } @freezed abstract class FileInfo with _$FileInfo { const factory FileInfo({required int size, required DateTime lastModified}) = _FileInfo; } extension FileInfoExt on FileInfo { String get desc => '${TrafficValue(value: size).show} · ${lastModified.lastUpdateTimeDesc}'; } @freezed abstract class VersionInfo with _$VersionInfo { const factory VersionInfo({ @Default('') String clashName, @Default('') String version, }) = _VersionInfo; factory VersionInfo.fromJson(Map json) => _$VersionInfoFromJson(json); } class Traffic { int id; TrafficValue up; TrafficValue down; Traffic({int? up, int? down}) : id = DateTime.now().millisecondsSinceEpoch, up = TrafficValue(value: up), down = TrafficValue(value: down); num get speed => up.value + down.value; factory Traffic.fromMap(Map map) { return Traffic(up: map['up'], down: map['down']); } String toSpeedText() { return '↑ $up/s ↓ $down/s'; } @override String toString() { return '$up↑ $down↓'; } @override bool operator ==(Object other) => identical(this, other) || other is Traffic && runtimeType == other.runtimeType && id == other.id && up == other.up && down == other.down; @override int get hashCode => id.hashCode ^ up.hashCode ^ down.hashCode; } @immutable class TrafficValueShow { final double value; final TrafficUnit unit; const TrafficValueShow({required this.value, required this.unit}); } @freezed abstract class Proxy with _$Proxy { const factory Proxy({ required String name, required String type, String? now, }) = _Proxy; factory Proxy.fromJson(Map json) => _$ProxyFromJson(json); } @freezed abstract class Group with _$Group { const factory Group({ required GroupType type, @Default([]) List all, String? now, bool? hidden, String? testUrl, @Default('') String icon, required String name, }) = _Group; factory Group.fromJson(Map json) => _$GroupFromJson(json); } extension GroupsExt on List { Group? getGroup(String groupName) { final index = indexWhere((element) => element.name == groupName); return index != -1 ? this[index] : null; } } extension GroupExt on Group { String get realNow => now ?? ''; String getCurrentSelectedName(String proxyName) { if (type.isComputedSelected) { return realNow.isNotEmpty ? realNow : proxyName; } return proxyName.isNotEmpty ? proxyName : realNow; } } @immutable class TrafficValue { final int _value; const TrafficValue({int? value}) : _value = value ?? 0; int get value => _value; String get show => '$showValue $showUnit'; String get shortShow => '${trafficValueShow.value.fixed(decimals: 1)} $showUnit'; String get showValue => trafficValueShow.value.fixed(); String get showUnit => trafficValueShow.unit.name; TrafficValueShow get trafficValueShow { if (_value > pow(1024, 4)) { return TrafficValueShow( value: _value / pow(1024, 4), unit: TrafficUnit.TB, ); } if (_value > pow(1024, 3)) { return TrafficValueShow( value: _value / pow(1024, 3), unit: TrafficUnit.GB, ); } if (_value > pow(1024, 2)) { return TrafficValueShow( value: _value / pow(1024, 2), unit: TrafficUnit.MB, ); } if (_value > pow(1024, 1)) { return TrafficValueShow( value: _value / pow(1024, 1), unit: TrafficUnit.KB, ); } return TrafficValueShow(value: _value.toDouble(), unit: TrafficUnit.B); } @override String toString() { return '$showValue$showUnit'; } @override bool operator ==(Object other) => identical(this, other) || other is TrafficValue && runtimeType == other.runtimeType && _value == other._value; @override int get hashCode => _value.hashCode; } @freezed abstract class ColorSchemes with _$ColorSchemes { const factory ColorSchemes({ ColorScheme? lightColorScheme, ColorScheme? darkColorScheme, }) = _ColorSchemes; } extension ColorSchemesExt on ColorSchemes { ColorScheme getColorSchemeForBrightness( Brightness brightness, DynamicSchemeVariant schemeVariant, ) { if (brightness == Brightness.dark) { return darkColorScheme != null ? ColorScheme.fromSeed( seedColor: darkColorScheme!.primary, brightness: Brightness.dark, dynamicSchemeVariant: schemeVariant, ) : ColorScheme.fromSeed( seedColor: Color(defaultPrimaryColor), brightness: Brightness.dark, dynamicSchemeVariant: schemeVariant, ); } return lightColorScheme != null ? ColorScheme.fromSeed( seedColor: lightColorScheme!.primary, dynamicSchemeVariant: schemeVariant, ) : ColorScheme.fromSeed( seedColor: Color(defaultPrimaryColor), dynamicSchemeVariant: schemeVariant, ); } } class IpInfo { final String ip; final String countryCode; const IpInfo({required this.ip, required this.countryCode}); static IpInfo fromCloudflareTrace(String traceText) { // Cloudflare trace格式示例: // fl=... // h=... // ip=1.2.3.4 // ts=... // visit_scheme=https // uag=... // colo=... // sliver=none // http=http/2 // loc=US // tls=TLSv1.3 // sni=plaintext // warp=off // gateway=off // rbi=off // kex=X25519 final lines = traceText.split('\n'); String? ip; String? countryCode; for (final line in lines) { final parts = line.split('='); if (parts.length == 2) { final key = parts[0].trim(); final value = parts[1].trim(); if (key == 'ip') { ip = value; } else if (key == 'loc') { countryCode = value; } } } if (ip != null && countryCode != null) { return IpInfo(ip: ip, countryCode: countryCode); } throw const FormatException('invalid cloudflare trace format'); } IpInfo copyWith({String? ip, String? countryCode}) { return IpInfo( ip: ip ?? this.ip, countryCode: countryCode ?? this.countryCode, ); } @override String toString() { return 'IpInfo{ip: $ip, countryCode: $countryCode}'; } } @freezed abstract class HotKeyAction with _$HotKeyAction { const factory HotKeyAction({ required HotAction action, int? key, @Default({}) Set modifiers, }) = _HotKeyAction; factory HotKeyAction.fromJson(Map json) => _$HotKeyActionFromJson(json); } typedef Validator = String? Function(String? value); @freezed abstract class Field with _$Field { const factory Field({ required String label, required String value, Validator? validator, }) = _Field; } enum PopupMenuItemType { primary, danger } class PopupMenuItemData { const PopupMenuItemData({ this.icon, required this.label, required this.onPressed, }); final String label; final VoidCallback? onPressed; final IconData? icon; } @freezed abstract class TextPainterParams with _$TextPainterParams { const factory TextPainterParams({ required String? text, required double? fontSize, required double textScaleFactor, @Default(double.infinity) double maxWidth, int? maxLines, }) = _TextPainterParams; factory TextPainterParams.fromJson(Map json) => _$TextPainterParamsFromJson(json); } class CloseWindowIntent extends Intent { const CloseWindowIntent(); } @freezed abstract class Result with _$Result { const factory Result({ required T? data, required ResultType type, required String message, @Default(false) bool needRestart, }) = _Result; factory Result.success(T data, {bool needRestart = false}) => Result(data: data, type: ResultType.success, message: '', needRestart: needRestart); factory Result.error(String message) => Result(data: null, type: ResultType.error, message: message); } extension ResultExt on Result { bool get isError => type == ResultType.error; bool get isSuccess => type == ResultType.success; } @freezed abstract class Script with _$Script { const factory Script({ required String id, required String label, required String content, String? url, }) = _Script; factory Script.create({required String label, required String content, String? url}) { return Script(id: utils.uuidV4, label: label, content: content, url: url); } factory Script.fromJson(Map json) => _$ScriptFromJson(json); } ================================================ FILE: lib/models/config.dart ================================================ // ignore_for_file: invalid_annotation_target import 'package:bett_box/common/common.dart'; import 'package:bett_box/enum/enum.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'models.dart'; part 'generated/config.freezed.dart'; part 'generated/config.g.dart'; const defaultBypassDomain = [ '*jd.com', '*zhihu.com', '*zhimg.com', '*360buyimg.com', 'localhost', '*.local', '127.*', '10.*', '172.16.*', '172.17.*', '172.18.*', '172.19.*', '172.2*', '172.30.*', '172.31.*', '192.168.*', ]; const defaultAppSettingProps = AppSettingProps(); const defaultVpnProps = VpnProps(); const defaultNetworkProps = NetworkProps(); const defaultProxiesStyle = ProxiesStyle(); const defaultWindowProps = WindowProps(); const defaultAccessControl = AccessControl(); final defaultThemeProps = ThemeProps(primaryColor: defaultPrimaryColor); const List defaultDashboardWidgets = [ DashboardWidget.networkSpeed, DashboardWidget.systemProxyButton, DashboardWidget.tunButton, DashboardWidget.outboundMode, DashboardWidget.networkDetection, DashboardWidget.trafficUsage, DashboardWidget.intranetIp, DashboardWidget.memoryInfo, DashboardWidget.startButton, ]; List dashboardWidgetsSafeFormJson( List? dashboardWidgets, ) { try { return dashboardWidgets ?.map((e) => $enumDecode(_$DashboardWidgetEnumMap, e)) .toList() ?? defaultDashboardWidgets; } catch (_) { return defaultDashboardWidgets; } } @freezed abstract class AppSettingProps with _$AppSettingProps { const factory AppSettingProps({ String? locale, @Default(defaultDashboardWidgets) @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, @Default(false) bool onlyStatisticsProxy, @Default(false) bool autoLaunch, @Default(false) bool silentLaunch, @Default(true) bool smartDelayLaunch, @Default(false) bool autoRun, @Default(true) bool openLogs, @Default(true) bool closeConnections, @Default(defaultTestUrl) String testUrl, @Default(true) bool isAnimateToPage, @Default(false) bool enableNavBarHapticFeedback, @Default(true) bool autoCheckUpdate, @Default(false) bool showLabel, @Default(false) bool disclaimerAccepted, @Default(true) bool minimizeOnExit, @Default(false) bool hidden, @Default(false) bool developerMode, @Default(false) bool enableHighRefreshRate, @Default(RecoveryStrategy.compatible) RecoveryStrategy recoveryStrategy, }) = _AppSettingProps; factory AppSettingProps.fromJson(Map json) => _$AppSettingPropsFromJson(json); factory AppSettingProps.safeFromJson(Map? json) { final props = json == null ? defaultAppSettingProps : AppSettingProps.fromJson(json); return props.copyWith( minimizeOnExit: true, openLogs: true, ); } } @freezed abstract class AccessControl with _$AccessControl { const factory AccessControl({ @Default(false) bool enable, @Default(AccessControlMode.rejectSelected) AccessControlMode mode, @Default([]) List acceptList, @Default([]) List rejectList, @Default(AccessSortType.none) AccessSortType sort, @Default(true) bool isFilterSystemApp, @Default(true) bool isFilterNonInternetApp, }) = _AccessControl; factory AccessControl.fromJson(Map json) => _$AccessControlFromJson(json); } extension AccessControlExt on AccessControl { List get currentList => switch (mode) { AccessControlMode.acceptSelected => acceptList, AccessControlMode.rejectSelected => rejectList, }; } @freezed abstract class WindowProps with _$WindowProps { const factory WindowProps({ @Default(750) double width, @Default(600) double height, double? top, double? left, @Default(false) bool isLocked, }) = _WindowProps; factory WindowProps.fromJson(Map? json) => json == null ? const WindowProps() : _$WindowPropsFromJson(json); } @freezed abstract class VpnProps with _$VpnProps { const factory VpnProps({ @Default(true) bool enable, @Default(false) bool systemProxy, @Default(false) bool allowBypass, @Default(true) bool bypassPrivateRoute, @Default(true) bool dozeSuspend, @Default(false) bool smartAutoStop, @Default('') String smartAutoStopNetworks, @Default(false) bool storeFix, @Default(false) bool networkFix, @Default(false) bool disableQuic, @Default(false) bool excludeChina, @Default(false) bool fcmOptimization, @Default(false) bool quickResponse, @Default(defaultAccessControl) AccessControl accessControl, }) = _VpnProps; factory VpnProps.fromJson(Map json) => _$VpnPropsFromJson(json); factory VpnProps.safeFromJson(Map? json) { final props = json == null ? defaultVpnProps : VpnProps.fromJson(json); var safeProps = props; if (system.isAndroid) { safeProps = safeProps.copyWith(systemProxy: false); } if (safeProps.fcmOptimization && system.isAndroid) { safeProps = safeProps.copyWith(allowBypass: false); } if (safeProps.smartAutoStop && safeProps.quickResponse) { safeProps = safeProps.copyWith(quickResponse: false); } return safeProps; } } @freezed abstract class NetworkProps with _$NetworkProps { const factory NetworkProps({ @Default(false) bool systemProxy, @Default(defaultBypassDomain) List bypassDomain, @Default(true) bool bypassPrivateRoute, @Default(true) bool autoSetSystemDns, }) = _NetworkProps; factory NetworkProps.fromJson(Map? json) => json == null ? const NetworkProps() : _$NetworkPropsFromJson(json); } @freezed abstract class ProxiesStyle with _$ProxiesStyle { const factory ProxiesStyle({ @Default(ProxiesType.tab) ProxiesType type, @Default(ProxiesSortType.none) ProxiesSortType sortType, @Default(ProxiesLayout.standard) ProxiesLayout layout, @Default(ProxiesIconStyle.none) ProxiesIconStyle iconStyle, @Default(ProxyCardType.shrink) ProxyCardType cardType, @Default(DelayAnimationType.none) DelayAnimationType delayAnimation, @Default({}) Map iconMap, @Default(16) int concurrencyLimit, }) = _ProxiesStyle; factory ProxiesStyle.fromJson(Map? json) => json == null ? defaultProxiesStyle : _$ProxiesStyleFromJson(json); } @freezed abstract class TextScale with _$TextScale { const factory TextScale({ @Default(false) bool enable, @Default(1.0) double scale, }) = _TextScale; factory TextScale.fromJson(Map json) => _$TextScaleFromJson(json); } @freezed abstract class ThemeProps with _$ThemeProps { const factory ThemeProps({ int? primaryColor, @Default(defaultPrimaryColors) List primaryColors, @Default(ThemeMode.system) ThemeMode themeMode, @Default(DynamicSchemeVariant.content) DynamicSchemeVariant schemeVariant, @Default(false) bool pureBlack, @Default(TextScale()) TextScale textScale, @Default(false) bool useLightIcon, @Default(false) bool useHarmonyFont, }) = _ThemeProps; factory ThemeProps.fromJson(Map json) => _$ThemePropsFromJson(json); factory ThemeProps.safeFromJson(Map? json) { if (json == null) { return defaultThemeProps; } try { return ThemeProps.fromJson(json); } catch (_) { return defaultThemeProps; } } } @freezed abstract class ScriptProps with _$ScriptProps { const factory ScriptProps({ String? currentId, @Default([]) List ================================================ FILE: plugins/flutter_distributor/examples/hello_world/web/manifest.json ================================================ { "name": "hello_world", "short_name": "hello_world", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) project(hello_world LANGUAGES CXX) set(BINARY_NAME "hello_world") cmake_policy(SET CMP0063 NEW) set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Configure build options. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") # Flutter library and tool build rules. add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/flutter/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void RegisterPlugins(flutter::PluginRegistry* registry) { } ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/packaging/exe/custom-inno-setup-script.iss ================================================ [Setup] AppId={{APP_ID}} AppVersion={{APP_VERSION}} AppName={{DISPLAY_NAME}} AppPublisher={{PUBLISHER_NAME}} AppPublisherURL={{PUBLISHER_URL}} AppSupportURL={{PUBLISHER_URL}} AppUpdatesURL={{PUBLISHER_URL}} DefaultDirName={{INSTALL_DIR_NAME}} DisableProgramGroupPage=yes OutputDir=. OutputBaseFilename={{OUTPUT_BASE_FILENAME}} Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] {% for locale in LOCALES %} {% if locale == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %} {% if locale == 'zh' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %} {% if locale == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %} {% endfor %} [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %} Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %} [Files] Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{autoprograms}\\{{APP_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}" Name: "{autodesktop}\\{{APP_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon Name: "{userstartup}\\{{APP_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup [Run] Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: nowait postinstall skipifsilent ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/packaging/exe/make_config.yaml ================================================ # script_template: "custom-inno-setup-script.iss" app_id: 15F38A1C-4C4B-4477-99C3-1205BC28C4CC publisher_name: LeanFlutter publisher_url: https://github.com/leanflutter/flutter_distributor display_name: Hello 世界 create_desktop_icon: true launch_at_startup: true # install_dir_name: "D:\\HELLO-WORLD" locales: - zh ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/packaging/msix/make_config.yaml ================================================ display_name: HelloWorld msix_version: 1.0.0.0 # logo_path: C:\path\to\logo.png ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) project(runner LANGUAGES CXX) add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) apply_standard_settings(${BINARY_NAME}) target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" VALUE "FileDescription", "A new Flutter project." "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "hello_world" "\0" VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "hello_world.exe" "\0" VALUE "ProductName", "hello_world" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.CreateAndShow(L"hello_world", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr); if (target_length == 0) { return std::string(); } std::string utf8_string; utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include "resource.h" namespace { constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); FreeLibrary(user32_module); } } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } return OnCreate(); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } ================================================ FILE: plugins/flutter_distributor/examples/hello_world/windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size to will treat the width height passed in to this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, const Point& origin, const Size& size); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responsponds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release # flutter_distributor related dist/ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - platform: android create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { namespace "org.leanflutter.examples.multiple_flavors" compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "org.leanflutter.examples.multiple_flavors" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion 19 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } flavorDimensions "mode" productFlavors { dev { dimension "mode" } prod { dimension "mode" } prod_64 { dimension "mode" ndk { abiFilters "arm64-v8a" } } } buildTypes { release { productFlavors.dev.signingConfig signingConfigs.debug productFlavors.prod.signingConfig signingConfigs.debug productFlavors.prod_64.signingConfig signingConfigs.debug } } } flutter { source '../..' } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/main/java/org/leanflutter/examples/multiple_flavors/MainActivity.java ================================================ package org.leanflutter.examples.multiple_flavors; import io.flutter.embedding.android.FlutterActivity; public class MainActivity extends FlutterActivity { } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/distribute_options.yaml ================================================ output: dist/ # See https://mustache.github.io/mustache.5.html for string template format. artifact_name: "{{name}}{{#channel}}-{{channel}}{{/channel}}-{{build_name}}({{build_number}}){{#is_profile}}-{{build_mode}}{{/is_profile}}-{{platform}}.{{ext}}" releases: - name: release-android jobs: - name: release-android-dev package: platform: android target: apk channel: dev2 build_args: profile: true flavor: dev dart-define: APP_ENV: dev - name: release-android-prod package: platform: android target: apk channel: prod2 build_args: profile: true flavor: prod dart-define: APP_ENV: prod - name: release-android-prod64 package: platform: android target: apk channel: prod2_64 build_args: profile: true flavor: prod_64 target-platform: android-arm64 dart-define: APP_ENV: prod - name: release-android-dev-aab package: platform: android target: aab build_args: profile: true flavor: dev dart-define: APP_ENV: dev - name: release-android-prod-aab package: platform: android target: aab build_args: profile: true flavor: prod dart-define: APP_ENV: prod - name: release-android-prod64-aab package: platform: android target: aab build_args: profile: true flavor: prod_64 target-platform: android-arm64 dart-define: APP_ENV: prod - name: release-ios jobs: - name: release-ios-dev-ipa package: platform: ios target: ipa channel: dev2 build_args: export-options-plist: ios/dev_ExportOptions.plist flavor: dev dart-define: APP_ENV: dev - name: release-ios-prod-ipa package: platform: ios target: ipa channel: prod2 build_args: export-options-plist: ios/prod_ExportOptions.plist flavor: prod dart-define: APP_ENV: prod ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 11.0 ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, 'Release-dev' => :release, 'Release-prod' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/AppDelegate.h ================================================ #import #import @interface AppDelegate : FlutterAppDelegate @end ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/AppDelegate.m ================================================ #import "AppDelegate.h" #import "GeneratedPluginRegistrant.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Multiple Flavors CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(BUNDLE_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner/main.m ================================================ #import #import #import "AppDelegate.h" int main(int argc, char* argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1CAEDD5155F2EB163DB44156 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E9ECD0077873D16742718AF3 /* libPods-Runner.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0C88C752B959FEAD3300AB0E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 23EA6A7DCC1D9B995BBF4360 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 2CA062782B731FA3340C7B07 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 30E41D6617A08ACBFA85E15F /* Pods-Runner.release-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-prod.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 6A9AA014DF44CAC71B1DBFDE /* Pods-Runner.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-dev.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E9ECD0077873D16742718AF3 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 1CAEDD5155F2EB163DB44156 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, A2C8F672AF56CBDA2D1C04AD /* Pods */, B011BCA6C13E4EB47B215E83 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, ); path = Runner; sourceTree = ""; }; 97C146F11CF9000F007C117D /* Supporting Files */ = { isa = PBXGroup; children = ( 97C146F21CF9000F007C117D /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; A2C8F672AF56CBDA2D1C04AD /* Pods */ = { isa = PBXGroup; children = ( 2CA062782B731FA3340C7B07 /* Pods-Runner.debug.xcconfig */, 0C88C752B959FEAD3300AB0E /* Pods-Runner.release.xcconfig */, 23EA6A7DCC1D9B995BBF4360 /* Pods-Runner.profile.xcconfig */, 30E41D6617A08ACBFA85E15F /* Pods-Runner.release-prod.xcconfig */, 6A9AA014DF44CAC71B1DBFDE /* Pods-Runner.release-dev.xcconfig */, ); path = Pods; sourceTree = ""; }; B011BCA6C13E4EB47B215E83 /* Frameworks */ = { isa = PBXGroup; children = ( E9ECD0077873D16742718AF3 /* libPods-Runner.a */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 795BBEB30B39DB26BF559A5B /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 795BBEB30B39DB26BF559A5B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 97C146F31CF9000F007C117D /* main.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G83H824X6L; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G83H824X6L; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G83H824X6L; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; F55ED2D2280AD5CC003CBF8D /* Release-prod */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = "Release-prod"; }; F55ED2D3280AD5CC003CBF8D /* Release-prod */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G83H824X6L; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; name = "Release-prod"; }; F55ED2D4280AD5D5003CBF8D /* Release-dev */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = "Release-dev"; }; F55ED2D5280AD5D5003CBF8D /* Release-dev */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G83H824X6L; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; name = "Release-dev"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, F55ED2D4280AD5D5003CBF8D /* Release-dev */, F55ED2D2280AD5CC003CBF8D /* Release-prod */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, F55ED2D5280AD5D5003CBF8D /* Release-dev */, F55ED2D3280AD5CC003CBF8D /* Release-prod */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/dev_ExportOptions.plist ================================================ compileBitcode method ad-hoc signingStyle automatic stripSwiftSymbols teamID G83H824X6L thinning <none> ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ios/prod_ExportOptions.plist ================================================ compileBitcode method ad-hoc signingStyle automatic stripSwiftSymbols teamID G83H824X6L thinning <none> ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/lib/main.dart ================================================ import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Column( // Column is also a layout widget. It takes a list of children and // arranges them vertically. By default, it sizes itself to fit its // children horizontally, and tries to be as tall as its parent. // // Invoke "debug painting" (press "p" in the console, choose the // "Toggle Debug Paint" action from the Flutter Inspector in Android // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) // to see the wireframe for each widget. // // Column has various properties to control how it sizes itself and // how it positions its children. Here we use mainAxisAlignment to // center the children vertically; the main axis here is the vertical // axis because Columns are vertical (the cross axis would be // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Flutter/Flutter-Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Flutter/Flutter-Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import package_info_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Podfile ================================================ platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = multiple_flavors // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2024 org.leanflutter.examples. All rights reserved. ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 0640139504EBB825519BC570 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA4893A83552EBAABCA498D9 /* Pods_Runner.framework */; }; 0F3046C7A8405A4885456CCA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 771AFDF2BAA0ACD25543902A /* Pods_RunnerTests.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC10EC2044A3C60003C045; remoteInfo = Runner; }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 18CE1DE3B79E8BA6C8C5F5B4 /* Pods-Runner.profile-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-prod.xcconfig"; sourceTree = ""; }; 1C2D8297C5B081A26E4A5EF7 /* Pods-Runner.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-dev.xcconfig"; sourceTree = ""; }; 21F0E5EDEEFBAE48AC66E470 /* Pods-RunnerTests.profile-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-prod.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-prod.xcconfig"; sourceTree = ""; }; 2A747F6A39C21082F9411486 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* multiple_flavors.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = multiple_flavors.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 550DAD9A3B9C07FBAC407740 /* Pods-Runner.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-dev.xcconfig"; sourceTree = ""; }; 701F0FAB6E430954A43A0CF7 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 72779351C7D178E838823752 /* Pods-RunnerTests.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-dev.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-dev.xcconfig"; sourceTree = ""; }; 771AFDF2BAA0ACD25543902A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; AF05764EFDB89356A66BE764 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; B58363B85E9ED6606E885147 /* Pods-RunnerTests.release-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release-prod.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release-prod.xcconfig"; sourceTree = ""; }; B8C78CD30F864BD0B242D321 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; BA4893A83552EBAABCA498D9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CC3CA63941F965225A33A488 /* Pods-RunnerTests.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release-dev.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release-dev.xcconfig"; sourceTree = ""; }; D45A6BB3F7EAAA796E36F97B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; EE99E17B771005D809DE73AC /* Pods-Runner.release-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-prod.xcconfig"; sourceTree = ""; }; F8B6CAD2DDD4DD37C70F7904 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 331C80D2294CF70F00263BE5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 0F3046C7A8405A4885456CCA /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 0640139504EBB825519BC570 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C80D7294CF71000263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 54901D1B7AF2B05E9EA35CB1 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* multiple_flavors.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; 54901D1B7AF2B05E9EA35CB1 /* Pods */ = { isa = PBXGroup; children = ( AF05764EFDB89356A66BE764 /* Pods-Runner.debug.xcconfig */, F8B6CAD2DDD4DD37C70F7904 /* Pods-Runner.release.xcconfig */, B8C78CD30F864BD0B242D321 /* Pods-Runner.profile.xcconfig */, 2A747F6A39C21082F9411486 /* Pods-RunnerTests.debug.xcconfig */, D45A6BB3F7EAAA796E36F97B /* Pods-RunnerTests.release.xcconfig */, 701F0FAB6E430954A43A0CF7 /* Pods-RunnerTests.profile.xcconfig */, EE99E17B771005D809DE73AC /* Pods-Runner.release-prod.xcconfig */, B58363B85E9ED6606E885147 /* Pods-RunnerTests.release-prod.xcconfig */, 1C2D8297C5B081A26E4A5EF7 /* Pods-Runner.release-dev.xcconfig */, CC3CA63941F965225A33A488 /* Pods-RunnerTests.release-dev.xcconfig */, 550DAD9A3B9C07FBAC407740 /* Pods-Runner.profile-dev.xcconfig */, 18CE1DE3B79E8BA6C8C5F5B4 /* Pods-Runner.profile-prod.xcconfig */, 72779351C7D178E838823752 /* Pods-RunnerTests.profile-dev.xcconfig */, 21F0E5EDEEFBAE48AC66E470 /* Pods-RunnerTests.profile-prod.xcconfig */, ); path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( BA4893A83552EBAABCA498D9 /* Pods_Runner.framework */, 771AFDF2BAA0ACD25543902A /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C80D4294CF70F00263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 4239D5188C53CC88E92CEEE6 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C80DA294CF71000263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 28BB9CAB224434EC9D1D6B6E /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, D4A182524C116952FF1A809D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* multiple_flavors.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 33CC10EC2044A3C60003C045; }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C80D3294CF70F00263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 28BB9CAB224434EC9D1D6B6E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; 4239D5188C53CC88E92CEEE6 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; D4A182524C116952FF1A809D /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C80D1294CF70F00263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC10EC2044A3C60003C045 /* Runner */; targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 2A747F6A39C21082F9411486 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multiple_flavors.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multiple_flavors"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = D45A6BB3F7EAAA796E36F97B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multiple_flavors.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multiple_flavors"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 701F0FAB6E430954A43A0CF7 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multiple_flavors.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multiple_flavors"; }; name = Profile; }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; F5C5D0032C1DC6380057B2C4 /* Release-prod */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = "Release-prod"; }; F5C5D0042C1DC6380057B2C4 /* Release-prod */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = "Release-prod"; }; F5C5D0052C1DC6380057B2C4 /* Release-prod */ = { isa = XCBuildConfiguration; baseConfigurationReference = B58363B85E9ED6606E885147 /* Pods-RunnerTests.release-prod.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multiple_flavors.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multiple_flavors"; }; name = "Release-prod"; }; F5C5D0062C1DC6380057B2C4 /* Release-prod */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = "Release-prod"; }; F5C5D0072C1DC64F0057B2C4 /* Release-dev */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = "Release-dev"; }; F5C5D0082C1DC64F0057B2C4 /* Release-dev */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = "Release-dev"; }; F5C5D0092C1DC64F0057B2C4 /* Release-dev */ = { isa = XCBuildConfiguration; baseConfigurationReference = CC3CA63941F965225A33A488 /* Pods-RunnerTests.release-dev.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multiple_flavors.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multiple_flavors"; }; name = "Release-dev"; }; F5C5D00A2C1DC64F0057B2C4 /* Release-dev */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = "Release-dev"; }; F5C5D00F2C1DC7CF0057B2C4 /* Profile-prod */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = "Profile-prod"; }; F5C5D0102C1DC7CF0057B2C4 /* Profile-prod */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = "Profile-prod"; }; F5C5D0112C1DC7CF0057B2C4 /* Profile-prod */ = { isa = XCBuildConfiguration; baseConfigurationReference = 21F0E5EDEEFBAE48AC66E470 /* Pods-RunnerTests.profile-prod.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multiple_flavors.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multiple_flavors"; }; name = "Profile-prod"; }; F5C5D0122C1DC7CF0057B2C4 /* Profile-prod */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = "Profile-prod"; }; F5C5D0132C1DC7DA0057B2C4 /* Profile-dev */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = "Profile-dev"; }; F5C5D0142C1DC7DA0057B2C4 /* Profile-dev */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = "Profile-dev"; }; F5C5D0152C1DC7DA0057B2C4 /* Profile-dev */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72779351C7D178E838823752 /* Pods-RunnerTests.profile-dev.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.leanflutter.examples.multipleFlavors.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multiple_flavors.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multiple_flavors"; }; name = "Profile-dev"; }; F5C5D0162C1DC7DA0057B2C4 /* Profile-dev */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = "Profile-dev"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C80DB294CF71000263BE5 /* Debug */, 331C80DC294CF71000263BE5 /* Release */, F5C5D0092C1DC64F0057B2C4 /* Release-dev */, F5C5D0052C1DC6380057B2C4 /* Release-prod */, 331C80DD294CF71000263BE5 /* Profile */, F5C5D0152C1DC7DA0057B2C4 /* Profile-dev */, F5C5D0112C1DC7CF0057B2C4 /* Profile-prod */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, F5C5D0072C1DC64F0057B2C4 /* Release-dev */, F5C5D0032C1DC6380057B2C4 /* Release-prod */, 338D0CE9231458BD00FA5F75 /* Profile */, F5C5D0132C1DC7DA0057B2C4 /* Profile-dev */, F5C5D00F2C1DC7CF0057B2C4 /* Profile-prod */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, F5C5D0082C1DC64F0057B2C4 /* Release-dev */, F5C5D0042C1DC6380057B2C4 /* Release-prod */, 338D0CEA231458BD00FA5F75 /* Profile */, F5C5D0142C1DC7DA0057B2C4 /* Profile-dev */, F5C5D0102C1DC7CF0057B2C4 /* Profile-prod */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, F5C5D00A2C1DC64F0057B2C4 /* Release-dev */, F5C5D0062C1DC6380057B2C4 /* Release-prod */, 338D0CEB231458BD00FA5F75 /* Profile */, F5C5D0162C1DC7DA0057B2C4 /* Profile-dev */, F5C5D0122C1DC7CF0057B2C4 /* Profile-prod */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/RunnerTests/RunnerTests.swift ================================================ import Cocoa import FlutterMacOS import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/macos/packaging/dmg/make_config.yaml ================================================ title: multiple_flavors contents: - x: 448 y: 344 type: link path: "/Applications" - x: 192 y: 344 type: file path: multiple_flavors.app ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/.gitignore ================================================ /node_modules /oh_modules /local.properties /.idea **/build /.hvigor .cxx /.clangd /.clang-format /.clang-tidy **/.test *.har **/BuildProfile.ets **/oh-package-lock.json5 **/src/main/resources/rawfile/flutter_assets/ **/libs/arm64-v8a/libapp.so **/libs/arm64-v8a/libflutter.so **/libs/arm64-v8a/libvmservice_snapshot.so har/flutter.har oh-package-lock.json5 dta ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/AppScope/app.json5 ================================================ { "app": { "bundleName": "org.leanflutter.examples.multiple_flavors", "vendor": "example", "versionCode": 1000000, "versionName": "1.0.0", "icon": "$media:app_icon", "label": "$string:app_name" } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/AppScope/resources/base/element/string.json ================================================ { "string": [ { "name": "app_name", "value": "multiple_flavors" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/build-profile.json5 ================================================ { "app": { "signingConfigs": [], "products": [ { "name": "default", "signingConfig": "default", // replace with your own signing config in production "compatibleSdkVersion": "5.0.0(12)", "runtimeOS": "HarmonyOS", }, { "name": "prod", "signingConfig": "default", // replace with your own signing config in production "compatibleSdkVersion": "5.0.0(12)", "runtimeOS": "HarmonyOS", "buildOption": { "debuggable": false } }, ] }, "modules": [ { "name": "entry", "srcPath": "./entry", "targets": [ { "name": "default", "applyToProducts": [ "default", ] }, { "name": "prod", // target name defined in build-profile.json5 in entry module "applyToProducts": [ "prod", // apply to 'prod' product defined above ] } ] } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/.gitignore ================================================ /node_modules /oh_modules /.preview /build /.cxx /.test ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/build-profile.json5 ================================================ { "apiType": 'stageMode', "buildOption": { }, "targets": [ { "name": "default", "runtimeOS": "HarmonyOS" }, { "name": "prod", "runtimeOS": "HarmonyOS" }, ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/hvigorfile.ts ================================================ // Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. export { hapTasks } from '@ohos/hvigor-ohos-plugin'; ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/oh-package.json5 ================================================ { "name": "entry", "version": "1.0.0", "description": "Please describe the basic information.", "main": "", "author": "", "license": "", "dependencies": {} } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/ets/entryability/EntryAbility.ets ================================================ import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos'; import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant'; export default class EntryAbility extends FlutterAbility { configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine) } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/ets/pages/Index.ets ================================================ import common from '@ohos.app.ability.common'; import { FlutterPage } from '@ohos/flutter_ohos' let storage = LocalStorage.getShared() const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS' @Entry(storage) @Component struct Index { private context = getContext(this) as common.UIAbilityContext @LocalStorageLink('viewId') viewId: string = ""; build() { Column() { FlutterPage({ viewId: this.viewId }) } } onBackPress(): boolean { this.context.eventHub.emit(EVENT_BACK_PRESS) return true } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets ================================================ import { FlutterEngine, Log } from '@ohos/flutter_ohos'; /** * Generated file. Do not edit. * This file is generated by the Flutter tool based on the * plugins that support the Ohos platform. */ const TAG = "GeneratedPluginRegistrant"; export class GeneratedPluginRegistrant { static registerWith(flutterEngine: FlutterEngine) { try { } catch (e) { Log.e( TAG, "Tried to register plugins with FlutterEngine (" + flutterEngine + ") failed."); Log.e(TAG, "Received exception while registering", e); } } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/module.json5 ================================================ { "module": { "name": "entry", "type": "entry", "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": [ "phone" ], "deliveryWithInstall": true, "installationFree": false, "pages": "$profile:main_pages", "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "$string:EntryAbility_desc", "icon": "$media:icon", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:icon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "entities": [ "entity.system.home" ], "actions": [ "action.system.home" ] } ] } ], "requestPermissions": [ {"name" : "ohos.permission.INTERNET"}, ] } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/resources/base/element/color.json ================================================ { "color": [ { "name": "start_window_background", "value": "#FFFFFF" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/resources/base/element/string.json ================================================ { "string": [ { "name": "module_desc", "value": "module description" }, { "name": "EntryAbility_desc", "value": "description" }, { "name": "EntryAbility_label", "value": "multiple_flavors" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/resources/base/profile/buildinfo.json5 ================================================ { "string": [ { "name": "enable_impeller", "value": "true" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/resources/base/profile/main_pages.json ================================================ { "src": [ "pages/Index" ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/resources/en_US/element/string.json ================================================ { "string": [ { "name": "module_desc", "value": "module description" }, { "name": "EntryAbility_desc", "value": "description" }, { "name": "EntryAbility_label", "value": "multiple_flavors" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/main/resources/zh_CN/element/string.json ================================================ { "string": [ { "name": "module_desc", "value": "模块描述" }, { "name": "EntryAbility_desc", "value": "description" }, { "name": "EntryAbility_label", "value": "multiple_flavors" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/ets/test/Ability.test.ets ================================================ import hilog from '@ohos.hilog'; import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium' export default function abilityTest() { describe('ActsAbilityTest', function () { // Defines a test suite. Two parameters are supported: test suite name and test suite function. beforeAll(function () { // Presets an action, which is performed only once before all test cases of the test suite start. // This API supports only one parameter: preset action function. }) beforeEach(function () { // Presets an action, which is performed before each unit test case starts. // The number of execution times is the same as the number of test cases defined by **it**. // This API supports only one parameter: preset action function. }) afterEach(function () { // Presets a clear action, which is performed after each unit test case ends. // The number of execution times is the same as the number of test cases defined by **it**. // This API supports only one parameter: clear action function. }) afterAll(function () { // Presets a clear action, which is performed after all test cases of the test suite end. // This API supports only one parameter: clear action function. }) it('assertContain',0, function () { // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); let a = 'abc' let b = 'b' // Defines a variety of assertion methods, which are used to declare expected boolean conditions. expect(a).assertContain(b) expect(a).assertEqual(a) }) }) } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/ets/test/List.test.ets ================================================ import abilityTest from './Ability.test' export default function testsuite() { abilityTest() } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets ================================================ import UIAbility from '@ohos.app.ability.UIAbility'; import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; import hilog from '@ohos.hilog'; import { Hypium } from '@ohos/hypium'; import testsuite from '../test/List.test'; import window from '@ohos.window'; export default class TestAbility extends UIAbility { onCreate(want, launchParam) { hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:'+ JSON.stringify(launchParam) ?? ''); var abilityDelegator: any abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() var abilityDelegatorArguments: any abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite) } onDestroy() { hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); } onWindowStageCreate(windowStage: window.WindowStage) { hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); windowStage.loadContent('testability/pages/Index', (err, data) => { if (err.code) { hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); return; } hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); }); } onWindowStageDestroy() { hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); } onForeground() { hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); } onBackground() { hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets ================================================ import hilog from '@ohos.hilog'; @Entry @Component struct Index { aboutToAppear() { hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility index aboutToAppear'); } @State message: string = 'Hello World' build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) Button() { Text('next page') .fontSize(20) .fontWeight(FontWeight.Bold) }.type(ButtonType.Capsule) .margin({ top: 20 }) .backgroundColor('#0D9FFB') .width('35%') .height('5%') .onClick(()=>{ }) } .width('100%') } .height('100%') } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts ================================================ import hilog from '@ohos.hilog'; import TestRunner from '@ohos.application.testRunner'; import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; var abilityDelegator = undefined var abilityDelegatorArguments = undefined async function onAbilityCreateCallback() { hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); } async function addAbilityMonitorCallback(err: any) { hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); } export default class OpenHarmonyTestRunner implements TestRunner { constructor() { } onPrepare() { hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); } async onRun() { hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() var testAbilityName = abilityDelegatorArguments.bundleName + '.TestAbility' let lMonitor = { abilityName: testAbilityName, onAbilityCreate: onAbilityCreateCallback, }; abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) var cmd = 'aa start -d 0 -a TestAbility' + ' -b ' + abilityDelegatorArguments.bundleName var debug = abilityDelegatorArguments.parameters['-D'] if (debug == 'true') { cmd += ' -D' } hilog.info(0x0000, 'testTag', 'cmd : %{public}s', cmd); abilityDelegator.executeShellCommand(cmd, (err: any, d: any) => { hilog.info(0x0000, 'testTag', 'executeShellCommand : err : %{public}s', JSON.stringify(err) ?? ''); hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.stdResult ?? ''); hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.exitCode ?? ''); }) hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/module.json5 ================================================ { "module": { "name": "entry_test", "type": "feature", "description": "$string:module_test_desc", "mainElement": "TestAbility", "deviceTypes": [ "phone" ], "deliveryWithInstall": true, "installationFree": false, "pages": "$profile:test_pages", "abilities": [ { "name": "TestAbility", "srcEntry": "./ets/testability/TestAbility.ets", "description": "$string:TestAbility_desc", "icon": "$media:icon", "label": "$string:TestAbility_label", "exported": true, "startWindowIcon": "$media:icon", "startWindowBackground": "$color:start_window_background", "skills": [ { "actions": [ "action.system.home" ], "entities": [ "entity.system.home" ] } ] } ] } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/resources/base/element/color.json ================================================ { "color": [ { "name": "start_window_background", "value": "#FFFFFF" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/resources/base/element/string.json ================================================ { "string": [ { "name": "module_test_desc", "value": "test ability description" }, { "name": "TestAbility_desc", "value": "the test ability" }, { "name": "TestAbility_label", "value": "test label" } ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json ================================================ { "src": [ "testability/pages/Index" ] } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/hvigor/hvigor-config.json5 ================================================ { "modelVersion": "5.0.0", "dependencies": { } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/hvigorfile.ts ================================================ import { appTasks } from '@ohos/hvigor-ohos-plugin'; export default { system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/ohos/oh-package.json5 ================================================ { "modelVersion": "5.0.0", "name": "multiple_flavors", "version": "1.0.0", "description": "Please describe the basic information.", "main": "", "author": "", "license": "", "dependencies": { "@ohos/flutter_ohos": "file:./har/flutter.har" }, "devDependencies": { "@ohos/hypium": "1.0.6" }, "overrides": { "@ohos/flutter_ohos": "file:./har/flutter.har" } } ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/pubspec.yaml ================================================ name: multiple_flavors description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: sdk: ">=2.16.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter package_info_plus: ^4.0.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^1.0.0 flutter_flavorizr: ^2.1.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/release.sh ================================================ #!/bin/bash dart ../../packages/flutter_distributor/bin/main.dart release --name $1 --skip-clean ================================================ FILE: plugins/flutter_distributor/examples/multiple_flavors/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:multiple_flavors/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } ================================================ FILE: plugins/flutter_distributor/melos.yaml ================================================ name: flutter_distributor repository: https://github.com/leanflutter/flutter_distributor packages: - apps/** - examples/** - packages/** command: bootstrap: # Uses the pubspec_overrides.yaml instead of having Melos modifying the lock file. usePubspecOverrides: true scripts: analyze: exec: flutter analyze --fatal-infos description: Run `flutter analyze` for all packages. test: exec: flutter test description: Run `flutter test` for a specific package. packageFilters: dirExists: - test format: exec: dart format . --fix description: Run `dart format` for all packages. format-check: exec: dart format . --fix --set-exit-if-changed description: Run `dart format` checks for all packages. fix: exec: dart fix . --apply description: Run `dart fix` for all packages. dependency_validator: exec: flutter pub run dependency_validator packageFilters: dependsOn: - dependency_validator activate: run: melos exec --scope="flutter_distributor" -- dart pub global activate -s path . build_apk: run: melos exec --scope="hello_world" -- flutter_distributor release --name dev --jobs android-apk --skip-clean build_ipa: run: melos exec --scope="hello_world" -- flutter_distributor release --name dev --jobs ios-ipa --skip-clean ================================================ FILE: plugins/flutter_distributor/packages/fastforge/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages .flutter-plugins .flutter-plugins-dependencies ================================================ FILE: plugins/flutter_distributor/packages/fastforge/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/fastforge/analysis_options.yaml ================================================ include: package:mostly_reasonable_lints/analysis_options.yaml linter: rules: avoid_print: false ================================================ FILE: plugins/flutter_distributor/packages/fastforge/bin/main.dart ================================================ import 'package:unified_distributor/unified_distributor.dart'; Future main(List args) async { final cli = UnifiedDistributorCommandLineInterface( 'fastforge', 'Package and publish your apps with ease.', packageName: 'fastforge', displayName: 'Fastforge', ); return await cli.run(args); } ================================================ FILE: plugins/flutter_distributor/packages/fastforge/lib/fastforge.dart ================================================ library fastforge; import 'package:unified_distributor/unified_distributor.dart'; /// The main class for the Fastforge package. /// /// This class extends the [UnifiedDistributor] class and provides a /// default implementation for the [UnifiedDistributor] class. class Fastforge extends UnifiedDistributor { /// Creates a new instance of the Fastforge class. Fastforge() : super('fastforge', 'Fastforge'); } ================================================ FILE: plugins/flutter_distributor/packages/fastforge/pubspec.yaml ================================================ name: fastforge description: A powerful and efficient tool for packaging and publishing your applications with ease. version: 0.6.0 homepage: https://fastforge.dev repository: https://github.com/fastforgedev/fastforge/tree/main/packages/fastforge issue_tracker: https://github.com/fastforgedev/fastforge/issues platforms: linux: macos: windows: environment: sdk: ">=2.16.0 <4.0.0" dependencies: unified_distributor: ^0.2.0 dev_dependencies: dependency_validator: ^3.0.0 mostly_reasonable_lints: ^0.1.2 test: ^1.23.1 executables: fastforge: main ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/analysis_options.yaml ================================================ include: package:mostly_reasonable_lints/analysis_options.yaml linter: rules: avoid_print: false ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/flutter_app_builder.dart ================================================ library flutter_app_builder; export 'src/build_config.dart'; export 'src/build_result.dart'; export 'src/builders/app_builder.dart'; export 'src/builders/builders.dart'; export 'src/flutter_app_builder.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/build_config.dart ================================================ enum BuildMode { profile, release } class BuildConfig { BuildConfig({ this.arguments = const {}, }); final Map arguments; BuildMode get mode { return arguments.containsKey('profile') ? BuildMode.profile : BuildMode.release; } String? get flavor { return arguments['flavor']; } Map toJson() { return { 'mode': mode.name, 'flavor': flavor, 'arguments': arguments, }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/build_error.dart ================================================ class BuildError extends Error { BuildError([this.message]); final String? message; @override String toString() { var message = this.message; return (message != null) ? 'BuildError: $message' : 'BuildError'; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/build_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; abstract class BuildResult { BuildResult( this.config, { this.duration, this.outputFiles = const [], }); final BuildConfig config; Duration? duration; Directory get outputDirectory; List outputFiles; Map toJson() { return { 'config': config.toJson(), 'outputDirectory': outputDirectory.path, 'duration': duration?.inMilliseconds, 'outputFiles': outputFiles.map((e) => e.path).toList(), }..removeWhere((key, value) => value == null); } } abstract class BuildResultResolver { BuildResult resolve(BuildConfig config); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/android/app_builder_android.dart ================================================ import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/android/build_android_result.dart'; import 'package:flutter_app_builder/src/builders/app_builder.dart'; class AppBuilderAndroid extends AppBuilder { AppBuilderAndroid(this.target); factory AppBuilderAndroid.apk() { return AppBuilderAndroid('apk'); } factory AppBuilderAndroid.aab() { return AppBuilderAndroid('aab'); } @override String get platform => 'android'; @override bool get isSupportedOnCurrentPlatform => true; @override BuildResultResolver get resultResolver => BuildAndroidResultResolver(target); @override String get buildSubcommand => target == 'aab' ? 'appbundle' : 'apk'; final String target; @override bool match(String platform, [String? target]) { return this.platform == platform && this.target == target; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/android/build_android_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:glob/glob.dart'; import 'package:glob/list_local_fs.dart'; import 'package:recase/recase.dart'; class BuildAndroidResultResolver extends BuildResultResolver { BuildAndroidResultResolver( this.target, ) : _actualResultResolver = target == 'aab' ? _BuildAndroidAabResultResolver() : _BuildAndroidApkResultResolver(); factory BuildAndroidResultResolver.apk() { return BuildAndroidResultResolver('apk'); } factory BuildAndroidResultResolver.aab() { return BuildAndroidResultResolver('aab'); } final String target; late BuildResultResolver _actualResultResolver; @override BuildResult resolve(BuildConfig config) { return _actualResultResolver.resolve(config); } } class BuildAndroidResult extends BuildResult { BuildAndroidResult(this.target, BuildConfig config) : _actualResult = target == 'aab' ? _BuildAndroidAabResult(config) : _BuildAndroidApkResult(config), super(config); factory BuildAndroidResult.apk(BuildConfig config) { return BuildAndroidResult('apk', config); } factory BuildAndroidResult.aab(BuildConfig config) { return BuildAndroidResult( 'aab', config, ); } final String target; late BuildResult _actualResult; @override Directory get outputDirectory => _actualResult.outputDirectory; } class _BuildAndroidAabResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config) { final r = _BuildAndroidAabResult(config); final String pattern = [ '${r.outputDirectory.path}/**', config.flavor != null ? '-${config.flavor}' : '', '-${config.mode.name}.aab', ].join(); r.outputFiles = Glob(pattern).listSync().map((e) => File(e.path)).toList(); return r; } } class _BuildAndroidAabResult extends BuildResult { _BuildAndroidAabResult(BuildConfig config) : super(config); @override Directory get outputDirectory { String buildMode = config.mode.name; String path = 'build/app/outputs/bundle/$buildMode'; if (config.flavor != null) { buildMode = ReCase(buildMode).sentenceCase; path = 'build/app/outputs/bundle/${config.flavor}$buildMode'; } return Directory(path); } } class _BuildAndroidApkResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config, {Duration? duration}) { final r = _BuildAndroidApkResult(config)..duration = duration; final String pattern = [ '${r.outputDirectory.path}/**', config.flavor != null ? '-${config.flavor}' : '', '-${config.mode.name}.apk', ].join(); r.outputFiles = Glob(pattern).listSync().map((e) => File(e.path)).toList(); return r; } } class _BuildAndroidApkResult extends BuildResult { _BuildAndroidApkResult(BuildConfig config) : super(config); @override Directory get outputDirectory { return Directory('build/app/outputs/flutter-apk'); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/app_builder.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_error.dart'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/commands/flutter.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; abstract class AppBuilder { String get platform => throw UnimplementedError(); bool get isSupportedOnCurrentPlatform => throw UnimplementedError(); BuildResultResolver get resultResolver; String get buildSubcommand => platform; String get appName => pubspec.name; Version get appVersion => pubspec.version!; String get appBuildName => appVersion.toString().split('+').first; String get appBuildNumber => appVersion.toString().split('+').last; Pubspec? _pubspec; Pubspec get pubspec { if (_pubspec == null) { final yamlString = File('pubspec.yaml').readAsStringSync(); _pubspec = Pubspec.parse(yamlString); } return _pubspec!; } bool match(String platform, [String? target]) { return this.platform == platform; } Future build({ required Map arguments, Map? environment, }) async { final time = Stopwatch()..start(); BuildConfig config = BuildConfig(arguments: arguments); List buildArguments = []; for (String key in config.arguments.keys) { dynamic value = config.arguments[key]; if (value == null || value is bool) { buildArguments.add('--$key'); } else if (value is Map) { for (String subKey in value.keys) { if(key == "dart-define"){ buildArguments.add('--$key=$subKey=${value[subKey]}'); }else{ buildArguments.addAll(['--$key', '$subKey=${value[subKey]}']); } } } else { buildArguments.addAll(['--$key', value]); } } buildArguments.addAll([ '--dart-define', 'FLUTTER_BUILD_NAME=$appBuildName', '--dart-define', 'FLUTTER_BUILD_NUMBER=$appBuildNumber', ]); ProcessResult processResult = await flutter.withEnv(environment).build( [buildSubcommand, ...buildArguments], ); if (processResult.exitCode != 0) { throw BuildError('${processResult.stderr}'); } return resultResolver.resolve(config)..duration = time.elapsed; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/builders.dart ================================================ export 'android/app_builder_android.dart'; export 'android/build_android_result.dart'; export 'app_builder.dart'; export 'ios/app_builder_ios.dart'; export 'ios/build_ios_result.dart'; export 'linux/app_builder_linux.dart'; export 'linux/build_linux_result.dart'; export 'macos/app_builder_macos.dart'; export 'macos/build_macos_result.dart'; export 'web/app_builder_web.dart'; export 'web/build_web_result.dart'; export 'windows/app_builder_windows.dart'; export 'windows/build_windows_result.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/ios/app_builder_ios.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_error.dart'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/app_builder.dart'; import 'package:flutter_app_builder/src/builders/ios/build_ios_result.dart'; class AppBuilderIos extends AppBuilder { @override String get platform => 'ios'; @override bool get isSupportedOnCurrentPlatform => Platform.isMacOS; @override BuildResultResolver get resultResolver => BuildIosResultResolver(); @override String get buildSubcommand => 'ipa'; @override Future build({ required Map arguments, Map? environment, }) { if (!arguments.containsKey('export-options-plist') && !arguments.containsKey('export-method')) { throw BuildError( 'Missing `export-options-plist` or `export-method` build argument.', ); } return super.build( arguments: arguments, environment: environment, ); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/ios/build_ios_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:glob/glob.dart'; import 'package:glob/list_local_fs.dart'; class BuildIosResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config, {Duration? duration}) { final r = BuildIosResult(config); final String pattern = '${r.outputDirectory.path}/**.ipa'; List entities = Glob(pattern).listSync(); List pkgFiles = (entities.map((e) => File(e.path)).toList()) ..sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync())); r.outputFiles = [pkgFiles.first]; return r; } } class BuildIosResult extends BuildResult { BuildIosResult(BuildConfig config) : super(config); @override Directory get outputDirectory { String path = 'build/ios/ipa'; return Directory(path); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/linux/app_builder_linux.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/app_builder.dart'; import 'package:flutter_app_builder/src/builders/linux/build_linux_result.dart'; class AppBuilderLinux extends AppBuilder { @override String get platform => 'linux'; @override bool get isSupportedOnCurrentPlatform => Platform.isLinux; @override BuildResultResolver get resultResolver => BuildLinuxResultResolver(); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/linux/build_linux_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_result.dart'; class BuildLinuxResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config, {Duration? duration}) { return BuildLinuxResult(config)..duration = duration; } } class BuildLinuxResult extends BuildResult { BuildLinuxResult(BuildConfig config) : super(config); String? _arch; String get arch { if (_arch == null) { ProcessResult r = Process.runSync('uname', ['-m']); if ('${r.stdout}'.trim() == 'aarch64') { _arch = 'arm64'; } else { _arch = 'x64'; } } return _arch!; } set arch(String value) { _arch = value; } @override Directory get outputDirectory { String buildMode = config.mode.name; String path = 'build/linux/$arch/$buildMode/bundle'; return Directory(path); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/macos/app_builder_macos.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/app_builder.dart'; import 'package:flutter_app_builder/src/builders/macos/build_macos_result.dart'; class AppBuilderMacOs extends AppBuilder { @override String get platform => 'macos'; @override bool get isSupportedOnCurrentPlatform => Platform.isMacOS; @override BuildResultResolver get resultResolver => BuildMacOsResultResolver(); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/macos/build_macos_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:glob/glob.dart'; import 'package:glob/list_local_fs.dart'; import 'package:recase/recase.dart'; class BuildMacOsResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config) { final r = BuildMacOsResult(config); final String pattern = '${r.outputDirectory.path}/*.app'; r.outputFiles = Glob(pattern).listSync().map((e) => File(e.path)).toList(); return r; } } class BuildMacOsResult extends BuildResult { BuildMacOsResult(BuildConfig config) : super(config); @override Directory get outputDirectory { String buildMode = ReCase(config.mode.name).sentenceCase; String path = 'build/macos/Build/Products/$buildMode'; return Directory(path); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/ohos/app_builder_ohos.dart ================================================ import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/app_builder.dart'; import 'package:flutter_app_builder/src/builders/ohos/build_ohos_result.dart'; class AppBuilderOhos extends AppBuilder { AppBuilderOhos(this.target); factory AppBuilderOhos.hap() { return AppBuilderOhos('hap'); } factory AppBuilderOhos.app() { return AppBuilderOhos('app'); } @override String get platform => 'ohos'; @override bool get isSupportedOnCurrentPlatform => true; @override BuildResultResolver get resultResolver => BuildOhosResultResolver(target); @override String get buildSubcommand => target == 'hap' ? 'hap' : 'app'; final String target; @override bool match(String platform, [String? target]) { return this.platform == platform && this.target == target; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/ohos/build_ohos_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:glob/glob.dart'; import 'package:glob/list_local_fs.dart'; class BuildOhosResultResolver extends BuildResultResolver { BuildOhosResultResolver( this.target, ) : _actualResultResolver = target == 'hap' ? _BuildOhosHapResultResolver() : _BuildOhosAppResultResolver(); factory BuildOhosResultResolver.hap() { return BuildOhosResultResolver('hap'); } factory BuildOhosResultResolver.app() { return BuildOhosResultResolver('app'); } final String target; late BuildResultResolver _actualResultResolver; @override BuildResult resolve(BuildConfig config) { return _actualResultResolver.resolve(config); } } class BuildOhosResult extends BuildResult { BuildOhosResult(this.target, BuildConfig config) : _actualResult = target == 'hap' ? _BuildOhosHapResult(config) : _BuildOhosAppResult(config), super(config); factory BuildOhosResult.hap(BuildConfig config) { return BuildOhosResult('hap', config); } factory BuildOhosResult.app(BuildConfig config) { return BuildOhosResult('app', config); } final String target; late BuildResult _actualResult; @override Directory get outputDirectory => _actualResult.outputDirectory; } class _BuildOhosHapResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config) { final r = _BuildOhosHapResult(config); final String pattern = [ '${r.outputDirectory.path}/**', '-${config.flavor ?? 'default'}', '-signed.hap', ].join(); r.outputFiles = Glob(pattern).listSync().map((e) => File(e.path)).toList(); return r; } } class _BuildOhosHapResult extends BuildResult { _BuildOhosHapResult(BuildConfig config) : super(config); @override Directory get outputDirectory { String flavor = config.flavor ?? 'default'; String path = 'ohos/entry/build/$flavor/outputs/$flavor'; return Directory(path); } } class _BuildOhosAppResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config) { final r = _BuildOhosAppResult(config); final String pattern = [ '${r.outputDirectory.path}/**', '-${config.flavor ?? 'default'}', '-signed.app', ].join(); r.outputFiles = Glob(pattern).listSync().map((e) => File(e.path)).toList(); return r; } } class _BuildOhosAppResult extends BuildResult { _BuildOhosAppResult(BuildConfig config) : super(config); @override Directory get outputDirectory { String flavor = config.flavor ?? 'default'; String path = 'ohos/build/outputs/$flavor'; return Directory(path); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/web/app_builder_web.dart ================================================ import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/app_builder.dart'; import 'package:flutter_app_builder/src/builders/web/build_web_result.dart'; class AppBuilderWeb extends AppBuilder { @override String get platform => 'web'; @override bool get isSupportedOnCurrentPlatform => true; @override BuildResultResolver get resultResolver => BuildWebResultResolver(); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/web/build_web_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_result.dart'; class BuildWebResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config, {Duration? duration}) { return BuildWebResult(config)..duration = duration; } } class BuildWebResult extends BuildResult { BuildWebResult(BuildConfig config) : super(config); @override Directory get outputDirectory { String path = 'build/web'; return Directory(path); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/windows/app_builder_windows.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/app_builder.dart'; import 'package:flutter_app_builder/src/builders/windows/build_windows_result.dart'; class AppBuilderWindows extends AppBuilder { @override String get platform => 'windows'; @override bool get isSupportedOnCurrentPlatform => Platform.isWindows; @override BuildResultResolver get resultResolver => BuildWindowsResultResolver(); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/builders/windows/build_windows_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/commands/flutter.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:recase/recase.dart'; bool currentVersionIsGreaterOrEqual(Version version, String versionString) { return version.compareTo(Version.parse(versionString)) >= 0; } class BuildWindowsResultResolver extends BuildResultResolver { @override BuildResult resolve(BuildConfig config, {Duration? duration}) { return BuildWindowsResult(config)..duration = duration; } } class BuildWindowsResult extends BuildResult { BuildWindowsResult(BuildConfig config) : super(config); FlutterVersion? _flutterVersion; FlutterVersion get flutterVersion { _flutterVersion ??= flutter.version; return _flutterVersion!; } set flutterVersion(FlutterVersion value) { _flutterVersion = value; } String? _arch; String get arch { if (_arch == null) { final processorArchitecture = Platform.environment['PROCESSOR_ARCHITECTURE']; if (processorArchitecture?.toUpperCase() == 'ARM64') { _arch = 'arm64'; } else { _arch = 'x64'; } } return _arch!; } set arch(String value) { _arch = value; } @override Directory get outputDirectory { String buildMode = ReCase(config.mode.name).sentenceCase; String path = 'build/windows/$arch/runner/$buildMode'; if (!flutterVersion.isGreaterOrEqual('3.15.0')) { path = 'build/windows/runner/$buildMode'; } return Directory(path); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/commands/flutter.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:shell_executor/shell_executor.dart'; class FlutterVersion { const FlutterVersion({ this.frameworkVersion, this.channel, this.repositoryUrl, this.frameworkRevision, this.frameworkCommitDate, this.engineRevision, this.dartSdkVersion, this.devToolsVersion, this.flutterVersion, }); factory FlutterVersion.fromJson(Map json) { return FlutterVersion( frameworkVersion: json['frameworkVersion'] as String?, channel: json['channel'] as String?, repositoryUrl: json['repositoryUrl'] as String?, frameworkRevision: json['frameworkRevision'] as String?, frameworkCommitDate: json['frameworkCommitDate'] as String, engineRevision: json['engineRevision'] as String?, dartSdkVersion: json['dartSdkVersion'] as String?, devToolsVersion: json['devToolsVersion'] as String?, flutterVersion: json['flutterVersion'] as String? ?? (json['frameworkVersion'] as String?), ); } final String? frameworkVersion; final String? channel; final String? repositoryUrl; final String? frameworkRevision; final String? frameworkCommitDate; final String? engineRevision; final String? dartSdkVersion; final String? devToolsVersion; final String? flutterVersion; bool isGreaterOrEqual(String versionString) { // just keep the first part of the version string final String currentVersionString = flutterVersion!.split('-').first; final Version currentVersion = Version.parse(currentVersionString); return currentVersion.compareTo(Version.parse(versionString)) >= 0; } } class _Flutter extends Command { @override String get executable { String flutterRoot = environment?['FLUTTER_ROOT'] ?? ''; if (flutterRoot.isNotEmpty) { flutterRoot = pathExpansion(flutterRoot, environment ?? {}); if (!Directory(flutterRoot).existsSync()) { throw CommandError( this, 'FLUTTER_ROOT environment variable is set to a path that does not exist: $flutterRoot', ); } return p.join(flutterRoot, 'bin', 'flutter'); } return 'flutter'; } Map? environment; _Flutter withEnv(Map? environment) { this.environment = environment; return this; } FlutterVersion get version { final result = execSync( ['--version', '--machine'], environment: environment, runInShell: true, ); final String jsonString = '${result.stdout}'; return FlutterVersion.fromJson( Map.from( json.decode(jsonString) as Map, ), ); } Future clean() { return exec( ['clean'], environment: environment, ); } Future build(List arguments) { return exec( ['build', ...arguments], environment: environment, ); } @override Future install() { throw UnimplementedError(); } } final flutter = _Flutter(); ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/lib/src/flutter_app_builder.dart ================================================ import 'package:flutter_app_builder/src/build_result.dart'; import 'package:flutter_app_builder/src/builders/builders.dart'; import 'package:flutter_app_builder/src/commands/flutter.dart'; class FlutterAppBuilder { final List _builders = [ AppBuilderAndroid.aab(), AppBuilderAndroid.apk(), AppBuilderIos(), AppBuilderLinux(), AppBuilderMacOs(), AppBuilderWeb(), AppBuilderWindows(), ]; Future clean({ Map? environment, }) async { await flutter.withEnv(environment).clean(); } Future build( String platform, { String? target, required Map arguments, Map? environment, }) { final builder = _builders.firstWhere((e) => e.match(platform, target)); if (!builder.isSupportedOnCurrentPlatform) { throw UnsupportedError( '${builder.runtimeType} is not supported on the current platform', ); } return builder.build( arguments: arguments, environment: environment, ); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/pubspec.yaml ================================================ name: flutter_app_builder description: Build your Flutter app via Dart. version: 0.4.2 homepage: https://distributor.leanflutter.dev repository: https://github.com/leanflutter/flutter_distributor/tree/main/packages/flutter_app_builder environment: sdk: ">=2.16.0 <4.0.0" dependencies: glob: ^2.1.1 path: ^1.8.1 pub_semver: ^2.1.0 pubspec_parse: ^1.1.0 recase: ^4.1.0 shell_executor: ^0.1.5 dev_dependencies: dependency_validator: ^3.0.0 test: ^1.23.1 ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/build_config_test.dart ================================================ import 'package:flutter_app_builder/src/build_config.dart'; import 'package:test/test.dart'; void main() { group('build config', () { test('mode', () { final profileConfig = BuildConfig( arguments: {'profile': true}, ); expect(profileConfig.mode, BuildMode.profile); final releaseCconfig = BuildConfig(); expect(releaseCconfig.mode, BuildMode.release); }); test('flavor', () { final config = BuildConfig( arguments: {'flavor': 'dev'}, ); expect(config.flavor, 'dev'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/builders/android/build_android_result_test.dart ================================================ import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/builders/android/build_android_result.dart'; import 'package:test/test.dart'; void main() { group('android aab result', () { test('profile mode', () { final r = BuildAndroidResult.aab( BuildConfig( arguments: {'profile': true}, ), ); expect(r.outputDirectory.path, 'build/app/outputs/bundle/profile'); }); test('profile mode + flavor', () { final r = BuildAndroidResult.aab( BuildConfig( arguments: {'profile': true, 'flavor': 'dev'}, ), ); expect(r.outputDirectory.path, 'build/app/outputs/bundle/devProfile'); }); test('release mode', () { final r = BuildAndroidResult.aab( BuildConfig(), ); expect(r.outputDirectory.path, 'build/app/outputs/bundle/release'); }); test('release mode + flavor', () { final r = BuildAndroidResult.aab( BuildConfig( arguments: {'flavor': 'dev'}, ), ); expect(r.outputDirectory.path, 'build/app/outputs/bundle/devRelease'); }); }); group('android apk result', () { test('profile mode', () { final r = BuildAndroidResult.apk( BuildConfig( arguments: {'profile': true}, ), ); expect(r.outputDirectory.path, 'build/app/outputs/flutter-apk'); }); test('profile mode + flavor', () { final r = BuildAndroidResult.apk( BuildConfig( arguments: {'profile': true, 'flavor': 'dev'}, ), ); expect(r.outputDirectory.path, 'build/app/outputs/flutter-apk'); }); test('release mode', () { final r = BuildAndroidResult.apk( BuildConfig(), ); expect(r.outputDirectory.path, 'build/app/outputs/flutter-apk'); }); test('release mode + flavor', () { final r = BuildAndroidResult.apk( BuildConfig( arguments: {'flavor': 'dev'}, ), ); String dirPath = r.outputDirectory.path; expect(dirPath, 'build/app/outputs/flutter-apk'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/builders/ios/build_ios_result_test.dart ================================================ import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/builders/ios/build_ios_result.dart'; import 'package:test/test.dart'; void main() { group('ios result', () { test('profile mode', () { final r = BuildIosResult( BuildConfig( arguments: {'profile': true}, ), ); expect(r.outputDirectory.path, 'build/ios/ipa'); }); test('release mode', () { final r = BuildIosResult( BuildConfig(), ); expect(r.outputDirectory.path, 'build/ios/ipa'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/builders/linux/build_linux_result_test.dart ================================================ import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/builders/linux/build_linux_result.dart'; import 'package:test/test.dart'; void main() { group('linux result', () { test('profile mode', () { final r = BuildLinuxResult( BuildConfig( arguments: {'profile': true}, ), ); r.arch = 'x64'; expect(r.outputDirectory.path, 'build/linux/x64/profile/bundle'); r.arch = 'arm64'; expect(r.outputDirectory.path, 'build/linux/arm64/profile/bundle'); }); test('release mode', () { final r = BuildLinuxResult( BuildConfig(), ); r.arch = 'x64'; expect(r.outputDirectory.path, 'build/linux/x64/release/bundle'); r.arch = 'arm64'; expect(r.outputDirectory.path, 'build/linux/arm64/release/bundle'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/builders/macos/build_ios_result_test.dart ================================================ import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/builders/macos/build_macos_result.dart'; import 'package:test/test.dart'; void main() { group('macos result', () { test('profile mode', () { final r = BuildMacOsResult( BuildConfig( arguments: {'profile': true}, ), ); expect(r.outputDirectory.path, 'build/macos/Build/Products/Profile'); }); test('release mode', () { final r = BuildMacOsResult( BuildConfig(), ); expect(r.outputDirectory.path, 'build/macos/Build/Products/Release'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/builders/web/build_web_result_test.dart ================================================ import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/builders/web/build_web_result.dart'; import 'package:test/test.dart'; void main() { group('web result', () { test('profile mode', () { final r = BuildWebResult( BuildConfig( arguments: {'profile': true}, ), ); expect(r.outputDirectory.path, 'build/web'); }); test('release mode', () { final r = BuildWebResult( BuildConfig(), ); expect(r.outputDirectory.path, 'build/web'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/builders/windows/build_windows_result_test.dart ================================================ import 'package:flutter_app_builder/src/build_config.dart'; import 'package:flutter_app_builder/src/builders/windows/build_windows_result.dart'; import 'package:flutter_app_builder/src/commands/flutter.dart'; import 'package:test/test.dart'; void main() { group('windows result', () { test('profile mode', () { final r = BuildWindowsResult( BuildConfig( arguments: {'profile': true}, ), ); r.flutterVersion = const FlutterVersion(flutterVersion: '3.16.0'); expect(r.outputDirectory.path, 'build/windows/x64/runner/Profile'); }); test('profile mode (less 3.15.0)', () { final r = BuildWindowsResult( BuildConfig( arguments: {'profile': true}, ), ); r.flutterVersion = const FlutterVersion(flutterVersion: '3.10.0'); expect(r.outputDirectory.path, 'build/windows/runner/Profile'); }); test('release mode', () { final r = BuildWindowsResult( BuildConfig(), ); r.flutterVersion = const FlutterVersion(flutterVersion: '3.16.0'); expect(r.outputDirectory.path, 'build/windows/x64/runner/Release'); }); test('release mode (less 3.15.0)', () { final r = BuildWindowsResult( BuildConfig(), ); r.flutterVersion = const FlutterVersion(flutterVersion: '3.10.0'); expect(r.outputDirectory.path, 'build/windows/runner/Release'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_builder/test/src/commands/flutter_test.dart ================================================ import 'package:flutter_app_builder/src/commands/flutter.dart'; import 'package:test/test.dart'; void main() { group('FlutterVersion', () { test('isGreaterOrEqual#1', () { const v3100 = FlutterVersion( flutterVersion: '3.10.0', ); expect(v3100.isGreaterOrEqual('3.3.10'), true); expect(v3100.isGreaterOrEqual('3.10.0'), true); expect(v3100.isGreaterOrEqual('3.10.1'), false); }); test('isGreaterOrEqual#2', () { const v3150 = FlutterVersion( flutterVersion: '3.15.0-15.2.pre', ); expect(v3150.isGreaterOrEqual('3.3.10'), true); expect(v3150.isGreaterOrEqual('3.15.0'), true); expect(v3150.isGreaterOrEqual('3.16.1'), false); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages .flutter-plugins .flutter-plugins-dependencies ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/analysis_options.yaml ================================================ include: package:mostly_reasonable_lints/analysis_options.yaml linter: rules: avoid_print: false ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/flutter_app_packager.dart ================================================ library flutter_app_packager; export 'src/api/app_package_maker.dart'; export 'src/api/make_config.dart'; export 'src/api/make_error.dart'; export 'src/api/make_result.dart'; export 'src/flutter_app_packager.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/api/app_package_maker.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_packager/src/api/make_config.dart'; import 'package:flutter_app_packager/src/api/make_result.dart'; import 'package:shell_executor/shell_executor.dart'; import 'package:yaml/yaml.dart'; export 'make_config.dart'; export 'make_error.dart'; export 'make_result.dart'; Map loadMakeConfigYaml(String path) { final yamlDoc = loadYaml(File(path).readAsStringSync()); return json.decode(json.encode(yamlDoc)); } abstract class AppPackageMaker { List get requirements => []; String get name => throw UnimplementedError(); String get platform => throw UnimplementedError(); bool get isSupportedOnCurrentPlatform => true; String get packageFormat => throw UnimplementedError(); MakeConfigLoader get configLoader { return DefaultMakeConfigLoader() ..platform = platform ..packageFormat = packageFormat; } MakeResultResolver get resultResolver => DefaultMakeResultResolver(); bool match(String platform, [String? target]) { return this.platform == platform && name == target; } Future make(MakeConfig config) { throw UnimplementedError(); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/api/distribute_options_base.dart ================================================ class DistributeOptionsBase { DistributeOptionsBase({ this.appName, }); factory DistributeOptionsBase.fromJson(Map json) { return DistributeOptionsBase( appName: json['app_name'] ); } final String? appName; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/api/make_config.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_packager/src/api/make_error.dart'; import 'package:flutter_app_packager/src/api/distribute_options_base.dart'; import 'package:mustache_template/mustache.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; const _kArtifactName = '{{name}}-{{build_name}}-{{platform}}{{#description}}-{{description}}{{/description}}{{#is_installer}}-setup{{/is_installer}}{{#ext}}.{{ext}}{{/ext}}'; class MakeConfig { late bool isInstaller = false; late String buildMode; late Directory buildOutputDirectory; late List buildOutputFiles; late String platform; String? flavor; String? arch; String? channel; String? description; /// https://mustache.github.io/mustache.5.html String? artifactName; late String packageFormat; late Directory outputDirectory; String get appName => distributeOptionsBase.appName ?? pubspec.name; String get appBinaryName => distributeOptionsBase.appName ?? pubspec.name; Version get appVersion => pubspec.version!; String get appBuildName => appVersion.toString().split('+').first; String get appBuildNumber => appVersion.toString().split('+').last; Pubspec? _pubspec; DistributeOptionsBase? _distributeOptionsBase; Directory? _packagingDirectory; MakeConfig copyWith(MakeConfig makeConfig) { buildMode = makeConfig.buildMode; buildOutputDirectory = makeConfig.buildOutputDirectory; buildOutputFiles = makeConfig.buildOutputFiles; platform = makeConfig.platform; description = makeConfig.description; arch = makeConfig.arch; flavor = makeConfig.flavor; channel = makeConfig.channel; artifactName = makeConfig.artifactName; packageFormat = makeConfig.packageFormat; outputDirectory = makeConfig.outputDirectory; return this; } File get outputFile { if (packageFormat.isEmpty) { throw MakeError('Direct output is not a file'); } return File(outputArtifactPath); } String get outputArtifactPath { String useArtifactName = _kArtifactName; if (artifactName != null) useArtifactName = artifactName!; Map variables = { 'is_installer': isInstaller, 'is_profile': buildMode == 'profile', 'name': appName, 'version': appVersion.toString(), 'build_name': appBuildName, 'build_number': appBuildNumber, 'build_mode': buildMode, 'platform': platform, 'flavor': flavor, 'channel': channel, 'description': description, 'ext': packageFormat.isEmpty ? null : packageFormat, }; String filename = Template(useArtifactName).renderString(variables); Directory versionOutputDirectory = Directory(outputDirectory.path); if (!versionOutputDirectory.existsSync()) { versionOutputDirectory.createSync(recursive: true); } return '${versionOutputDirectory.path}/$filename'; } List get outputArtifacts { List artifacts = []; if (packageFormat.isEmpty) { artifacts.add(Directory(outputArtifactPath)); } else { artifacts.add(File(outputArtifactPath)); } return artifacts; } Directory get packagingDirectory { if (_packagingDirectory == null) { _packagingDirectory = Directory( outputArtifactPath.replaceAll('.$packageFormat', '_$packageFormat'), ); if (_packagingDirectory!.existsSync()) { _packagingDirectory!.deleteSync(recursive: true); } _packagingDirectory!.createSync(recursive: true); } return _packagingDirectory!; } Pubspec get pubspec { if (_pubspec == null) { final yamlString = File('pubspec.yaml').readAsStringSync(); _pubspec = Pubspec.parse(yamlString); } return _pubspec!; } DistributeOptionsBase get distributeOptionsBase { if (_distributeOptionsBase == null) { File file = File('distribute_options.yaml'); if (file.existsSync()) { final yamlString = File('distribute_options.yaml').readAsStringSync(); final yamlDoc = loadYaml(yamlString); _distributeOptionsBase = DistributeOptionsBase.fromJson( json.decode(json.encode(yamlDoc)), ); } else { _distributeOptionsBase = DistributeOptionsBase(); } } return _distributeOptionsBase!; } Map toJson() { return { 'isInstaller': isInstaller, 'buildMode': buildMode, 'buildOutputDirectory': buildOutputDirectory.path, 'buildOutputFiles': buildOutputFiles.map((e) => e.path).toList(), 'platform': platform, 'arch': arch, 'description': description, 'flavor': flavor, 'channel': channel, 'artifactName': artifactName, 'packageFormat': packageFormat, 'outputDirectory': outputDirectory.path, 'appName': appName, 'appVersion': appVersion.toString(), 'appBuildName': appBuildName, 'appBuildNumber': appBuildNumber, }..removeWhere((key, value) => value == null); } } abstract class MakeConfigLoader { late String platform; late String packageFormat; MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }); } class DefaultMakeConfigLoader extends MakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { return MakeConfig() ..platform = platform ..arch = arguments?['arch'] ..buildMode = arguments?['build_mode'] ..buildOutputDirectory = buildOutputDirectory ..buildOutputFiles = buildOutputFiles ..flavor = arguments?['flavor'] ..description = arguments?['description'] ..channel = arguments?['channel'] ..artifactName = arguments?['artifact_name'] ..packageFormat = packageFormat ..outputDirectory = outputDirectory; } } class MakeLinuxPackageConfig extends MakeConfig { String? _appBinaryName; @override String get appBinaryName { if (_appBinaryName == null) { final cMakeListsFile = File('linux/CMakeLists.txt'); final RegExp regex = RegExp(r'(?<=set\(BINARY_NAME\s")[^"]+(?="\))'); final Match? match = regex.firstMatch(cMakeListsFile.readAsStringSync()); if (match != null) { final String? binaryName = match.group(0); _appBinaryName = binaryName; } else { _appBinaryName = appName; } } return _appBinaryName!; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/api/make_error.dart ================================================ class MakeError extends Error { MakeError([this.message]); final String? message; @override String toString() { var message = this.message; return (message != null) ? 'MakeError: $message' : 'MakeError'; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/api/make_result.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/make_config.dart'; import 'package:flutter_app_packager/src/api/make_error.dart'; class MakeResult { MakeResult( this.config, { this.duration, }) : artifacts = config.outputArtifacts; final MakeConfig config; final List artifacts; final Duration? duration; Map toJson() { return { 'config': config.toJson(), 'artifacts': artifacts .map( (e) => { 'type': e is File ? 'file' : 'directory', 'path': e.path, }, ) .toList(), 'duration': duration, }..removeWhere((key, value) => value == null); } } abstract class MakeResultResolver { MakeResult resolve(MakeConfig config); } class DefaultMakeResultResolver extends MakeResultResolver { @override MakeResult resolve(MakeConfig config) { MakeResult makeResult = MakeResult(config); return makeResult; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/flutter_app_packager.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/makers.dart'; class FlutterAppPackager { final List _makers = [ AppPackageMakerAab(), AppPackageMakerApk(), AppPackageMakerAppImage(), AppPackageMakerDeb(), AppPackageMakerDirect('linux'), AppPackageMakerDirect('windows'), AppPackageMakerDirect('web'), AppPackageMakerDmg(), AppPackageMakerExe(), AppPackageMakerIpa(), AppPackageMakerMsix(), AppPackageMakerPkg(), AppPackageMakerRPM(), AppPackageMakerZip('linux'), AppPackageMakerZip('macos'), AppPackageMakerZip('windows'), AppPackageMakerZip('web'), ]; Future package( String platform, String target, Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final maker = _makers.firstWhere((e) => e.match(platform, target)); final config = maker.configLoader.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); return maker.make(config); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/aab/app_package_maker_aab.dart ================================================ import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class AppPackageMakerAab extends AppPackageMaker { @override String get name => 'aab'; @override String get platform => 'android'; @override String get packageFormat => 'aab'; @override Future make(MakeConfig config) { config.buildOutputFiles.first.copySync(config.outputFile.path); return Future.value(resultResolver.resolve(config)); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/apk/app_package_maker_apk.dart ================================================ import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class AppPackageMakerApk extends AppPackageMaker { @override String get name => 'apk'; @override String get platform => 'android'; @override String get packageFormat => 'apk'; @override Future make(MakeConfig config) { for (final file in config.buildOutputFiles) { final splits = file.uri.pathSegments.last.split('-'); final outputPath = config.outputFile.path; if (splits.length > 2) { final sublist = splits.sublist(1, splits.length - 1); final lastDotIndex = outputPath.lastIndexOf('.'); final firstPart = outputPath.substring(0, lastDotIndex); final lastPart = outputPath.substring(lastDotIndex + 1); final output = '$firstPart-${sublist.join('-')}.${lastPart}'; file.copySync(output); } else { file.copySync(outputPath); } } return Future.value(resultResolver.resolve(config)); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/app/app_package_maker_app.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class AppPackageMakerApp extends AppPackageMaker { @override String get name => 'app'; @override String get platform => 'ohos'; @override String get packageFormat => 'app'; @override Future make(MakeConfig config) { File pkgFile = config.buildOutputFiles.first; pkgFile.copySync(config.outputFile.path); return Future.value(resultResolver.resolve(config)); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/appimage/app_package_maker_appimage.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/appimage/make_appimage_config.dart'; import 'package:path/path.dart' as path; import 'package:shell_executor/shell_executor.dart'; class AppPackageMakerAppImage extends AppPackageMaker { @override String get name => 'appimage'; @override String get platform => 'linux'; @override bool get isSupportedOnCurrentPlatform => Platform.isLinux; @override String get packageFormat => 'appimage'; @override MakeConfigLoader get configLoader { return MakeAppImageConfigLoader() ..platform = platform ..packageFormat = packageFormat; } Future> _getSharedDependencies(String so) { return $('ldd', ['-d', so]) .then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } return value.stdout as String; }) .then((lines) { final soDeps = lines .split('\n') .where( (line) => line.contains('=>') && line.trim().startsWith('lib'), ) /// converts this: /// libkeybinder-3.0.so.0 => /lib64/libkeybinder-3.0.so.0 (0x00007f6513811000) /// to this: /// /lib64/libkeybinder-3.0.so.0 .map( (line) => line.split(' => ')[1].trim().split(' ').first.trim(), ) .toList() ..sort(); return soDeps.toSet(); }); } @override Future make(MakeConfig config) { return _make( config.buildOutputDirectory, outputDirectory: config.outputDirectory, makeConfig: config as MakeAppImageConfig, ); } Future _make( Directory appDirectory, { required Directory outputDirectory, required MakeAppImageConfig makeConfig, }) async { try { await $('cp', [ '-r', appDirectory.path, path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir', ), ]).then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } }); final desktopFile = File( path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir', '${makeConfig.appName}.desktop', ), )..createSync(recursive: true); await desktopFile.writeAsString(makeConfig.desktopFileContent); final appRunFile = File( path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir', 'AppRun', ), )..createSync(recursive: true); await appRunFile.writeAsString(makeConfig.appRunContent); await $('chmod', ['+x', appRunFile.path]).then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } }); final iconFile = File(makeConfig.icon); if (!iconFile.existsSync()) { throw MakeError("icon ${makeConfig.icon} path doesn't exist"); } await iconFile.copy( path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir', '${makeConfig.appName}${path.extension(makeConfig.icon)}', ), ); final icon256x256 = path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir/usr/share/icons/hicolor/256x256/apps', ); final icon128x128 = path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir/usr/share/icons/hicolor/128x128/apps', ); await $('mkdir', ['-p', icon128x128, icon256x256]).then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } }); await iconFile.copy( path.join( icon128x128, '${makeConfig.appName}${path.extension(makeConfig.icon)}', ), ); await iconFile.copy( path.join( icon256x256, '${makeConfig.appName}${path.extension(makeConfig.icon)}', ), ); final defaultSharedObjects = [ 'libapp.so', 'libflutter_linux_gtk.so', 'libgtk-3.so.0', ]; final appSOLibs = Directory( path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir/lib', ), ).listSync().where( (e) => !defaultSharedObjects.contains(path.basename(e.path)), ); await $('mkdir', [ '-p', path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir/usr/lib', ), ]).then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } }); final libFlutterGtkDeps = await _getSharedDependencies( path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir/lib/libflutter_linux_gtk.so', ), ); final allReferencedSharedLibs = {}; for (final so in appSOLibs) { final referencedSharedLibs = await _getSharedDependencies(so.path).then( (d) => d.difference(libFlutterGtkDeps) ..removeWhere((lib) => lib.contains('libflutter_linux_gtk.so')), ); allReferencedSharedLibs.addAll(referencedSharedLibs); } if (allReferencedSharedLibs.isNotEmpty) { await $('cp', [ ...allReferencedSharedLibs, path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir/usr/lib', ), ]).then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } }); } await Future.wait( makeConfig.include.map((so) async { final file = await $('locate', [so]) .then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } return value.stdout as String; }) .then((out) { final paths = out .split('\n') .where((p) => p.isNotEmpty && !p.contains('/Trash')) .toList(); if (paths.isEmpty) { throw MakeError("Can't find specified shared object $so"); } return File(paths.first.trim()); }); await file.copy( path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir/usr/lib/', path.basename(file.path), ), ); }), ); await $( 'appimagetool', [ path.join( makeConfig.packagingDirectory.path, '${makeConfig.appName}.AppDir', ), makeConfig.outputFile.path.replaceAll('.appimage', '.AppImage'), ], environment: {'ARCH': 'x86_64'}, ).then((value) { if (value.exitCode != 0) { throw MakeError(value.stderr as String); } }); makeConfig.packagingDirectory.deleteSync(recursive: true); return MakeResult(makeConfig); } catch (e) { if (e is MakeError) rethrow; throw MakeError(e.toString()); } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/appimage/make_appimage_config.dart ================================================ // ignore_for_file: flutter_style_todo,todo import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class AppImageAction { AppImageAction({ required this.label, required this.name, required this.arguments, }); factory AppImageAction.fromJson(Map map) { return AppImageAction( label: map['label'] as String, name: map['name'] as String, arguments: (map['arguments'] as List).cast(), ); } String label; String name; List arguments; Map toJson() { return { 'label': label, 'name': name, 'arguments': arguments, }; } } class MakeAppImageConfig extends MakeConfig { MakeAppImageConfig({ required this.displayName, required this.icon, this.keywords = const [], this.categories = const [], this.actions = const [], this.include = const [], this.startupNotify = true, this.genericName = 'A Flutter Application', }); factory MakeAppImageConfig.fromJson(Map map) { return MakeAppImageConfig( displayName: map['display_name'] as String, icon: map['icon'] as String, include: (map['include'] as List? ?? []).cast(), keywords: (map['keywords'] as List? ?? []).cast(), categories: (map['categories'] as List? ?? []).cast(), startupNotify: map['startup_notify'] as bool? ?? false, genericName: map['generic_name'] as String? ?? 'A Flutter Application', actions: (map['actions'] as List? ?? []) .map( (e) => AppImageAction.fromJson( (Map.castFrom(e)), ), ) .toList(), ); } final String icon; final List keywords; final List categories; final List actions; final bool startupNotify; final String genericName; final String displayName; final List include; String get desktopFileContent { final fields = { 'Name': displayName, 'GenericName': genericName, 'Exec': 'LD_LIBRARY_PATH=usr/lib $appName %u', 'Icon': appName, 'Type': 'Application', 'StartupNotify': startupNotify ? 'true' : 'false', if (categories.isNotEmpty) 'Categories': categories.join(';'), if (keywords.isNotEmpty) 'Keywords': keywords.join(';'), if (this.actions.isNotEmpty) 'Actions': this.actions.map((e) => e.label).join(';'), }.entries.map((e) => '${e.key}=${e.value}').join('\n'); final actions = this.actions.map((action) { final fields = { 'Name': action.name, 'Exec': 'LD_LIBRARY_PATH=usr/lib $appName ${action.arguments.join(' ')} %u', }; return '[Desktop Action ${action.label}]\n${fields.entries.map((e) => '${e.key}=${e.value}').join('\n')}'; }).join('\n\n'); return '[Desktop Entry]\n$fields\n\n$actions'; } String get appRunContent { return ''' #!/bin/bash cd "\$(dirname "\$0")" export LD_LIBRARY_PATH=usr/lib exec ./$appName '''; } } class MakeAppImageConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakeAppImageConfig.fromJson(map).copyWith(baseMakeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/deb/app_package_maker_deb.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/deb/make_deb_config.dart'; import 'package:path/path.dart' as path; import 'package:shell_executor/shell_executor.dart'; class AppPackageMakerDeb extends AppPackageMaker { @override String get name => 'deb'; @override String get platform => 'linux'; @override bool get isSupportedOnCurrentPlatform => Platform.isLinux; @override String get packageFormat => 'deb'; @override MakeConfigLoader get configLoader { return MakeDebConfigLoader() ..platform = platform ..packageFormat = packageFormat; } @override Future make(MakeConfig config) { return _make( config.buildOutputDirectory, outputDirectory: config.outputDirectory, makeConfig: config as MakeDebConfig, ); } Future _make( Directory appDirectory, { required Directory outputDirectory, required MakeDebConfig makeConfig, }) async { final files = makeConfig.toFilesString(); Directory packagingDirectory = makeConfig.packagingDirectory; /// Need to create following directories /// /DEBIAN /// /usr/share/$appBinaryName /// /usr/share/applications /// /usr/share/icons/hicolor/128x128/apps /// /usr/share/icons/hicolor/256x256/apps final debianDir = path.join(packagingDirectory.path, 'DEBIAN'); final applicationsDir = path.join(packagingDirectory.path, 'usr/share/applications'); final icon128Dir = path.join( packagingDirectory.path, 'usr/share/icons/hicolor/128x128/apps', ); final icon256Dir = path.join( packagingDirectory.path, 'usr/share/icons/hicolor/256x256/apps', ); final mkdirProcessResult = await $('mkdir', [ '-p', debianDir, path.join(packagingDirectory.path, 'usr/share', makeConfig.appBinaryName), applicationsDir, if (makeConfig.icon != null) ...[icon128Dir, icon256Dir], ]); if (mkdirProcessResult.exitCode != 0) throw MakeError(); if (makeConfig.icon != null) { final iconFile = File(makeConfig.icon!); if (!iconFile.existsSync()) { throw MakeError("provided icon ${makeConfig.icon} path wasn't found"); } await iconFile.copy( path.join( icon128Dir, makeConfig.appBinaryName + path.extension(makeConfig.icon!), ), ); await iconFile.copy( path.join( icon256Dir, makeConfig.appBinaryName + path.extension(makeConfig.icon!), ), ); } // create & write the files got from makeConfig final controlFile = File(path.join(debianDir, 'control')); final postinstFile = File(path.join(debianDir, 'postinst')); final postrmFile = File(path.join(debianDir, 'postrm')); final desktopEntryFile = File(path.join(applicationsDir, '${makeConfig.appBinaryName}.desktop')); if (!controlFile.existsSync()) controlFile.createSync(); if (!postinstFile.existsSync()) postinstFile.createSync(); if (!postrmFile.existsSync()) postrmFile.createSync(); if (!desktopEntryFile.existsSync()) desktopEntryFile.createSync(); await controlFile.writeAsString(files['CONTROL']!); await desktopEntryFile.writeAsString(files['DESKTOP']!); await postinstFile.writeAsString(files['postinst']!); await postrmFile.writeAsString(files['postrm']!); // give execution permission to shell scripts await $('chmod', ['+x', postinstFile.path, postrmFile.path]); // copy the application binary to /usr/share/$appBinaryName await $('cp', [ '-fr', '${appDirectory.path}/.', '${packagingDirectory.path}/usr/share/${makeConfig.appBinaryName}/', ]); ProcessResult processResult = await $('dpkg-deb', [ '--build', '--root-owner-group', packagingDirectory.path, makeConfig.outputFile.path, ]); if (processResult.exitCode != 0) { throw MakeError(); } packagingDirectory.deleteSync(recursive: true); return MakeResult(makeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/deb/make_deb_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; // format of make_config for deb /* # the name used to display in the OS. Specifically desktop # entry name display_name: Hola Amigos # package name for debian/apt repository # the name should be all lowercase with -+. package_name: hola-amigos maintainer: name: Gamer Boy 69 email: rickastley@gmail.lol co_authors: - name: Mir Jafar email: contributor@gmail.com # enum options -> required, important, standard, optional, extra # refer: https://www.debian.org/doc/debian-policy/ch-archive.html#s-priorities priority: optional # enum options: admin, cli-mono, comm, database, debug, devel, doc, editors, education, electronics, embedded, fonts, games, gnome, gnu-r, gnustep, graphics, hamradio, haskell, httpd, interpreters, introspection, java, javascript, kde, kernel, libdevel, libs, lisp, localization, mail, math, metapackages, misc, net, news, ocaml, oldlibs, otherosfs, perl, php, python, ruby, rust, science, shells, sound, tasks, tex, text, utils, vcs, video, web, x11, xfce, zope # refer: https://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections section: x11 # the size of binary in kilobyte installed_size: 24400 # direct dependencies required by the application # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html dependencies: - libkeybinder-3.0-0 (>= 0.3.2) # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html build_dependencies_indep: - texinfo # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html build_dependencies: - kernel-headers-2.2.10 [!hurd-i386] - gnumach-dev [hurd-i386] - libluajit5.1-dev [i386 amd64 kfreebsd-i386 armel armhf powerpc mips] # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html recommended_dependencies: - neofetch # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html suggested_dependencies: - libkeybinder-3.0-0 (>= 0.3.2) # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html enhances: - spotube # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html pre_dependencies: - libc6 # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#packages-which-break-other-packages-breaks breaks: - libspotify (<< 3.0.0) # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#conflicting-binary-packages-conflicts conflicts: - spotify # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#virtual-packages-provides provides: - libx11 # refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#overwriting-files-and-replacing-packages-replaces replaces: - spotify essential: false postinstall_scripts: - echo `Installed my awesome app` postuninstall_scripts: - echo `Surprised Pickachu face` # application icon path relative to project url icon: assets/logo.png keywords: - Hello - World - Test - Application # a name to categorize the app into a section of application generic_name: Hobby Application # supported mime types that can be opened using this application supported_mime_type: - audio/mpeg # shown when right clicked the desktop entry icons actions: - Gallery - Create # the categories the application belong to # refer: https://specifications.freedesktop.org/menu-spec/latest/ categories: - Music - Media # let OS know if the application can be run on start_up. If it's false # the application will deny to the OS if it was added as a start_up # application startup_notify: true */ class MakeDebConfig extends MakeLinuxPackageConfig { MakeDebConfig({ required this.displayName, required this.packageName, required this.installedSize, required this.maintainer, this.startupNotify = true, this.essential = false, List? postinstallScripts, List? postuninstallScripts, this.priority = 'optional', this.section = 'x11', this.actions, this.breaks, this.buildDependencies, this.buildDependenciesIndep, this.suggestedDependencies, this.categories, this.coAuthors, this.dependencies, this.enhances, this.genericName, this.icon, this.keywords, this.preDependencies, this.provides, this.recommendedDependencies, this.replaces, this.conflicts, this.supportedMimeType, }) : _postinstallScripts = postinstallScripts ?? [], _postuninstallScripts = postuninstallScripts ?? []; factory MakeDebConfig.fromJson(Map map) { return MakeDebConfig( displayName: map['display_name'], packageName: map['package_name'], maintainer: "${map['maintainer']['name']} <${map['maintainer']['email']}>", coAuthors: (map['co_authors'] as List?) ?.map((e) => "${e['name']} <${e['email']}>") .toList(), priority: map['priority'], section: map['section'], dependencies: map['dependencies'] != null ? List.castFrom(map['dependencies']) : null, buildDependenciesIndep: map['build_dependencies_indep'] != null ? List.castFrom(map['build_dependencies_indep']) : null, buildDependencies: map['build_dependencies'] != null ? List.castFrom(map['build_dependencies']) : null, recommendedDependencies: map['recommended_dependencies'] != null ? List.castFrom(map['recommended_dependencies']) : null, suggestedDependencies: map['suggested_dependencies'] != null ? List.castFrom(map['suggested_dependencies']) : null, enhances: map['enhances'] != null ? List.castFrom(map['enhances']) : null, preDependencies: map['pre_dependencies'] != null ? List.castFrom(map['pre_dependencies']) : null, breaks: map['breaks'] != null ? List.castFrom(map['breaks']) : null, conflicts: map['conflicts'] != null ? List.castFrom(map['conflicts']) : null, provides: map['provides'] != null ? List.castFrom(map['provides']) : null, replaces: map['replaces'] != null ? List.castFrom(map['replaces']) : null, postinstallScripts: map['postinstall_scripts'] != null ? List.castFrom(map['postinstall_scripts']) : null, postuninstallScripts: map['postuninstall_scripts'] != null ? List.castFrom(map['postuninstall_scripts']) : null, keywords: map['keywords'] != null ? List.castFrom(map['keywords']) : null, supportedMimeType: map['supported_mime_type'] != null ? List.castFrom(map['supported_mime_type']) : null, actions: map['actions'] != null ? List.castFrom(map['actions']) : null, categories: map['categories'] != null ? List.castFrom(map['categories']) : null, essential: map['essential'], genericName: map['generic_name'], startupNotify: map['startup_notify'], installedSize: map['installed_size'], icon: map['icon'], ); } String displayName; String packageName; String maintainer; String priority; String section; int installedSize; bool? essential; String? icon; String? genericName; bool? startupNotify; List? coAuthors; List? dependencies; List? buildDependenciesIndep; List? buildDependencies; List? recommendedDependencies; List? suggestedDependencies; List? enhances; List? preDependencies; List? breaks; List? conflicts; List? provides; List? replaces; List _postinstallScripts; List _postuninstallScripts; List? keywords; List? supportedMimeType; List? actions; List? categories; List get postinstallScripts => [ 'ln -s /usr/share/$appBinaryName/$appBinaryName /usr/bin/$appBinaryName', 'chmod +x /usr/bin/$appBinaryName', ..._postinstallScripts, ]; List get postuninstallScripts => [ 'rm /usr/bin/$appBinaryName', ..._postuninstallScripts, ]; @override Map toJson() { return { 'CONTROL': { 'Maintainer': maintainer, 'Package': packageName, 'Version': appVersion.toString(), 'Section': section, 'Priority': priority, 'Architecture': _getArchitecture(), 'Essential': essential != null ? (essential == true ? 'yes' : 'no') : null, 'Installed-Size': installedSize, 'Description': pubspec.description, 'Homepage': pubspec.homepage, 'Depends': dependencies?.join(', '), 'Build-Depends-Indep': buildDependenciesIndep?.join(', '), 'Build-Depends': buildDependencies?.join(', '), 'Pre-Depends': preDependencies?.join(', '), 'Recommends': recommendedDependencies?.join(', '), 'Suggests': suggestedDependencies?.join(', '), 'Enhances': enhances?.join(', '), 'Breaks': breaks?.join(', '), 'Conflicts': conflicts?.join(', '), 'Provides': provides?.join(', '), 'Replaces': replaces?.join(', '), 'Uploaders': coAuthors?.join(', '), }..removeWhere((key, value) => value == null), 'DESKTOP': { 'Type': 'Application', 'Version': appVersion.toString(), 'Name': displayName, 'GenericName': genericName, 'Icon': appBinaryName, 'Exec': '$appBinaryName %U', 'Actions': actions != null && actions!.isNotEmpty ? '${actions!.join(';')};' : null, 'MimeType': supportedMimeType != null && supportedMimeType!.isNotEmpty ? '${supportedMimeType!.join(';')};' : null, 'Categories': categories != null && categories!.isNotEmpty ? '${categories!.join(';')};' : null, 'Keywords': keywords != null && keywords!.isNotEmpty ? '${keywords!.join(';')};' : null, 'StartupNotify': startupNotify, }..removeWhere((key, value) => value == null), }; } Map toFilesString() { final json = toJson(); final controlFile = '${(json['CONTROL'] as Map).entries.map( (e) => '${e.key}: ${e.value}', ).join('\n')}\n'; final desktopFile = [ '[Desktop Entry]', ...(json['DESKTOP'] as Map).entries.map( (e) => '${e.key}=${e.value}', ), ].join('\n'); final map = { 'CONTROL': controlFile, 'DESKTOP': desktopFile, 'postinst': postinstallScripts.isNotEmpty ? [ '#!/usr/bin/env sh', ...postinstallScripts, 'exit 0', ].join('\n') : null, 'postrm': postuninstallScripts.isNotEmpty ? [ '#!/usr/bin/env sh', ...postuninstallScripts, 'exit 0', ].join('\n') : null, }..removeWhere((key, value) => value == null); return Map.castFrom(map); } } class MakeDebConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakeDebConfig.fromJson(map).copyWith(baseMakeConfig); } } String _getArchitecture() { final result = Process.runSync('uname', ['-m']); if ('${result.stdout}'.trim() == 'aarch64') { return 'arm64'; } else { return 'amd64'; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/direct/app_package_maker_direct.dart ================================================ import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:io/io.dart'; class AppPackageMakerDirect extends AppPackageMaker { AppPackageMakerDirect(String platform) { _platform = platform; } late String _platform; @override String get name => 'direct'; @override String get platform => _platform; @override String get packageFormat => ''; @override Future make(MakeConfig config) { copyPathSync(config.buildOutputDirectory.path, config.outputArtifactPath); return Future.value(resultResolver.resolve(config)); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/dmg/app_package_maker_dmg.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/dmg/commands/appdmg.dart'; import 'package:flutter_app_packager/src/makers/dmg/make_dmg_config.dart'; import 'package:shell_executor/shell_executor.dart'; class AppPackageMakerDmg extends AppPackageMaker { @override List get requirements => [appdmg]; @override String get name => 'dmg'; @override String get platform => 'macos'; @override bool get isSupportedOnCurrentPlatform => Platform.isMacOS; @override String get packageFormat => 'dmg'; @override MakeConfigLoader get configLoader { return MakeDmgConfigLoader() ..platform = platform ..packageFormat = packageFormat; } @override Future make(MakeConfig config) async { Directory packagingDirectory = config.packagingDirectory; File appFile = config.buildOutputDirectory .listSync() .where((e) => e.path.endsWith('.app')) .map((e) => File(e.path)) .first; try { await $('cp', ['-RH', appFile.path, packagingDirectory.path]); await $('cp', ['-RH', 'macos/packaging/dmg/.', packagingDirectory.path]); File makeDmgConfigJsonFile = File( '${packagingDirectory.path}/make_config.json', ); makeDmgConfigJsonFile.writeAsStringSync(json.encode(config.toJson())); final file = File(config.outputFile.path); if (file.existsSync()) { file.deleteSync(); } ProcessResult processResult = await appdmg.exec([ makeDmgConfigJsonFile.path, config.outputFile.path, ]); if (processResult.exitCode != 0) { throw MakeError(); } } catch (error) { rethrow; } finally { packagingDirectory.deleteSync(recursive: true); } return resultResolver.resolve(config); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/dmg/commands/appdmg.dart ================================================ import 'package:shell_executor/shell_executor.dart'; class _AppDmg extends Command { @override String get executable => 'appdmg'; @override Future install() async { await $('pnpm', 'install -g appdmg'.split(' ')); } } final appdmg = _AppDmg(); ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/dmg/make_dmg_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class DmgWindowPosition { DmgWindowPosition({ required this.x, required this.y, }); factory DmgWindowPosition.fromJson(Map json) { return DmgWindowPosition( x: json['x'], y: json['y'], ); } final num x; final num y; Map toJson() { return { 'x': x, 'y': y, }; } } class DmgWindowSize { DmgWindowSize({ required this.width, required this.height, }); factory DmgWindowSize.fromJson(Map json) { return DmgWindowSize( width: json['width'], height: json['height'], ); } final num width; final num height; Map toJson() { return { 'width': width, 'height': height, }; } } class DmgWindow { DmgWindow({ this.position, this.size, }); factory DmgWindow.fromJson(Map json) { return DmgWindow( position: json['position'] != null ? DmgWindowPosition.fromJson(json['position']) : null, size: json['size'] != null ? DmgWindowSize.fromJson(json['size']) : null, ); } final DmgWindowPosition? position; final DmgWindowSize? size; Map toJson() { return { 'position': position?.toJson(), 'size': size?.toJson(), }..removeWhere((key, value) => value == null); } } class DmgCodeSign { DmgCodeSign({ required this.signingIdentity, this.identifier, }); factory DmgCodeSign.fromJson(Map json) { return DmgCodeSign( signingIdentity: json['signing-identity'], identifier: json['identifier'], ); } final String signingIdentity; final String? identifier; Map toJson() { return { 'signing-identity': signingIdentity, 'identifier': identifier, }..removeWhere((key, value) => value == null); } } class DmgContent { DmgContent({ required this.x, required this.y, required this.type, required this.path, this.name, }); factory DmgContent.fromJson(Map json) { return DmgContent( x: json['x'], y: json['y'], type: json['type'], path: json['path'], name: json['name'], ); } final num x; final num y; final String type; final String path; final String? name; Map toJson() { return { 'x': x, 'y': y, 'type': type, 'path': path, 'name': name, }..removeWhere((key, value) => value == null); } } class MakeDmgConfig extends MakeConfig { MakeDmgConfig({ required this.title, this.icon, this.background, this.backgroundColor, this.iconSize, this.format, required this.contents, this.codeSign, this.window, }); factory MakeDmgConfig.fromJson(Map json) { List contents = (json['contents'] as List) .map((item) => DmgContent.fromJson(item)) .toList(); return MakeDmgConfig( title: json['title'], icon: json['icon'], background: json['background'], backgroundColor: json['background-color'], iconSize: json['icon-size'], format: json['format'], contents: contents, codeSign: json['code-sign'] != null ? DmgCodeSign.fromJson(json['code-sign']) : null, window: json['window'] != null ? DmgWindow.fromJson(json['window']) : null, ); } final String title; final String? icon; final String? background; final String? backgroundColor; final int? iconSize; final String? format; final List contents; final DmgCodeSign? codeSign; final DmgWindow? window; @override Map toJson() { return { 'title': title, 'icon': icon, 'background': background, 'background-color': backgroundColor, 'icon-size': iconSize, 'format': format, 'contents': contents.map((e) => e.toJson()).toList(), 'code-sign': codeSign?.toJson(), 'window': window?.toJson(), }..removeWhere((key, value) => value == null); } } class MakeDmgConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakeDmgConfig.fromJson(map).copyWith(baseMakeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/exe/app_package_maker_exe.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/exe/inno_setup/inno_setup_compiler.dart'; import 'package:flutter_app_packager/src/makers/exe/inno_setup/inno_setup_script.dart'; import 'package:flutter_app_packager/src/makers/exe/make_exe_config.dart'; import 'package:io/io.dart'; class AppPackageMakerExe extends AppPackageMaker { @override String get name => 'exe'; @override String get platform => 'windows'; @override bool get isSupportedOnCurrentPlatform => Platform.isWindows; @override String get packageFormat => 'exe'; @override MakeConfigLoader get configLoader { return MakeExeConfigLoader() ..platform = platform ..packageFormat = packageFormat; } @override Future make(MakeConfig config) { return _make( config.buildOutputDirectory, outputDirectory: config.outputDirectory, makeConfig: config as MakeExeConfig, ); } Future _make( Directory appDirectory, { required Directory outputDirectory, required MakeExeConfig makeConfig, }) async { Directory packagingDirectory = makeConfig.packagingDirectory; copyPathSync(appDirectory.path, packagingDirectory.path); InnoSetupScript script = InnoSetupScript.fromMakeConfig(makeConfig); InnoSetupCompiler compiler = InnoSetupCompiler(); bool compiled = await compiler.compile(script); if (!compiled) { throw MakeError(); } packagingDirectory.deleteSync(recursive: true); return MakeResult(makeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/exe/inno_setup/inno_setup_compiler.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/makers/exe/inno_setup/inno_setup_script.dart'; import 'package:path/path.dart' as p; import 'package:shell_executor/shell_executor.dart'; class InnoSetupCompiler { Future compile(InnoSetupScript script) async { Directory innoSetupDirectory = Directory('C:\\Program Files (x86)\\Inno Setup 6'); if (!innoSetupDirectory.existsSync()) { throw Exception('`Inno Setup 6` was not installed.'); } File file = await script.createFile(); ProcessResult processResult = await $( p.join(innoSetupDirectory.path, 'ISCC.exe'), [file.path], ); if (processResult.exitCode != 0) { return false; } file.deleteSync(recursive: true); return true; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/exe/inno_setup/inno_setup_script.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_packager/src/makers/exe/make_exe_config.dart'; import 'package:liquid_engine/liquid_engine.dart'; import 'package:path/path.dart' as path; const String _template = """ [Setup] AppId={{APP_ID}} AppVersion={{APP_VERSION}} AppName={{DISPLAY_NAME}} AppPublisher={{PUBLISHER_NAME}} AppPublisherURL={{PUBLISHER_URL}} AppSupportURL={{PUBLISHER_URL}} AppUpdatesURL={{PUBLISHER_URL}} DefaultDirName={{INSTALL_DIR_NAME}} DisableProgramGroupPage=yes OutputDir=. OutputBaseFilename={{OUTPUT_BASE_FILENAME}} Compression=lzma SolidCompression=yes SetupIconFile={{SETUP_ICON_FILE}} WizardStyle=modern PrivilegesRequired={{PRIVILEGES_REQUIRED}} ArchitecturesAllowed={{ARCH}} ArchitecturesInstallIn64BitMode={{ARCH}} [Languages] {% for locale in LOCALES %} {% if locale.lang == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %} {% if locale.lang == 'hy' %}Name: "armenian"; MessagesFile: "compiler:Languages\\Armenian.isl"{% endif %} {% if locale.lang == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %} {% if locale.lang == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %} {% if locale.lang == 'zh' %} Name: "chineseSimplified"; MessagesFile: {% if locale.file %}{{ locale.file }}{% else %}"compiler:Languages\\ChineseSimplified.isl"{% endif %} {% endif %} {% if locale.lang == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %} {% if locale.lang == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %} {% if locale.lang == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %} {% if locale.lang == 'nl' %}Name: "dutch"; MessagesFile: "compiler:Languages\\Dutch.isl"{% endif %} {% if locale.lang == 'fi' %}Name: "finnish"; MessagesFile: "compiler:Languages\\Finnish.isl"{% endif %} {% if locale.lang == 'fr' %}Name: "french"; MessagesFile: "compiler:Languages\\French.isl"{% endif %} {% if locale.lang == 'de' %}Name: "german"; MessagesFile: "compiler:Languages\\German.isl"{% endif %} {% if locale.lang == 'he' %}Name: "hebrew"; MessagesFile: "compiler:Languages\\Hebrew.isl"{% endif %} {% if locale.lang == 'is' %}Name: "icelandic"; MessagesFile: "compiler:Languages\\Icelandic.isl"{% endif %} {% if locale.lang == 'it' %}Name: "italian"; MessagesFile: "compiler:Languages\\Italian.isl"{% endif %} {% if locale.lang == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %} {% if locale.lang == 'no' %}Name: "norwegian"; MessagesFile: "compiler:Languages\\Norwegian.isl"{% endif %} {% if locale.lang == 'pl' %}Name: "polish"; MessagesFile: "compiler:Languages\\Polish.isl"{% endif %} {% if locale.lang == 'pt' %}Name: "portuguese"; MessagesFile: "compiler:Languages\\Portuguese.isl"{% endif %} {% if locale.lang == 'ru' %}Name: "russian"; MessagesFile: "compiler:Languages\\Russian.isl"{% endif %} {% if locale.lang == 'sk' %}Name: "slovak"; MessagesFile: "compiler:Languages\\Slovak.isl"{% endif %} {% if locale.lang == 'sl' %}Name: "slovenian"; MessagesFile: "compiler:Languages\\Slovenian.isl"{% endif %} {% if locale.lang == 'es' %}Name: "spanish"; MessagesFile: "compiler:Languages\\Spanish.isl"{% endif %} {% if locale.lang == 'tr' %}Name: "turkish"; MessagesFile: "compiler:Languages\\Turkish.isl"{% endif %} {% if locale.lang == 'uk' %}Name: "ukrainian"; MessagesFile: "compiler:Languages\\Ukrainian.isl"{% endif %} {% endfor %} [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %} Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %} [Files] Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}" Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon Name: "{userstartup}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup [Run] Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent """; class InnoSetupScript { InnoSetupScript({ required this.makeConfig, }); factory InnoSetupScript.fromMakeConfig(MakeExeConfig makeConfig) { return InnoSetupScript( makeConfig: makeConfig, ); } final MakeExeConfig makeConfig; Future createFile() async { Map variables = { 'APP_ID': makeConfig.appId, 'APP_NAME': makeConfig.appName, 'APP_VERSION': makeConfig.appVersion.toString(), 'EXECUTABLE_NAME': makeConfig.executableName ?? makeConfig.defaultExecutableName, 'DISPLAY_NAME': makeConfig.displayName, 'PUBLISHER_NAME': makeConfig.publisherName, 'ARCH': makeConfig.arch ?? 'x64', 'PUBLISHER_URL': makeConfig.publisherUrl, 'CREATE_DESKTOP_ICON': makeConfig.createDesktopIcon, 'LAUNCH_AT_STARTUP': makeConfig.launchAtStartup, 'INSTALL_DIR_NAME': makeConfig.installDirName ?? makeConfig.defaultInstallDirName, 'SOURCE_DIR': makeConfig.sourceDir, 'OUTPUT_BASE_FILENAME': makeConfig.outputBaseFileName, 'LOCALES': makeConfig.locales, 'SETUP_ICON_FILE': makeConfig.setupIconFile ?? '', 'PRIVILEGES_REQUIRED': makeConfig.privilegesRequired ?? 'none', }..removeWhere((key, value) => value == null); Context context = Context.create(); context.variables = variables; String scriptTemplate = _template; if (makeConfig.scriptTemplate != null) { File scriptTemplateFile = File( path.join( 'windows/packaging/exe/', makeConfig.scriptTemplate!, ), ); scriptTemplate = scriptTemplateFile.readAsStringSync(); } Template template = Template.parse( context, Source.fromString(scriptTemplate), ); String content = '\uFEFF${await template.render(context)}'; File file = File('${makeConfig.packagingDirectory.path}.iss'); file.writeAsBytesSync(utf8.encode(content)); return file; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/exe/make_exe_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:path/path.dart' as p; class MakeExeConfig extends MakeConfig { MakeExeConfig({ this.scriptTemplate, required this.appId, this.executableName, this.displayName, this.publisherName, this.publisherUrl, this.createDesktopIcon, this.launchAtStartup, this.installDirName, this.setupIconFile, this.privilegesRequired, this.locales, }); factory MakeExeConfig.fromJson(Map json) { List>? locales = json['locales'] != null ? List>.from(json['locales']) : null; if (locales == null || locales.isEmpty) { locales = [ {'lang': 'en'} ]; } MakeExeConfig makeExeConfig = MakeExeConfig( scriptTemplate: json['script_template'], appId: json['app_id'] ?? json['appId'], executableName: json['executable_name'], displayName: json['display_name'], publisherName: json['publisher_name'] ?? json['appPublisher'], publisherUrl: json['publisher_url'] ?? json['appPublisherUrl'], createDesktopIcon: json['create_desktop_icon'], launchAtStartup: json['launch_at_startup'], installDirName: json['install_dir_name'], setupIconFile: json['setup_icon_file'], privilegesRequired: json['privileges_required'], locales: locales, ); return makeExeConfig; } String? scriptTemplate; final String appId; String? executableName; String? displayName; String? publisherName; String? publisherUrl; bool? createDesktopIcon; bool? launchAtStartup; String? installDirName; String? setupIconFile; String? privilegesRequired; List>? locales; String get defaultExecutableName { File executableFile = packagingDirectory .listSync() .where((e) => e.path.endsWith('.exe')) .map((e) => File(e.path)) .first; return p.basename(executableFile.path); } String get defaultInstallDirName => '{autopf64}\\$appName'; String get sourceDir => p.basename(packagingDirectory.path); String get outputBaseFileName => p.basename(outputFile.path).replaceAll('.exe', ''); @override Map toJson() { return { 'script_template': scriptTemplate, 'app_id': appId, 'arch': arch, 'app_name': appName, 'app_version': appVersion.toString(), 'executable_name': executableName, 'display_name': displayName, 'publisher_name': publisherName, 'publisher_url': publisherUrl, 'create_desktop_icon': createDesktopIcon, 'launch_at_startup': launchAtStartup, 'install_dir_name': installDirName, 'setup_icon_file': setupIconFile, 'privileges_required': privilegesRequired, 'locales': locales, }..removeWhere((key, value) => value == null); } } class MakeExeConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakeExeConfig.fromJson(map).copyWith(baseMakeConfig) ..isInstaller = true; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/hap/app_package_maker_hap.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class AppPackageMakerHap extends AppPackageMaker { @override String get name => 'hap'; @override String get platform => 'ohos'; @override String get packageFormat => 'hap'; @override Future make(MakeConfig config) { File pkgFile = config.buildOutputFiles.first; pkgFile.copySync(config.outputFile.path); return Future.value(resultResolver.resolve(config)); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/ipa/app_package_maker_ipa.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class AppPackageMakerIpa extends AppPackageMaker { @override String get name => 'ipa'; @override String get platform => 'ios'; @override String get packageFormat => 'ipa'; @override Future make(MakeConfig config) { File pkgFile = config.buildOutputFiles.first; pkgFile.copySync(config.outputFile.path); return Future.value(resultResolver.resolve(config)); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/makers.dart ================================================ export 'aab/app_package_maker_aab.dart'; export 'apk/app_package_maker_apk.dart'; export 'appimage/app_package_maker_appimage.dart'; export 'deb/app_package_maker_deb.dart'; export 'direct/app_package_maker_direct.dart'; export 'dmg/app_package_maker_dmg.dart'; export 'exe/app_package_maker_exe.dart'; export 'ipa/app_package_maker_ipa.dart'; export 'msix/app_package_maker_msix.dart'; export 'pkg/app_package_maker_pkg.dart'; export 'rpm/app_package_maker_rpm.dart'; export 'zip/app_package_maker_zip.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/msix/app_package_maker_msix.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/msix/make_msix_config.dart'; import 'package:msix/msix.dart'; import 'package:path/path.dart' as p; class AppPackageMakerMsix extends AppPackageMaker { @override String get name => 'msix'; @override String get platform => 'windows'; @override bool get isSupportedOnCurrentPlatform => Platform.isWindows; @override String get packageFormat => 'msix'; @override MakeConfigLoader get configLoader { return MakeMsixConfigLoader() ..platform = platform ..packageFormat = packageFormat; } @override Future make(MakeConfig config) { return _make( config.buildOutputDirectory, outputDirectory: config.outputDirectory, makeConfig: config as MakeMsixConfig, ); } Future _make( Directory appDirectory, { required Directory outputDirectory, required MakeMsixConfig makeConfig, }) async { makeConfig.output_path = makeConfig.outputFile.parent.path; makeConfig.output_name = p.basenameWithoutExtension(makeConfig.outputFile.path); makeConfig.build_windows = 'false'; Map makeConfigJson = makeConfig.toJson(); List arguments = []; for (String key in makeConfigJson.keys) { dynamic value = makeConfigJson[key]; String newKey = key.replaceAll('_', '-'); if (newKey == 'msix-version') newKey = 'version'; if (value is Map) { for (String subKey in value.keys) { arguments.addAll(['--$newKey', '$subKey=${value[subKey]}']); } } else { arguments.addAll(['--$newKey', value]); } } await Msix(arguments).create(); return MakeResult(makeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/msix/make_msix_config.dart ================================================ // ignore_for_file: non_constant_identifier_names import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class MakeMsixConfig extends MakeConfig { MakeMsixConfig({ this.display_name, this.publisher_display_name, this.identity_name, this.msix_version, this.logo_path, this.trim_logo, this.capabilities, this.languages, this.file_extension, this.protocol_activation, this.add_execution_alias, this.enable_at_startup, this.store, this.debug, this.output_path, this.output_name, this.architecture, this.build_windows, this.certificate_path, this.certificate_password, this.publisher, this.signtool_options, this.sign_msix, this.install_certificate, }); factory MakeMsixConfig.fromJson(Map json) { return MakeMsixConfig( display_name: json['display_name'], publisher_display_name: json['publisher_display_name'], identity_name: json['identity_name'], msix_version: json['msix_version'], logo_path: json['logo_path'], trim_logo: json['trim_logo'], capabilities: json['capabilities'], languages: json['languages'], file_extension: json['file_extension'], protocol_activation: json['protocol_activation'], add_execution_alias: json['add_execution_alias'], enable_at_startup: json['enable_at_startup'], store: json['store'], debug: json['debug'], output_path: json['output_path'], output_name: json['output_name'], architecture: json['architecture'], build_windows: json['build_windows'], certificate_path: json['certificate_path'], certificate_password: json['certificate_password'], publisher: json['publisher'], signtool_options: json['signtool_options'], sign_msix: json['sign_msix'], install_certificate: json['install_certificate'], ); } // MSIX configuration /// A friendly app name that can be displayed to users. String? display_name; /// A friendly name for the publisher that can be displayed to users. String? publisher_display_name; /// Defines the unique identifier for the app. String? identity_name; /// The version number of the package, in `a.b.c.d` format. String? msix_version; /// Path to an [image file] for use as the app icon (size recommended at least 400x400px). String? logo_path; /// If `false`, don't trim the logo image, default is `true`. String? trim_logo; /// List of the [capabilities][windows capabilities] the app requires. String? capabilities; /// Declares the language resources contained in the package. String? languages; /// File extensions that the app may be registered to open. String? file_extension; /// [Protocol activation] that will open the app. String? protocol_activation; /// Add an alias to active the app, use the `pubspec.yaml` `name:` value, so if your app calls 'Flutter_App', user can activate the app using `flutterapp` command. String? add_execution_alias; /// App start at startup or user log-in. String? enable_at_startup; /// Generate a MSIX file for publishing to the Microsoft Store. | String? store; // Build configuration /// Create MSIX from the **debug** or **release** build files (`\build\windows\runner\`), **release** is the default. | `true` | String? debug; /// The directory where the output MSIX file should be stored. | `C:\src\some\folder` | String? output_path; /// The filename that should be given to the created MSIX file. | `flutterApp_dev` | String? output_name; /// Describes the architecture of the code in the package, `x64` or `x86`, `x64` is default. | `x64` | String? architecture; /// If `false`, don't run the build command `flutter build windows`, default is `true`. | `true` | String? build_windows; /// Path to the certificate content to place in the store. | `C:\certs\signcert.pfx` | String? certificate_path; /// Password for the certificate. | `1234` | String? certificate_password; /// The `Subject` value in the certificate. | `CN=BF212345-5644-46DF-8668-014043C1B138` or `CN=Contoso Software, O=Contoso Corporation, C=US` | String? publisher; /// Options to be provided to the `signtool` for app signing (see below.) | `/v /fd SHA256 /f C:/Users/me/Desktop/my.cer` | String? signtool_options; /// If `false`, don't sign the msix file, default is `true`. | `true` | String? sign_msix; /// If `false`, don't try to install the certificate, default is `true`. | `true` | String? install_certificate; @override Map toJson() { return { 'display_name': display_name, 'publisher_display_name': publisher_display_name, 'identity_name': identity_name, 'msix_version': msix_version, 'logo_path': logo_path, 'trim_logo': trim_logo, 'capabilities': capabilities, 'languages': languages, 'file_extension': file_extension, 'protocol_activation': protocol_activation, 'add_execution_alias': add_execution_alias, 'enable_at_startup': enable_at_startup, 'store': store, 'debug': debug, 'output_path': output_path, 'output_name': output_name, 'architecture': architecture, 'build_windows': build_windows, 'certificate_path': certificate_path, 'certificate_password': certificate_password, 'publisher': publisher, 'signtool_options': signtool_options, 'sign_msix': sign_msix, 'install_certificate': install_certificate, }..removeWhere((key, value) => value == null); } } class MakeMsixConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakeMsixConfig.fromJson(map).copyWith(baseMakeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/pacman/app_package_maker_pacman.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/pacman/make_pacman_config.dart'; import 'package:path/path.dart' as path; import 'package:shell_executor/shell_executor.dart'; class AppPackageMakerPacman extends AppPackageMaker { @override String get name => 'pacman'; @override String get platform => 'linux'; @override bool get isSupportedOnCurrentPlatform => Platform.isLinux; @override String get packageFormat => 'pacman'; @override MakeConfigLoader get configLoader { return MakePacmanConfigLoader() ..platform = platform ..packageFormat = packageFormat; } @override Future make(MakeConfig config) { return _make( config.buildOutputDirectory, outputDirectory: config.outputDirectory, makeConfig: config as MakePacmanConfig, ); } Future _make( Directory appDirectory, { required Directory outputDirectory, required MakePacmanConfig makeConfig, }) async { final files = makeConfig.toFilesString(); Directory packagingDirectory = makeConfig.packagingDirectory; /// Need to create following directories /// /usr/share/$appBinaryName /// /usr/share/applications /// /usr/share/icons/hicolor/128x128/apps /// /usr/share/icons/hicolor/256x256/apps final applicationsDir = path.join(packagingDirectory.path, 'usr/share/applications'); final icon128Dir = path.join( packagingDirectory.path, 'usr/share/icons/hicolor/128x128/apps', ); final icon256Dir = path.join( packagingDirectory.path, 'usr/share/icons/hicolor/256x256/apps', ); final metainfoDir = path.join(packagingDirectory.path, 'usr/share/metainfo'); final mkdirProcessResult = await $('mkdir', [ '-p', path.join(packagingDirectory.path, 'usr/share', makeConfig.appBinaryName), applicationsDir, if (makeConfig.metainfo != null) metainfoDir, if (makeConfig.icon != null) ...[icon128Dir, icon256Dir], ]); if (mkdirProcessResult.exitCode != 0) throw MakeError(); if (makeConfig.icon != null) { final iconFile = File(makeConfig.icon!); if (!iconFile.existsSync()) { throw MakeError("provided icon ${makeConfig.icon} path wasn't found"); } await iconFile.copy( path.join( icon128Dir, makeConfig.appBinaryName + path.extension(makeConfig.icon!), ), ); await iconFile.copy( path.join( icon256Dir, makeConfig.appBinaryName + path.extension(makeConfig.icon!), ), ); } if (makeConfig.metainfo != null) { final metainfoPath = path.join(Directory.current.path, makeConfig.metainfo!); final metainfoFile = File(metainfoPath); if (!metainfoFile.existsSync()) { throw MakeError("Metainfo $metainfoPath path wasn't found"); } await metainfoFile.copy( path.join( metainfoDir, makeConfig.appBinaryName + path.extension(makeConfig.metainfo!, 2), ), ); } // create & write the files got from makeConfig final installFile = File(path.join(packagingDirectory.path, '.INSTALL')); final pkgInfoFile = File(path.join(packagingDirectory.path, '.PKGINFO')); final desktopEntryFile = File(path.join(applicationsDir, '${makeConfig.appBinaryName}.desktop')); if (!installFile.existsSync()) installFile.createSync(); if (!pkgInfoFile.existsSync()) pkgInfoFile.createSync(); if (!desktopEntryFile.existsSync()) desktopEntryFile.createSync(); await installFile.writeAsString(files['INSTALL']!); await pkgInfoFile.writeAsString(files['PKGINFO']!); await desktopEntryFile.writeAsString(files['DESKTOP']!); // copy the application binary to /usr/share/$appBinaryName await $('cp', [ '-fr', '${appDirectory.path}/.', '${packagingDirectory.path}/usr/share/${makeConfig.appBinaryName}/', ]); // MTREE Metadata using bsdtar and fakeroot ProcessResult mtreeResult = await $( 'bsdtar', [ '-czf', '.MTREE', '--format=mtree', '--options=!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link', '.PKGINFO', '.INSTALL', 'usr', ], environment: { 'LANG': 'C', }, workingDirectory: packagingDirectory.path, ); if (mtreeResult.exitCode != 0) { throw MakeError(mtreeResult.stderr); } // create the pacman package using fakeroot and bsdtar // fakeroot -- env LANG=C bsdtar -cf - .MTREE .PKGINFO * | xz -c -z - > $pkgname-$pkgver-$pkgrel-$arch.tar.xz ProcessResult archiveResult = await $( 'bsdtar', [ '-cf', 'temptar', '.MTREE', '.INSTALL', '.PKGINFO', 'usr', ], environment: { 'LANG': 'C', }, workingDirectory: packagingDirectory.path, ); if (archiveResult.exitCode != 0) { throw MakeError(archiveResult.stderr); } ProcessResult processResult = await $( 'xz', [ '-z', 'temptar', ], workingDirectory: packagingDirectory.path, ); if (processResult.exitCode != 0) { throw MakeError(processResult.stderr); } // copy file from temptar.xz to the makeConfig.outputFile.path final copyResult = await $( 'mv', [ '${packagingDirectory.path}/temptar.xz', makeConfig.outputFile.path, ], ); if (copyResult.exitCode != 0) { throw MakeError(copyResult.stderr); } packagingDirectory.deleteSync(recursive: true); return MakeResult(makeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/pacman/make_pacman_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; // Ported from https://gist.github.com/Earnestly/bebad057f40a662b5cc3 // format of make_config for pacman /* # the name used to display in the OS. Specifically desktop # entry name display_name: Hola Amigos # package name for arch repository # the name should be all lowercase with -+. package_name: hola-amigos licenses: - MIT maintainer: name: Gamer Boy 69 email: rickastley@gmail.lol # the size of binary in kilobyte installed_size: 24400 # direct dependencies required by the application # refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES dependencies: - mysupercooldep # optional dependencies not so much required by the application # refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES optional_dependencies: - iamalwaysoptional # refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES provides: - whatsup # refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES options: - zipman # refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES conflicts: - libwhatsup # refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES replaces: - yourdep # refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES provides: - libx11 postinstall_scripts: - echo `Installed my awesome app` postupgrade_scripts: - echo `Supercharger my awesome app` postuninstall_scripts: - echo `Surprised Pickachu face` # application icon path relative to project url icon: assets/logo.png keywords: - Hello - World - Test - Application # a name to categorize the app into a section of application generic_name: Hobby Application # supported mime types that can be opened using this application supported_mime_type: - audio/mpeg # shown when right clicked the desktop entry icons actions: - Gallery - Create # the categories the application belong to # refer: https://specifications.freedesktop.org/menu-spec/latest/ categories: - Music - Media # let OS know if the application can be run on start_up. If it's false # the application will deny to the OS if it was added as a start_up # application startup_notify: true */ class MakePacmanConfig extends MakeLinuxPackageConfig { MakePacmanConfig({ required this.displayName, required this.packageName, this.installedSize, required this.maintainer, this.packageRelease = 1, List? postinstallScripts, List? postupgradeScripts, List? postuninstallScripts, this.actions, this.categories, this.dependencies, this.genericName, this.optDependencies, this.options, this.startupNotify = false, this.groups = const ['default'], this.licenses = const ['unknown'], this.icon, this.metainfo, this.keywords, this.provides, this.conflicts, this.replaces, this.supportedMimeType, }) : _postinstallScripts = postinstallScripts ?? [], _postupgradeScripts = postupgradeScripts ?? [], _postremoveScripts = postuninstallScripts ?? []; factory MakePacmanConfig.fromJson(Map map) { return MakePacmanConfig( displayName: map['display_name'], packageName: map['package_name'], // packageRelease: int.tryParse(map['package_release'] ?? '1') ?? 1, maintainer: "${map['maintainer']['name']} <${map['maintainer']['email']}>", dependencies: map['dependencies'] != null ? List.castFrom(map['dependencies']) : null, conflicts: map['conflicts'] != null ? List.castFrom(map['conflicts']) : null, replaces: map['replaces'] != null ? List.castFrom(map['replaces']) : null, options: map['options'] != null ? List.castFrom(map['options']) : null, optDependencies: map['optional_dependencies'] != null ? List.castFrom(map['optional_dependencies']) : null, licenses: map['licenses'] != null ? List.castFrom(map['licenses']) : ['unknown'], groups: map['groups'] != null ? List.castFrom(map['groups']) : ['default'], provides: map['provides'] != null ? List.castFrom(map['provides']) : null, postinstallScripts: map['postinstall_scripts'] != null ? List.castFrom(map['postinstall_scripts']) : null, postuninstallScripts: map['postuninstall_scripts'] != null ? List.castFrom(map['postuninstall_scripts']) : null, postupgradeScripts: map['postupgrade_scripts'] != null ? List.castFrom(map['postupgrade_scripts']) : null, keywords: map['keywords'] != null ? List.castFrom(map['keywords']) : null, supportedMimeType: map['supported_mime_type'] != null ? List.castFrom(map['supported_mime_type']) : null, actions: map['actions'] != null ? List.castFrom(map['actions']) : null, categories: map['categories'] != null ? List.castFrom(map['categories']) : null, startupNotify: map['startup_notify'], genericName: map['generic_name'], installedSize: map['installed_size'], icon: map['icon'], metainfo: map['metainfo'], ); } String displayName; String packageName; String maintainer; int packageRelease; int? installedSize; List licenses; List groups; String? icon; String? metainfo; String? genericName; bool startupNotify; List? options; List? dependencies; List? optDependencies; List? conflicts; List? replaces; List? provides; List _postinstallScripts; List _postupgradeScripts; List _postremoveScripts; List? keywords; List? supportedMimeType; List? actions; List? categories; List get postinstallScripts => [ 'ln -s /usr/share/$appBinaryName/$appBinaryName /usr/bin/$appBinaryName', 'chmod +x /usr/bin/$appBinaryName', ..._postinstallScripts, ]; List get postuninstallScripts => [ 'rm /usr/bin/$appBinaryName', ..._postremoveScripts, ]; List get postupgradeScripts => _postupgradeScripts; @override Map toJson() { return { 'PKGINFO': { 'pkgname': packageName, 'pkgver': appVersion.toString(), 'pkgdesc': pubspec.description, 'packager': maintainer, 'size': installedSize, 'license': '(${licenses.join(', ')})', 'groups': '(${groups.join(', ')})', 'arch': '(${_getArchitecture()})', 'url': pubspec.homepage, 'options': options != null ? "(${options!.join(', ')})" : null, 'depends': dependencies != null ? "(${dependencies!.join(', ')})" : null, 'optdepends': optDependencies != null ? "(${optDependencies!.join(', ')})" : null, 'conflicts': conflicts != null ? "(${conflicts!.join(', ')})" : null, 'replaces': replaces != null ? "(${replaces!.join(', ')})" : null, 'provides': provides != null ? "(${provides!.join(', ')})" : null, }..removeWhere((key, value) => value == null), 'DESKTOP': { 'Type': 'Application', 'Version': appVersion.toString(), 'Name': displayName, 'GenericName': genericName, 'Icon': appBinaryName, 'Exec': '$appBinaryName %U', 'Actions': actions != null && actions!.isNotEmpty ? '${actions!.join(';')};' : null, 'MimeType': supportedMimeType != null && supportedMimeType!.isNotEmpty ? '${supportedMimeType!.join(';')};' : null, 'Categories': categories != null && categories!.isNotEmpty ? '${categories!.join(';')};' : null, 'Keywords': keywords != null && keywords!.isNotEmpty ? '${keywords!.join(';')};' : null, 'StartupNotify': startupNotify, }..removeWhere((key, value) => value == null), }; } Map toFilesString() { final json = toJson(); final pkginfoFile = '${(json['PKGINFO'] as Map).entries.map( (e) => '${e.key}=${e.value}', ).join('\n')}\n'; final installFileMap = { 'post_install': postinstallScripts.join('\n\t'), 'post_upgrade': postupgradeScripts.isNotEmpty ? postupgradeScripts.join('\n') : null, 'post_remove': postuninstallScripts.join('\n'), }..removeWhere((key, value) => value == null); final installFile = installFileMap.entries .map( (e) => '${e.key}() {\n\t${e.value}\n}', ) .join('\n'); final desktopFile = [ '[Desktop Entry]', ...(json['DESKTOP'] as Map).entries.map( (e) => '${e.key}=${e.value}', ), ].join('\n'); final map = { 'PKGINFO': pkginfoFile, 'DESKTOP': desktopFile, 'INSTALL': installFile, }; return map; } } class MakePacmanConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakePacmanConfig.fromJson(map).copyWith(baseMakeConfig); } } String _getArchitecture() { final result = Process.runSync('uname', ['-m']); if ('${result.stdout}'.trim() == 'aarch64') { return 'aarch64'; } else { return 'x86_64'; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/pkg/app_package_maker_pkg.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/pkg/make_pkg_config.dart'; import 'package:shell_executor/shell_executor.dart'; class AppPackageMakerPkg extends AppPackageMaker { @override String get name => 'pkg'; @override String get platform => 'macos'; @override String get packageFormat => 'pkg'; @override MakeConfigLoader get configLoader { return MakePkgConfigLoader() ..platform = platform ..packageFormat = packageFormat; } @override Future make(MakeConfig config) async { MakePkgConfig makeConfig = config as MakePkgConfig; File appFile = config.buildOutputFiles.first; File outputFile = config.outputFile; File unsignedPkgFile = File( outputFile.path.replaceFirst( '.$packageFormat', '-unsigned.$packageFormat', ), ); await $('xcrun', [ 'productbuild', '--root', appFile.path, makeConfig.installPath ?? '/Applications/', unsignedPkgFile.path, ]); if (makeConfig.signIdentity != null) { await $('xcrun', [ 'productsign', '--sign', makeConfig.signIdentity!, unsignedPkgFile.path, outputFile.path, ]); unsignedPkgFile.deleteSync(); } else { unsignedPkgFile.renameSync(outputFile.path); } return Future.value(resultResolver.resolve(config)); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/pkg/make_pkg_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class MakePkgConfig extends MakeConfig { MakePkgConfig({ this.installPath, this.signIdentity, }); factory MakePkgConfig.fromJson(Map json) { return MakePkgConfig( installPath: json['install-path'], signIdentity: json['sign-identity'], ); } final String? installPath; final String? signIdentity; @override Map toJson() { return { 'install-path': installPath, 'sign-identity': signIdentity, }..removeWhere((key, value) => value == null); } } class MakePkgConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakePkgConfig.fromJson(map).copyWith(baseMakeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/rpm/app_package_maker_rpm.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/rpm/make_rpm_config.dart'; import 'package:flutter_app_packager/src/makers/rpm/rpmbuild.dart'; import 'package:path/path.dart' as path; import 'package:shell_executor/shell_executor.dart'; class AppPackageMakerRPM extends AppPackageMaker { @override List get requirements => [rpmbuild]; @override String get name => 'rpm'; @override String get platform => 'linux'; @override String get packageFormat => 'rpm'; @override bool get isSupportedOnCurrentPlatform => Platform.isLinux; @override MakeConfigLoader get configLoader { return MakeRpmConfigLoader() ..platform = platform ..packageFormat = packageFormat; } @override Future make(MakeConfig config) { return _make( config.buildOutputDirectory, outputDirectory: config.outputDirectory, makeConfig: config as MakeRPMConfig, ); } Future _make( Directory appDirectory, { required Directory outputDirectory, required MakeRPMConfig makeConfig, }) async { final files = makeConfig.toFilesString(); Directory packagingDirectory = makeConfig.packagingDirectory; // making rpm setup directory final rpmbuild = [ 'BUILD', 'BUILDROOT', 'RPMS', 'SOURCES', 'SPECS', 'SRPMS', ]; final rpmbuildDirPath = path.join(packagingDirectory.absolute.path, 'rpmbuild'); for (final dir in rpmbuild) { final dirPath = path.join(rpmbuildDirPath, dir); final dirFile = Directory(dirPath); if (!dirFile.existsSync()) { dirFile.createSync(recursive: true); } } // making rpmbuild/BUILD/[appName] directory final buildPath = path.join(rpmbuildDirPath, 'BUILD'); final buildRoot = path.join(buildPath, makeConfig.appName); final specsPath = path.join(rpmbuildDirPath, 'SPECS'); final rpmPath = path.join(rpmbuildDirPath, 'RPMS', makeConfig.buildArch ?? 'x86_64'); final buildWivesDirFile = Directory(buildRoot); if (!buildWivesDirFile.existsSync()) { buildWivesDirFile.createSync(recursive: true); } /// copying app files to rpmbuild/BUILD/[appName] directory final bundleFiles = appDirectory.listSync(); for (final file in bundleFiles) { await $( 'cp', [ '-r', file.path, buildRoot, ], ); } // fix lib_*_plugin.so rpath // // more details: https://github.com/flutter/flutter/issues/65400 final libFiles = Directory(path.join(buildRoot, 'lib')).listSync(); for (final file in libFiles) { if (file is! File) continue; if (!file.path.endsWith('.so')) continue; // check if points to /home dir final processResult = await $( 'patchelf', [ '--print-rpath', file.path, ], ); if (processResult.stdout.toString().contains('/home')) { await $( 'patchelf', [ '--set-rpath', '\$ORIGIN', file.path, ], ); } } final iconFile = makeConfig.icon != null ? File(path.join(Directory.current.path, makeConfig.icon!)) : null; iconFile?.copy( path.join( buildPath, makeConfig.appName + path.extension(iconFile.path), ), ); if (makeConfig.icon != null) { final iconFile = File(makeConfig.icon!); if (!iconFile.existsSync()) { throw MakeError("provided icon ${makeConfig.icon} path wasn't found"); } } // create & write the files got from makeConfig final specFile = File(path.join(specsPath, '${makeConfig.appName}.spec')); final desktopEntryFile = File(path.join(buildPath, '${makeConfig.appName}.desktop')); if (!specFile.existsSync()) specFile.createSync(); if (!desktopEntryFile.existsSync()) desktopEntryFile.createSync(); await specFile.writeAsString(files['SPEC']!); await desktopEntryFile.writeAsString(files['DESKTOP']!); // make the rpm final processResult = await $( 'rpmbuild', [ '--define', '_topdir $rpmbuildDirPath', '-bb', specFile.path, ], environment: {'QA_RPATHS': (0x0001 | 0x0010).toString()}, ); if (processResult.exitCode != 0) { throw MakeError(); } final rpms = Directory(rpmPath).listSync(); for (var rpm in rpms) { if (rpm is! File) continue; await $( 'cp', [ rpm.path, makeConfig.outputFile.path, ], ); break; } packagingDirectory.deleteSync(recursive: true); return MakeResult(makeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/rpm/make_rpm_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; class MakeRPMConfig extends MakeConfig { MakeRPMConfig({ // Desktop file required this.displayName, this.startupNotify = true, this.actions, this.categories, this.genericName, this.icon, this.keywords, this.supportedMimeType, // Spec file this.summary, this.group, this.vendor, this.packager, this.packagerEmail, this.license, this.url, this.buildArch, this.requires, this.buildRequires, this.description, this.prep, this.build, this.install, this.postun, this.files, this.defattr, this.attr, this.changelog, }); factory MakeRPMConfig.fromJson(Map json) { return MakeRPMConfig( displayName: json['display_name'] as String, icon: json['icon'] as String?, genericName: json['generic_name'] as String?, startupNotify: json['startup_notify'] as bool?, keywords: (json['keywords'] as List?)?.cast(), supportedMimeType: (json['supported_mime_type'] as List?)?.cast(), actions: (json['actions'] as List?)?.cast(), categories: (json['categories'] as List?)?.cast(), summary: json['summary'] as String?, group: json['group'] as String?, vendor: json['vendor'] as String?, packager: json['packager'] as String?, packagerEmail: json['packagerEmail'] as String?, license: json['license'] as String?, url: json['url'] as String?, buildArch: json['build_arch'] as String?, requires: (json['requires'] as List?)?.cast(), buildRequires: (json['build_requires'] as List?)?.cast(), description: json['description'] as String?, prep: json['prep'] as String?, build: json['build'] as String?, install: json['install'] as String?, postun: json['postun'] as String?, files: json['files'] as String?, defattr: json['defattr'] as String?, attr: json['attr'] as String?, changelog: json['changelog'] as String?, ); } String displayName; String? icon; String? genericName; bool? startupNotify; List? keywords; List? supportedMimeType; List? actions; List? categories; //RPM preamble Spec file fields String? summary; String? group; String? vendor; String? packager; String? packagerEmail; String? license; String? url; String? buildArch; List? requires; List? buildRequires; //RPM postamble Spec file fields String? description; String? prep; String? build; String? install; String? postun; String? files; String? defattr; String? attr; String? changelog; @override Map toJson() { return { 'SPEC': { 'preamble': { 'Name': appName, 'Version': appVersion.toString(), 'Release': "${appVersion.build.isNotEmpty ? appVersion.build.first : "1"}%{?dist}", 'Summary': summary ?? pubspec.description, 'Group': group, 'Vendor': vendor, 'Packager': packagerEmail != null ? '$packager <$packagerEmail>' : packager, 'License': license, 'URL': url, 'Requires': requires?.join(', '), 'BuildRequires': buildRequires?.join(', '), 'BuildArch': buildArch ?? 'x86_64', }..removeWhere((key, value) => value == null), 'body': { '%description': description ?? pubspec.description, '%install': [ 'mkdir -p %{buildroot}%{_bindir}', 'mkdir -p %{buildroot}%{_datadir}/%{name}', 'mkdir -p %{buildroot}%{_datadir}/applications', 'mkdir -p %{buildroot}%{_datadir}/pixmaps', 'cp -r %{name}/* %{buildroot}%{_datadir}/%{name}', 'ln -s %{_datadir}/%{name}/%{name} %{buildroot}%{_bindir}/%{name}', 'cp -r %{name}.desktop %{buildroot}%{_datadir}/applications', 'cp -r %{name}.png %{buildroot}%{_datadir}/pixmaps', 'update-mime-database %{_datadir}/mime &> /dev/null || :', ].join('\n'), '%postun': ['update-mime-database %{_datadir}/mime &> /dev/null || :'] .join('\n'), '%files': [ '%{_bindir}/%{name}', '%{_datadir}/%{name}', '%{_datadir}/applications/%{name}.desktop', ].join('\n'), }..removeWhere((key, value) => value == null), 'inline-body': { '%defattr': '(-,root,root)', '%attr': '(4755, root, root) %{_datadir}/pixmaps/%{name}.png', }, }, 'DESKTOP': { 'Type': 'Application', 'Version': appVersion.toString(), 'Name': displayName, 'GenericName': genericName, 'Icon': appName, 'Exec': '$appName %U', 'Actions': actions != null && actions!.isNotEmpty ? '${actions!.join(';')};' : null, 'MimeType': supportedMimeType != null && supportedMimeType!.isNotEmpty ? '${supportedMimeType!.join(';')};' : null, 'Categories': categories != null && categories!.isNotEmpty ? '${categories!.join(';')};' : null, 'Keywords': keywords != null && keywords!.isNotEmpty ? '${keywords!.join(';')};' : null, 'StartupNotify': startupNotify, }..removeWhere((key, value) => value == null), }; } Map toFilesString() { final json = toJson(); final preamble = (json['SPEC']['preamble'] as Map) .entries .map((e) => '${e.key}: ${e.value}') .join('\n'); final body = (json['SPEC']['body'] as Map).entries.map( (e) { return '${e.key}\n${e.value}\n'; }, ).join('\n'); final inlineBody = (json['SPEC']['inline-body'] as Map).entries.map( (e) { return '${e.key}${e.value}\n'; }, ).join('\n'); final desktopFile = [ '[Desktop Entry]', ...(json['DESKTOP'] as Map).entries.map( (e) => '${e.key}=${e.value}', ), ].join('\n'); final map = { 'DESKTOP': desktopFile, 'SPEC': '$preamble\n\n$body\n\n$inlineBody', }; return Map.castFrom(map); } } class MakeRpmConfigLoader extends DefaultMakeConfigLoader { @override MakeConfig load( Map? arguments, Directory outputDirectory, { required Directory buildOutputDirectory, required List buildOutputFiles, }) { final baseMakeConfig = super.load( arguments, outputDirectory, buildOutputDirectory: buildOutputDirectory, buildOutputFiles: buildOutputFiles, ); final map = loadMakeConfigYaml( '$platform/packaging/$packageFormat/make_config.yaml', ); return MakeRPMConfig.fromJson(map).copyWith(baseMakeConfig); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/rpm/rpmbuild.dart ================================================ import 'package:shell_executor/shell_executor.dart'; class _RpmBuild extends Command { @override String get executable => 'rpmbuild'; @override Future install() { return Future.value(); } } final rpmbuild = _RpmBuild(); ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/lib/src/makers/zip/app_package_maker_zip.dart ================================================ import 'dart:io'; import 'package:archive/archive_io.dart'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:shell_executor/shell_executor.dart'; class AppPackageMakerZip extends AppPackageMaker { AppPackageMakerZip(String platform) { _platform = platform; } late String _platform; @override String get name => 'zip'; @override String get platform => _platform; @override String get packageFormat => 'zip'; @override Future make(MakeConfig config) async { Directory appDirectory = config.buildOutputDirectory; Directory packagingDirectory = appDirectory; if (platform == 'macos') { // 由于使用 archive 在压缩时会导致 app 损坏,所以这里使用 7z 压缩。 packagingDirectory = config.packagingDirectory; File appFile = appDirectory .listSync() .where((e) => e.path.endsWith('.app')) .map((e) => File(e.path)) .first; await $('cp', ['-RH', appFile.path, packagingDirectory.path]); await $( '7z', ['a', config.outputFile.path, './${packagingDirectory.path}/*.app'], ); packagingDirectory.deleteSync(recursive: true); } else { final zipFileEncoder = ZipFileEncoder(); zipFileEncoder.zipDirectory( packagingDirectory, filename: config.outputFile.path, followLinks: true, ); } return resultResolver.resolve(config); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/pubspec.yaml ================================================ name: flutter_app_packager description: Package your Flutter app into OS-specific bundles (.dmg, .exe, etc.) via Dart or the command line. version: 0.4.2 homepage: https://github.com/leanflutter/flutter_distributor environment: sdk: ">=2.16.0 <4.0.0" dependencies: archive: ^3.4.10 io: ^1.0.3 liquid_engine: ^0.2.2 msix: ^3.16.6 mustache_template: ^2.0.0 path: ^1.8.1 pub_semver: ^2.1.0 pubspec_parse: ^1.1.0 shell_executor: ^0.1.5 yaml: ^3.1.0 dev_dependencies: dependency_validator: ^3.0.0 ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_packager/test/src/api/make_config_test.dart ================================================ import 'dart:io'; import 'package:flutter_app_packager/src/api/make_config.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; void main() { group('MakeConfig', () { test('#1', () { final makeConfig = MakeConfig() ..buildMode = 'release' ..buildOutputDirectory = Directory('build') ..buildOutputFiles = [] ..platform = 'android' ..packageFormat = 'apk' ..outputDirectory = Directory('dist/') ..pubspec = Pubspec( 'test_app', version: Version.parse('1.0.0'), ); expect( makeConfig.outputArtifactPath, 'dist/1.0.0/test_app-1.0.0-android.apk', ); }); test('#2', () { final makeConfig = MakeConfig() ..buildMode = 'release' ..buildOutputDirectory = Directory('build') ..buildOutputFiles = [] ..platform = 'android' ..packageFormat = 'apk' ..outputDirectory = Directory('dist/') ..pubspec = Pubspec( 'test_app', version: Version.parse('1.0.0+1'), ); expect( makeConfig.outputArtifactPath, 'dist/1.0.0+1/test_app-1.0.0+1-android.apk', ); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/analysis_options.yaml ================================================ include: package:mostly_reasonable_lints/analysis_options.yaml linter: rules: avoid_print: false ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/flutter_app_publisher.dart ================================================ library flutter_app_publisher; export 'src/api/app_package_publisher.dart'; export 'src/flutter_app_publisher.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/api/app_package_publisher.dart ================================================ import 'dart:io'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:shell_executor/shell_executor.dart'; typedef PublishProgressCallback = void Function(int sent, int total); abstract class AppPackagePublisher { List get requirements => []; String get name => throw UnimplementedError(); List get supportedPlatforms => throw UnimplementedError(); Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }); } class PublishConfig { Pubspec? _pubspec; Pubspec get pubspec { if (_pubspec == null) { final yamlString = File('pubspec.yaml').readAsStringSync(); _pubspec = Pubspec.parse(yamlString); } return _pubspec!; } } class PublishResult { PublishResult({ this.url, }); final String? url; } class PublishError extends Error { PublishError([this.message]); final String? message; @override String toString() { var message = this.message; return (message != null) ? 'PublishError: $message' : 'PublishError'; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/flutter_app_publisher.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/publishers.dart'; class FlutterAppPublisher { final List _publishers = [ AppPackagePublisherAppCenter(), AppPackagePublisherAppStore(), AppPackagePublisherFir(), AppPackagePublisherFirebase(), AppPackagePublisherFirebaseHosting(), AppPackagePublisherGithub(), AppPackagePublisherPgyer(), AppPackagePublisherPlayStore(), AppPackagePublisherQiniu(), AppPackagePublisherVercel(), ]; Future publish( FileSystemEntity fileSystemEntity, { required String target, Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { AppPackagePublisher publisher = _publishers.firstWhere( (e) => e.name == target, ); return await publisher.publish( fileSystemEntity, environment: environment, publishArguments: publishArguments, onPublishProgress: onPublishProgress, ); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/appcenter/app_package_publisher_appcenter.dart ================================================ import 'dart:io'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/appcenter/publish_appcenter_config.dart'; import 'package:shell_executor/shell_executor.dart'; const _kUploadDomain = 'https://file.appcenter.ms/upload'; class AppPackagePublisherAppCenter extends AppPackagePublisher { final Dio _dio = Dio(); @override String get name => 'appcenter'; @override List get supportedPlatforms => [ 'android', 'ios', 'linux', 'macos', 'windows', 'web', ]; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; PublishAppCenterConfig publishConfig = PublishAppCenterConfig.parse( environment, publishArguments, ); _dio.options = BaseOptions( baseUrl: 'https://api.appcenter.ms/v0.1', headers: { 'X-API-Token': publishConfig.apiToken, }, ); // _dio.interceptors.add(LogInterceptor( // requestBody: true, // responseBody: true, // )); try { // Creating release (1/7) Map release = await _createRelease( ownerName: publishConfig.ownerName, appName: publishConfig.appName, ); String releasesId = release['id']; String packageAssetId = release['package_asset_id']; String urlEncodedToken = release['url_encoded_token']; // Creating metadata (2/7) String fileName = file.path.split('/').last; String contentType = 'application/octet-stream'; if (fileName.endsWith('.apk')) { contentType = 'application/vnd.android.package-archive'; } Map metadata = await _createMetadata( fileName: fileName, fileSize: file.lengthSync(), packageAssetId: packageAssetId, urlEncodedToken: urlEncodedToken, contentType: contentType, ); int chunkSize = metadata['chunk_size']; // Uploading chunked binary (3/7) await _uploadingChunkedBinary( file: file, packageAssetId: packageAssetId, urlEncodedToken: urlEncodedToken, contentType: contentType, chunkSize: chunkSize, onPublishProgress: onPublishProgress, ); // Finalising upload (4/7) await _finalisingUpload( packageAssetId: packageAssetId, urlEncodedToken: urlEncodedToken, ); // Commit release (5/7) await _commitRelease( ownerName: publishConfig.ownerName, appName: publishConfig.appName, releasesId: releasesId, ); // Polling for release id (6/7) int releaseDistinctId = await _pollingForReleaseDistinctId( ownerName: publishConfig.ownerName, appName: publishConfig.appName, releasesId: releasesId, ); // Applying destination to release (7/7) await _applyingDestinationToRelease( ownerName: publishConfig.ownerName, appName: publishConfig.appName, releaseDistinctId: releaseDistinctId, distributionGroup: publishConfig.distributionGroup!, ); } catch (error) { rethrow; } return PublishResult( url: 'https://install.appcenter.ms/users/${publishConfig.ownerName}/apps/${publishConfig.appName}/distribution_groups/${publishConfig.distributionGroup}', ); } Future> _createRelease({ required String ownerName, required String appName, }) async { final response = await _dio.post( '/apps/$ownerName/$appName/uploads/releases', ); return Map.from(response.data); } Future> _createMetadata({ required String fileName, required int fileSize, required String packageAssetId, required String urlEncodedToken, required String contentType, }) async { final response = await _dio.post( '$_kUploadDomain/set_metadata/$packageAssetId?file_name=$fileName&file_size=$fileSize&token=$urlEncodedToken&content_type=$contentType', ); return Map.from(response.data); } Future> _uploadingChunkedBinary({ required File file, required String packageAssetId, required String urlEncodedToken, required String contentType, required int chunkSize, PublishProgressCallback? onPublishProgress, }) async { String chunkingPath = '${file.path}_chunking/'; await $('rm', ['-rf', chunkingPath]); await $('mkdir', [chunkingPath]); await $( 'split', ['-b', '$chunkSize', file.path, chunkingPath], ); Directory chunkingDir = Directory(chunkingPath); List entityList = chunkingDir.listSync(); entityList.sort((a, b) => a.path.compareTo(b.path)); for (var i = 0; i < entityList.length; i++) { FileSystemEntity entity = entityList[i]; Uint8List fileData = File(entity.path).readAsBytesSync(); int contentLength = fileData.length; await _dio.post( '$_kUploadDomain/upload_chunk/$packageAssetId?token=$urlEncodedToken&block_number=${i + 1}', data: Stream.fromIterable(fileData.map((e) => [e])), options: Options( headers: { Headers.contentLengthHeader: contentLength, Headers.contentTypeHeader: contentType, }, ), onSendProgress: (sent, total) { if (onPublishProgress != null) { onPublishProgress((i * chunkSize) + sent, file.lengthSync()); } }, ); } await $('rm', ['-rf', chunkingPath]); return Map.from({}); } Future> _finalisingUpload({ required String packageAssetId, required String urlEncodedToken, }) async { final response = await _dio.post( '$_kUploadDomain/finished/$packageAssetId?token=$urlEncodedToken', ); return Map.from(response.data); } Future> _commitRelease({ required String ownerName, required String appName, required String releasesId, }) async { final response = await _dio.patch( '/apps/$ownerName/$appName/uploads/releases/$releasesId', data: { 'upload_status': 'uploadFinished', 'id': releasesId, }, ); return Map.from(response.data); } Future _pollingForReleaseDistinctId({ required String ownerName, required String appName, required String releasesId, }) async { int? releaseDistinctId; int counter = 0; int maxPollAttempts = 15; while (releaseDistinctId == null && counter < maxPollAttempts) { try { final response = await _dio.get( '/apps/$ownerName/$appName/uploads/releases/$releasesId', ); releaseDistinctId = response.data['release_distinct_id']; } catch (error) { // skip } counter = counter + 1; await Future.delayed(const Duration(seconds: 3)); } if (releaseDistinctId == null) { throw PublishError('Failed to find release from appcenter'); } return releaseDistinctId; } Future> _applyingDestinationToRelease({ required String ownerName, required String appName, required int releaseDistinctId, required String distributionGroup, }) async { final response = await _dio.patch( '/apps/$ownerName/$appName/releases/$releaseDistinctId', data: { 'destinations': [ {'name': distributionGroup}, ], }, ); return Map.from(response.data); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/appcenter/publish_appcenter_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; const kEnvAppCenterApiToken = 'APPCENTER_API_TOKEN'; class PublishAppCenterConfig extends PublishConfig { PublishAppCenterConfig({ required this.apiToken, required this.ownerName, required this.appName, this.distributionGroup, }); factory PublishAppCenterConfig.parse( Map? environment, Map? publishArguments, ) { String? apiToken = (environment ?? Platform.environment)[kEnvAppCenterApiToken]; if ((apiToken ?? '').isEmpty) { throw PublishError( 'Missing `$kEnvAppCenterApiToken` environment variable.', ); } String? ownerName = publishArguments?['owner-name']; if ((ownerName ?? '').isEmpty) { throw PublishError('Missing `owner-name` arg'); } String? appName = publishArguments?['app-name']; if ((appName ?? '').isEmpty) { throw PublishError('Missing `app-name` arg'); } PublishAppCenterConfig publishConfig = PublishAppCenterConfig( apiToken: apiToken!, ownerName: ownerName!, appName: appName!, distributionGroup: publishArguments?['distribution-group'] ?? 'Collaborators', ); return publishConfig; } final String apiToken; String ownerName; String appName; String? distributionGroup; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/appstore/app_package_publisher_appstore.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/appstore/publish_appstore_config.dart'; import 'package:shell_executor/shell_executor.dart'; /// AppStore doc [https://help.apple.com/asc/appsaltool/] class AppPackagePublisherAppStore extends AppPackagePublisher { @override String get name => 'appstore'; @override List get supportedPlatforms => ['ios', 'macos']; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; // Get type String type = file.path.endsWith('.ipa') ? 'ios' : 'osx'; // Get config PublishAppStoreConfig publishConfig = PublishAppStoreConfig.parse(environment, publishArguments); // Publish to AppStore ProcessResult processResult = await $( 'xcrun', [ 'altool', '--upload-app', '--file', file.path, '--type', type, // cmd list ...publishConfig.toAppStoreCliDistributeArgs(), ], ); if (processResult.exitCode == 0) { return PublishResult( url: 'https://appstoreconnect.apple.com/apps', ); } else { throw PublishError( '${processResult.exitCode} - Upload of appstore failed', ); } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/appstore/publish_appstore_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; const kEnvAppStoreUsername = 'APPSTORE_USERNAME'; const kEnvAppStorePassword = 'APPSTORE_PASSWORD'; const kEnvAppStoreApiKey = 'APPSTORE_APIKEY'; const kEnvAppStoreApiIssuer = 'APPSTORE_APIISSUER'; class PublishAppStoreConfig extends PublishConfig { PublishAppStoreConfig({ this.username, this.password, this.apiKey, this.apiIssuer, }); factory PublishAppStoreConfig.parse( Map? environment, Map? publishArguments, ) { // Get authorization info String? username = (environment ?? Platform.environment)[kEnvAppStoreUsername]; String? password = (environment ?? Platform.environment)[kEnvAppStorePassword]; String? apiKey = (environment ?? Platform.environment)[kEnvAppStoreApiKey]; String? apiIssuer = (environment ?? Platform.environment)[kEnvAppStoreApiIssuer]; // Check username & password & apiKey & apiIssuer if ('$username$password$apiKey$apiIssuer'.replaceAll('null', '').isEmpty) { throw PublishError( 'Missing `$kEnvAppStoreUsername` & `$kEnvAppStorePassword` | `$kEnvAppStoreApiKey` & `$kEnvAppStoreApiIssuer` environment variable. See:https://help.apple.com/asc/appsaltool/#/apdATD1E53-D1E1A1303-D1E53A1126', ); } // Check username & password if (((username ?? '').isNotEmpty && (password ?? '').isEmpty) || ((username ?? '').isEmpty && (password ?? '').isNotEmpty)) { throw PublishError( 'Missing `$kEnvAppStoreUsername` & `$kEnvAppStorePassword` environment variable. See:https://help.apple.com/asc/appsaltool/#/apdATD1E53-D1E1A1303-D1E53A1126', ); } else { // Check apiKey & apiIssuer if (((apiKey ?? '').isNotEmpty && (apiIssuer ?? '').isEmpty) || ((apiKey ?? '').isEmpty && (apiIssuer ?? '').isNotEmpty)) { throw PublishError( 'Missing `$kEnvAppStoreApiKey` & `$kEnvAppStoreApiIssuer` environment variable. See:https://help.apple.com/asc/appsaltool/#/apdATD1E53-D1E1A1303-D1E53A1126', ); } } return PublishAppStoreConfig( username: username, password: password, apiKey: apiKey, apiIssuer: apiIssuer, ); } final String? username; final String? password; final String? apiKey; final String? apiIssuer; List toAppStoreCliDistributeArgs() { Map cmdData = { '--username': username, '--password': password, '--apiKey': apiKey, '--apiIssuer': apiIssuer, }; // clean null value cmd cmdData.removeWhere((key, value) => value?.isEmpty ?? true); // format cmd List cmdList = []; cmdData.forEach((key, value) { cmdList.addAll([key, value!]); }); return cmdList; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/fir/app_package_publisher_fir.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/fir/publish_fir_config.dart'; import 'package:parse_app_package/parse_app_package.dart'; const kEnvFirApiToken = 'FIR_API_TOKEN'; class AppPackagePublisherFir extends AppPackagePublisher { @override String get name => 'fir'; @override List get supportedPlatforms => ['android', 'ios']; final Dio _dio = Dio( BaseOptions(baseUrl: 'http://api.bq04.com'), ); Future _uploadAppBinary( File file, AppPackage appPackage, { required String key, required String token, required String uploadUrl, PublishProgressCallback? onPublishProgress, }) async { FormData formData = FormData.fromMap({ 'key': key, 'token': token, 'file': await MultipartFile.fromFile(file.path), 'x:name': appPackage.name, 'x:version': appPackage.version, 'x:build': appPackage.buildNumber, }); final response = await _dio.post( uploadUrl, data: formData, onSendProgress: (int sent, int total) { if (onPublishProgress != null) { onPublishProgress(sent, total); } }, ); return response.data['release_id']; } @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; String? apiToken = (environment ?? Platform.environment)[kEnvFirApiToken]; if ((apiToken ?? '').isEmpty) { throw PublishError('Missing `$kEnvFirApiToken` environment variable.'); } PublishFirConfig publishConfig = PublishFirConfig( apiToken: apiToken!, ); try { AppPackage appPackage = await parseAppPackage(file); final response = await _dio.post( '/apps', data: { 'type': appPackage.platform, 'bundle_id': appPackage.identifier, 'api_token': publishConfig.apiToken, }, ); Map data = response.data; Map cert = data['cert']; String releaseId = await _uploadAppBinary( file, appPackage, key: cert['binary']['key'], token: cert['binary']['token'], uploadUrl: cert['binary']['upload_url'], onPublishProgress: onPublishProgress, ); Uri downloadUri = Uri( scheme: data['download_domain_https_ready'] ? 'https' : 'http', host: data['download_domain'], path: '/${data['short']}', queryParameters: {'release_id': releaseId}, ); return PublishResult( url: downloadUri.toString(), ); } on DioException catch (error) { String? message; if (error.response?.data != null) { int? code = error.response?.data['code']; message = error.response?.data['errors']['exception'][0]; message = '$code - $message'; } throw PublishError(message); } catch (error) { rethrow; } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/fir/publish_fir_config.dart ================================================ import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; class PublishFirConfig extends PublishConfig { PublishFirConfig({ required this.apiToken, }); final String apiToken; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/firebase/app_package_publisher_firebase.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/firebase/publish_firebase_config.dart'; import 'package:shell_executor/shell_executor.dart'; /// Firebase doc /// iOS: [https://firebase.google.com/docs/app-distribution/ios/distribute-cli] /// Android: [https://firebase.google.com/docs/app-distribution/android/distribute-cli] class AppPackagePublisherFirebase extends AppPackagePublisher { @override String get name => 'firebase'; @override List get supportedPlatforms => ['android', 'ios']; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; PublishFirebaseConfig publishConfig = PublishFirebaseConfig.parse(environment, publishArguments); // Publish to Firebase ProcessResult processResult = await $( 'firebase', [ 'appdistribution:distribute', file.path, // cmd list ...publishConfig.toFirebaseCliDistributeArgs(), ], ); if (processResult.exitCode == 0) { return PublishResult( url: 'https://console.firebase.google.com/project/_/appdistribution', ); } else { throw PublishError( '${processResult.exitCode} - Upload of firebase failed', ); } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/firebase/publish_firebase_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; const kEnvFirebaseToken = 'FIREBASE_TOKEN'; class PublishFirebaseConfig extends PublishConfig { PublishFirebaseConfig({ required this.app, this.token, this.releaseNotes, this.releaseNotesFile, this.testers, this.testersFile, this.groups, this.groupsFile, }); factory PublishFirebaseConfig.parse( Map? environment, Map? publishArguments, ) { // Get token String? token = (environment ?? Platform.environment)[kEnvFirebaseToken]; if ((token ?? '').isEmpty) { throw PublishError( 'Missing `$kEnvFirebaseToken` environment variable. See:https://firebase.google.com/docs/cli?authuser=0#cli-ci-systems', ); } // Get app String? app = publishArguments?['app']; if ((app ?? '').isEmpty) { throw PublishError( 'Missing app args. See:https://console.firebase.google.com/project/_/settings/general/?authuser=0', ); } return PublishFirebaseConfig( app: app!, token: token!, releaseNotes: publishArguments?['release-notes'], releaseNotesFile: publishArguments?['release-notes-file'], testers: publishArguments?['testers'], testersFile: publishArguments?['testers-file'], groups: publishArguments?['groups'], groupsFile: publishArguments?['groups-file'], ); } final String app; final String? token; final String? releaseNotes; final String? releaseNotesFile; final String? testers; final String? testersFile; final String? groups; final String? groupsFile; List toFirebaseCliDistributeArgs() { Map cmdData = { '--app': app, '--token': token, '--release-notes': releaseNotes, '--release-notes-file': releaseNotesFile, '--testers': testers, '--testers-file': testersFile, '--groups': groups, '--groups-file': groupsFile, }; // clean null value cmd cmdData.removeWhere((key, value) => value?.isEmpty ?? true); // format cmd List cmdList = []; cmdData.forEach((key, value) { cmdList.addAll([key, value!]); }); return cmdList; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/firebase_hosting/app_package_publisher_firebase_hosting.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/firebase_hosting/publish_firebase_hosting_config.dart'; import 'package:shell_executor/shell_executor.dart'; class AppPackagePublisherFirebaseHosting extends AppPackagePublisher { @override String get name => 'firebase-hosting'; @override List get supportedPlatforms => ['web']; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { Directory directory = fileSystemEntity as Directory; PublishFirebaseHostingConfig publishConfig = PublishFirebaseHostingConfig.parse( environment, publishArguments, ); try { File firebaseRcFile = File('${directory.path}/.firebaserc'); firebaseRcFile.createSync(recursive: true); firebaseRcFile.writeAsStringSync( json.encode({ 'projects': {'default': publishConfig.projectId}, }), ); File firebaseJsonFile = File('${directory.path}/firebase.json'); firebaseJsonFile.createSync(recursive: true); firebaseJsonFile.writeAsStringSync( json.encode({ 'hosting': { 'public': '.', 'ignore': ['firebase.json'], }, }), ); ProcessResult r = await $( 'firebase', ['deploy'], workingDirectory: directory.path, ); String log = r.stdout.toString(); RegExpMatch? match = RegExp(r'(?<=Hosting URL: )\bhttps?:\/\/\S+\b').firstMatch(log); return PublishResult( url: match != null ? match.group(0)! : '', ); } catch (error) { rethrow; } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/firebase_hosting/publish_firebase_hosting_config.dart ================================================ import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; class PublishFirebaseHostingConfig extends PublishConfig { PublishFirebaseHostingConfig({ required this.projectId, }); factory PublishFirebaseHostingConfig.parse( Map? environment, Map? publishArguments, ) { String? projectId = publishArguments?['project-id']; if ((projectId ?? '').isEmpty) { throw PublishError('Missing `project-id` config.'); } PublishFirebaseHostingConfig publishConfig = PublishFirebaseHostingConfig( projectId: projectId!, ); return publishConfig; } String projectId; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/github/app_package_publisher_github.dart ================================================ import 'dart:io'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/github/publish_github_config.dart'; class AppPackagePublisherGithub extends AppPackagePublisher { final Dio _dio = Dio(); @override String get name => 'github'; @override List get supportedPlatforms => [ 'android', 'ios', 'linux', 'macos', 'windows', 'web', ]; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; PublishGithubConfig publishConfig = PublishGithubConfig.parse( environment, publishArguments, ); // Set auth _dio.options = BaseOptions( headers: { 'Authorization': 'token ${publishConfig.token}', }, ); // Get uploadUrl String? uploadUrl; if (publishConfig.releaseTitle?.isEmpty ?? true) { uploadUrl = await _getUploadurlByLatestRelease(publishConfig); } else { uploadUrl = await _getUploadurlByReleaseName(publishConfig); if (uploadUrl?.isEmpty ?? true) { uploadUrl = await _createRelease(publishConfig); } } if (uploadUrl?.isEmpty ?? true) { throw PublishError('Upload url isEmpty'); } // Upload file String browserDownloadUrl = await _uploadReleaseAsset(file, uploadUrl!, onPublishProgress); return PublishResult( url: browserDownloadUrl, ); } /// Get uploadUrl by releaseName Future _getUploadurlByReleaseName( PublishGithubConfig publishConfig, ) async { Response resp = await _dio.get( 'https://api.github.com/repos/${publishConfig.repoOwner}/${publishConfig.repoName}/releases', ); List relist = (resp.data as List?) ?? []; var release = relist.firstWhere( (item) => item['name'] == publishConfig.releaseTitle, orElse: () => {}, ); return release?['upload_url']; } /// Create release Future _createRelease(PublishGithubConfig publishConfig) async { Response resp = await _dio.post( 'https://api.github.com/repos/${publishConfig.repoOwner}/${publishConfig.repoName}/releases', data: { 'tag_name': publishConfig.releaseTitle, 'name': publishConfig.releaseTitle, 'draft': true, }, ); return resp.data?['upload_url']; } /// Get uploadUrl by latest release Future _getUploadurlByLatestRelease( PublishGithubConfig publishConfig, ) async { Response resp = await _dio.get( 'https://api.github.com/repos/${publishConfig.repoOwner}/${publishConfig.repoName}/releases/latest', ); return resp.data?['upload_url']; } /// Upload Release Asset Future _uploadReleaseAsset( File file, String uploadUrl, PublishProgressCallback? onPublishProgress, ) async { // Fromat uploadUrl uploadUrl = uploadUrl.split('{').first; String fileName = file.path.split('/').last; Uint8List fileData = await file.readAsBytes(); String url = '$uploadUrl?name=${Uri.encodeComponent(fileName)}'; // dio upload _dio.options.contentType = 'application/octet-stream'; _dio.options.headers .putIfAbsent(Headers.contentLengthHeader, () => fileData.length); String? browserDownloadUrl; try { Response resp = await _dio.post( url, data: Stream.fromIterable(fileData.map((e) => [e])), onSendProgress: (int sent, int total) { if (onPublishProgress != null) { onPublishProgress(sent, total); } }, ); browserDownloadUrl = resp.data?['browser_download_url']; } catch (e) { throw PublishError(e.toString()); } // Check release asset if (browserDownloadUrl?.isEmpty ?? true) { throw PublishError('Release asset exist [$fileName]'); } return browserDownloadUrl!; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/github/publish_github_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; const kEnvGithubToken = 'GITHUB_TOKEN'; class PublishGithubConfig extends PublishConfig { PublishGithubConfig({ required this.token, required this.repoOwner, required this.repoName, this.releaseTitle, }); factory PublishGithubConfig.parse( Map? environment, Map? publishArguments, ) { String? token = (environment ?? Platform.environment)[kEnvGithubToken]; if ((token ?? '').isEmpty) { throw PublishError('Missing `$kEnvGithubToken` environment variable.'); } String? owner = publishArguments?['repo-owner']; if ((owner ?? '').isEmpty) { throw PublishError(' is null'); } String? name = publishArguments?['repo-name']; if ((name ?? '').isEmpty) { throw PublishError(' is null'); } PublishGithubConfig publishConfig = PublishGithubConfig( token: token!, repoOwner: owner!, repoName: name!, releaseTitle: publishArguments?['release-title'], ); String appVersion = publishConfig.pubspec.version.toString().split('+').first; String appBuildNumber = publishConfig.pubspec.version.toString().split('+').last; if ((publishConfig.releaseTitle ?? '').trim().isEmpty) { publishConfig.releaseTitle = 'v$appVersion'; } else { publishConfig.releaseTitle = publishConfig.releaseTitle ?.replaceAll('{appVersion}', appVersion) .replaceAll('{appBuildNumber}', appBuildNumber); } return publishConfig; } // Personal access tokens final String token; // Repository Owner String repoOwner; // Repository Name String repoName; // Release title String? releaseTitle; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/pgyer/app_package_publisher_pgyer.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; const kEnvPgyerApiKey = 'PGYER_API_KEY'; /// pgyer doc [https://www.pgyer.com/doc/view/api#uploadApp] class AppPackagePublisherPgyer extends AppPackagePublisher { @override String get name => 'pgyer'; // dio 网络请求实例 final Dio _dio = Dio(); // 轮询尝试次数 int tryCount = 0; // 最大尝试轮询次数 final maxTryCount = 10; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; String? apiKey = (environment ?? Platform.environment)[kEnvPgyerApiKey]; if ((apiKey ?? '').isEmpty) { throw PublishError('Missing `$kEnvPgyerApiKey` environment variable.'); } var tokenInfo = await getCOSToken(apiKey!, file.path); String uploadKey = await uploadApp(tokenInfo, file, onPublishProgress); if (uploadKey.isEmpty) { throw PublishError('UploadApp error'); } // 重试次数设置为 0 tryCount = 0; var buildResult = await getBuildInfo(apiKey, uploadKey); String buildKey = buildResult.data!['data']['buildKey']; return PublishResult( url: 'http://www.pgyer.com/$buildKey', ); } /// 获取上传 Token 信息 /// [apiKey] apiKey /// [filePath] 文件路径 Future getCOSToken(String apiKey, String filePath) async { FormData formData = FormData.fromMap({ '_api_key': apiKey, 'buildType': filePath.split('.').last, }); try { Response response = await _dio.post( 'https://www.pgyer.com/apiv2/app/getCOSToken', data: formData, ); if (response.data['code'] != 0) { throw PublishError('getCOSToken error: ${response.data}'); } return response; } catch (e) { throw PublishError(e.toString()); } } /// 上传应用 /// [tokenInfo] token信息 /// [file] 文件 /// [onPublishProgress] 进度回调 Future uploadApp( Response tokenInfo, File file, PublishProgressCallback? onPublishProgress, ) async { var tokenData = tokenInfo.data['data']; String endpoint = tokenData['endpoint']; String key = tokenData['key']; var params = tokenData['params']; FormData formData = FormData.fromMap({ 'key': key, 'signature': params['signature'], 'x-cos-security-token': params['x-cos-security-token'], 'x-cos-meta-file-name': file.path.split('/').last, 'file': await MultipartFile.fromFile(file.path), }); try { Response response = await _dio.post( endpoint, data: formData, onSendProgress: (int sent, int total) { if (onPublishProgress != null) { onPublishProgress(sent, total); } }, ); if (response.statusCode == 204) { // 上传成功,准备轮询结果 return key; } } catch (e) { throw PublishError(e.toString()); } return ''; } /// 获取应用发布构建信息 /// [apiKey] apiKey /// [uploadKey] uploadKey Future getBuildInfo(String apiKey, String uploadKey) async { if (tryCount > maxTryCount) { throw PublishError('getBuildInfo error :Too many retries'); } await Future.delayed(const Duration(seconds: 3)); try { Response response = await _dio.get( 'https://www.pgyer.com/apiv2/app/buildInfo', queryParameters: { '_api_key': apiKey, 'buildKey': uploadKey, }, ); int code = response.data['code']; if (code == 1247) { tryCount++; print('应用发布信息获取中,请稍等 $tryCount'); return await getBuildInfo(apiKey, uploadKey); } else if (code != 0) { throw PublishError('getBuildInfo error: ${response.data}'); } return response; } catch (e) { throw PublishError(e.toString()); } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/pgyer/publish_pgyer_config.dart ================================================ import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; class PublishPgyerConfig extends PublishConfig { PublishPgyerConfig({ required this.apiKey, }); final String apiKey; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/playstore/app_package_publisher_playstore.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/playstore/publish_playstore_config.dart'; import 'package:googleapis/androidpublisher/v3.dart'; import 'package:googleapis_auth/auth_io.dart'; class AppPackagePublisherPlayStore extends AppPackagePublisher { @override String get name => 'playstore'; @override List get supportedPlatforms => ['android']; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; PublishPlayStoreConfig publishConfig = PublishPlayStoreConfig.parse( environment, publishArguments, ); String jsonString = File(publishConfig.credentialsFile).readAsStringSync(); ServiceAccountCredentials serviceAccountCredentials = ServiceAccountCredentials.fromJson(json.decode(jsonString)); final client = await clientViaServiceAccount( serviceAccountCredentials, [ AndroidPublisherApi.androidpublisherScope, ], ); final AndroidPublisherApi publisherApi = AndroidPublisherApi(client); AppEdit appEdit = await publisherApi.edits.insert( AppEdit(), publishConfig.packageName, ); Media uploadMedia = Media(file.openRead(), file.lengthSync()); await publisherApi.edits.bundles.upload( publishConfig.packageName, appEdit.id!, uploadMedia: uploadMedia, ); await publisherApi.edits.commit( publishConfig.packageName, appEdit.id!, ); return PublishResult( url: '', ); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/playstore/publish_playstore_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; const kEnvPlayStoreCredentialsFile = 'PLAYSTORE_CREDENTIALS'; class PublishPlayStoreConfig extends PublishConfig { PublishPlayStoreConfig({ required this.credentialsFile, required this.packageName, }); factory PublishPlayStoreConfig.parse( Map? environment, Map? publishArguments, ) { String? credentialsFile = (environment ?? Platform.environment)[kEnvPlayStoreCredentialsFile]; if ((credentialsFile ?? '').isEmpty) { throw PublishError( 'Missing `$kEnvPlayStoreCredentialsFile` environment variable.', ); } PublishPlayStoreConfig publishConfig = PublishPlayStoreConfig( credentialsFile: credentialsFile!, packageName: publishArguments?['package-name'], ); return publishConfig; } final String credentialsFile; final String packageName; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/publishers.dart ================================================ export 'appcenter/app_package_publisher_appcenter.dart'; export 'appstore/app_package_publisher_appstore.dart'; export 'fir/app_package_publisher_fir.dart'; export 'firebase/app_package_publisher_firebase.dart'; export 'firebase_hosting/app_package_publisher_firebase_hosting.dart'; export 'github/app_package_publisher_github.dart'; export 'pgyer/app_package_publisher_pgyer.dart'; export 'playstore/app_package_publisher_playstore.dart'; export 'qiniu/app_package_publisher_qiniu.dart'; export 'vercel/app_package_publisher_vercel.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/qiniu/app_package_publisher_qiniu.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/qiniu/publish_qiniu_config.dart'; // import 'package:parse_app_package/parse_app_package.dart'; import 'package:qiniu_sdk_base/qiniu_sdk_base.dart'; class AppPackagePublisherQiniu extends AppPackagePublisher { @override String get name => 'qiniu'; @override List get supportedPlatforms => [ 'android', 'ios', 'linux', 'macos', 'windows', ]; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { File file = fileSystemEntity as File; PublishQiniuConfig publishConfig = PublishQiniuConfig.parse( environment, publishArguments, ); try { Auth auth = Auth( accessKey: publishConfig.accessKey, secretKey: publishConfig.secretKey, ); String saveKey = '${publishConfig.savekeyPrefix}${file.path.split('/').last}'; String uploadToken = auth.generateUploadToken( putPolicy: PutPolicy( scope: publishConfig.bucket, deadline: (DateTime.now().millisecondsSinceEpoch ~/ 1000) + 3600, saveKey: saveKey, ), ); Storage storage = Storage(); PutController putController = PutController(); int sent = 0; int total = file.lengthSync(); putController.addSendProgressListener((double percent) { if (onPublishProgress != null) { sent = (total * percent).toInt(); onPublishProgress(sent, total); } }); if (onPublishProgress != null) { onPublishProgress(sent, total); } PutResponse putResponse = await storage.putFile( file, uploadToken, options: PutOptions( controller: putController, ), ); return PublishResult( url: '${publishConfig.bucketDomain ?? ''}/${putResponse.key}', ); } on StorageError catch (error) { throw PublishError('${error.code} - ${error.message}'); } catch (error) { rethrow; } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/qiniu/publish_qiniu_config.dart ================================================ import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; const kEnvQiniuAccessKey = 'QINIU_ACCESS_KEY'; const kEnvQiniuSecretKey = 'QINIU_SECRET_KEY'; class PublishQiniuConfig extends PublishConfig { PublishQiniuConfig({ required this.accessKey, required this.secretKey, required this.bucket, this.bucketDomain, this.savekeyPrefix, }); factory PublishQiniuConfig.parse( Map? environment, Map? publishArguments, ) { String? accessKey = (environment ?? Platform.environment)[kEnvQiniuAccessKey]; String? secretKey = (environment ?? Platform.environment)[kEnvQiniuSecretKey]; if ((accessKey ?? '').isEmpty) { throw PublishError('Missing `$kEnvQiniuAccessKey` environment variable.'); } if ((secretKey ?? '').isEmpty) { throw PublishError('Missing `$kEnvQiniuSecretKey` environment variable.'); } return PublishQiniuConfig( accessKey: accessKey!, secretKey: secretKey!, bucket: publishArguments?['bucket'], bucketDomain: publishArguments?['bucket-domain'], savekeyPrefix: publishArguments?['savekey-prefix'] ?? '', ); } final String accessKey; final String secretKey; final String bucket; String? bucketDomain; String? savekeyPrefix; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/vercel/app_package_publisher_vercel.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; import 'package:flutter_app_publisher/src/publishers/vercel/publish_vercel_config.dart'; import 'package:shell_executor/shell_executor.dart'; class AppPackagePublisherVercel extends AppPackagePublisher { @override String get name => 'vercel'; @override List get supportedPlatforms => ['web']; @override Future publish( FileSystemEntity fileSystemEntity, { Map? environment, Map? publishArguments, PublishProgressCallback? onPublishProgress, }) async { Directory directory = fileSystemEntity as Directory; PublishVercelConfig publishConfig = PublishVercelConfig.parse( environment, publishArguments, ); try { File file = File('${directory.path}/.vercel/project.json'); file.createSync(recursive: true); file.writeAsStringSync( json.encode({ 'orgId': publishConfig.orgId, 'projectId': publishConfig.projectId, }), ); ProcessResult r = await $( 'vercel', ['--prod'], workingDirectory: directory.path, ); String log = r.stderr.toString(); RegExpMatch? match = RegExp(r'(?<=Production: )\bhttps?:\/\/\S+\b').firstMatch(log); return PublishResult( url: match != null ? match.group(0)! : '', ); } catch (error) { rethrow; } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/lib/src/publishers/vercel/publish_vercel_config.dart ================================================ import 'package:flutter_app_publisher/src/api/app_package_publisher.dart'; class PublishVercelConfig extends PublishConfig { PublishVercelConfig({ required this.orgId, required this.projectId, }); factory PublishVercelConfig.parse( Map? environment, Map? publishArguments, ) { String? orgId = publishArguments?['org-id']; if ((orgId ?? '').isEmpty) { throw PublishError('Missing `org-id` config.'); } String? projectId = publishArguments?['project-id']; if ((projectId ?? '').isEmpty) { throw PublishError('Missing `project-id` config.'); } PublishVercelConfig publishConfig = PublishVercelConfig( orgId: orgId!, projectId: projectId!, ); return publishConfig; } String orgId; String projectId; } ================================================ FILE: plugins/flutter_distributor/packages/flutter_app_publisher/pubspec.yaml ================================================ name: flutter_app_publisher description: Flutter app publisher version: 0.4.2 homepage: https://distributor.leanflutter.dev repository: https://github.com/leanflutter/flutter_distributor/tree/main/packages/flutter_app_publisher environment: sdk: ">=2.16.0 <4.0.0" dependencies: dio: ^5.3.4 googleapis: ^9.1.0 googleapis_auth: ^1.3.1 parse_app_package: path: ../parse_app_package pubspec_parse: ^1.1.0 qiniu_sdk_base: ^0.5.0 shell_executor: ^0.1.5 dev_dependencies: dependency_validator: ^3.0.0 ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages .flutter-plugins .flutter-plugins-dependencies ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/analysis_options.yaml ================================================ include: package:mostly_reasonable_lints/analysis_options.yaml linter: rules: avoid_print: false ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/bin/command_package.dart ================================================ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:flutter_distributor/flutter_distributor.dart'; import 'package:flutter_distributor/src/extensions/extensions.dart'; /// Package an application bundle for a specific platform and target /// /// This command wrapper defines, parses and transforms all passed arguments, /// so that they may be passed to `flutter_distributor`. The distributor will /// then build an application bundle using `flutter_app_packager`. class CommandPackage extends Command { CommandPackage(this.distributor) { argParser.addOption( 'platform', valueHelp: [ 'android', 'ios', 'linux', 'macos', 'windows', 'web', ].join(','), help: 'The platform to package the application for', ); argParser.addOption( 'targets', aliases: ['target'], valueHelp: [ 'apk', 'aab', 'appimage', 'deb', 'dmg', 'exe', 'ipa', 'msix', 'pkg', 'rpm', 'zip', ].join(','), help: 'Comma separated list of bundle types to build.', ); argParser.addOption('channel', valueHelp: ''); argParser.addOption('artifact-name', valueHelp: ''); argParser.addOption( 'description', valueHelp: '', ); argParser.addFlag( 'skip-clean', help: 'Whether or not to skip \'flutter clean\' before packaging.', ); argParser.addOption( 'flutter-build-args', valueHelp: 'verbose,obfuscate', help: 'Arguments to pass directly to flutter build', ); argParser.addOption( 'build-target', valueHelp: 'path', help: 'The --target argument passed to \'flutter build\'', ); argParser.addOption( 'build-flavor', valueHelp: '', help: 'The --flavor argument passed to \'flutter build\'', ); argParser.addOption( 'build-target-platform', valueHelp: '', help: 'The --target-platform argument passed to \'flutter build\'', ); argParser.addOption( 'build-export-options-plist', valueHelp: '', help: 'The --export-options-plist argument passed \'flutter build\'', ); argParser.addMultiOption( 'build-dart-define', valueHelp: 'foo=bar', help: [ 'The --dart-define argument(s) passed to \'flutter build\'', 'You may add multiple \'--build-dart-define key=value\' pairs', ].join('\n'), ); } final FlutterDistributor distributor; @override String get name => 'package'; @override String get description => [ 'Package the current Flutter application', '', 'Options named --build-* are passed to \'flutter build\' as is', 'Please consult the \'flutter build\' CLI help for more informations.', ].join('\n'); @override Future run() async { final String? platform = argResults?['platform']; final List targets = '${argResults?['targets'] ?? ''}' .split(',') .where((e) => e.isNotEmpty) .toList(); final String? channel = argResults?['channel']; final String? artifactName = argResults?['artifact-name']; final String? flutterBuildArgs = argResults?['flutter-build-args']; final bool isSkipClean = argResults?.wasParsed('skip-clean') ?? false; final Map buildArguments = _generateBuildArgs(flutterBuildArgs); // At least `platform` and one `targets` is required for flutter build if (platform == null) { print('\nThe \'platform\' options is mandatory!'.red(bold: true)); exit(1); } if (targets.isEmpty) { print('\nAt least one \'target\' must be specified!'.red(bold: true)); exit(1); } return distributor.package( platform, targets, channel: channel, artifactName: artifactName, cleanBeforeBuild: !isSkipClean, buildArguments: buildArguments, description: argResults!['description'], ); } Map _generateBuildArgs(String? flutterBuildArgs) { Map buildArguments = {}; if (argResults?.options == null) return buildArguments; for (var option in argResults!.options) { if (!option.startsWith('build-')) continue; dynamic value = argResults?[option]; if (value is List) { // ignore: prefer_for_elements_to_map_fromiterable value = Map.fromIterable( value, key: (e) => e.split('=')[0], value: (e) => e.split('=')[1], ); } buildArguments.putIfAbsent( option.replaceAll('build-', ''), () => value, ); } for (var arg in flutterBuildArgs?.split(',') ?? []) { if (arg.split('=').length == 2) { buildArguments.putIfAbsent( arg.split('=').first, () => arg.split('=').last, ); } else if (arg.split('=').length == 1) { buildArguments.putIfAbsent( arg.split('=')[0], () => true, ); } else { buildArguments.putIfAbsent(arg, () => true); } } return buildArguments; } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/bin/command_publish.dart ================================================ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:flutter_distributor/flutter_distributor.dart'; import 'package:flutter_distributor/src/extensions/extensions.dart'; /// Publish an application to a third party provider /// /// This command wrapper defines, parses and transforms all passed arguments, /// so that they may be passed to `flutter_distributor`. The distributor will /// then publish an application bundle using `flutter_app_publisher`. class CommandPublish extends Command { CommandPublish(this.distributor) { argParser.addOption( 'path', valueHelp: '', help: 'The path to the application bundle to publish.', ); argParser.addOption( 'targets', aliases: ['target'], valueHelp: [ 'appcenter', 'appstore', 'fir', 'firebase', 'github', 'playstore', 'pgyer', 'qiniu', 'vercel', ].join(','), help: 'The target provider(s) to publish to.', ); // AppCenter argParser.addSeparator('appcenter'); argParser.addOption( 'appcenter-owner-name', valueHelp: '', help: 'The owner name for appcenter.', ); argParser.addOption( 'appcenter-app-name', valueHelp: '', help: 'The app name for appcenter.', ); argParser.addOption( 'appcenter-distribution-group', valueHelp: '', help: 'The distribution group for appcenter.', ); // Firebase argParser.addSeparator('firebase'); argParser.addOption( 'firebase-app', valueHelp: '', help: [ 'The unique ID of the application on Firebase.', 'This is NOT your bundle identifier', ].join('\n'), ); argParser.addOption( 'firebase-release-notes', valueHelp: '', help: 'The release notes for the published application.', ); argParser.addOption( 'firebase-release-notes-file', valueHelp: '', help: [ 'The path of a file containing the release notes', 'This is a more extensive alternative to firebase-release-notes', ].join('\n'), ); argParser.addOption( 'firebase-testers', valueHelp: '', help: 'The testers that will be notified about the published application.', ); argParser.addOption( 'firebase-testers-file', valueHelp: '', help: [ 'The path of a file containing testers that will be notified', 'This is a more extensive alternative to firebase-testers', ].join('\n'), ); argParser.addOption( 'firebase-groups', valueHelp: '', help: 'The groups that will be notified about the published application.', ); argParser.addOption( 'firebase-groups-file', valueHelp: '', help: [ 'The path of a file containing groups that will be notified', 'This is a more extensive alternative to firebase-groups', ].join('\n'), ); // Firebase Hosting argParser.addSeparator('firebase-hosting'); argParser.addOption('firebase-hosting-project-id', valueHelp: ''); // Github argParser.addSeparator('github'); argParser.addOption( 'github-repo-owner', valueHelp: '', help: 'The name of the target GitHub repository wner (namespace)', ); argParser.addOption( 'github-repo-name', valueHelp: '', help: 'The name of the target GitHub repository', ); argParser.addOption( 'github-release-title', valueHelp: '', help: 'The title of the new release on GitHub', ); // PlayStore argParser.addSeparator('playstore'); argParser.addOption('playstore-package-name', valueHelp: ''); // Qiniu argParser.addSeparator('qiniu'); argParser.addOption('qiniu-bucket', valueHelp: ''); argParser.addOption('qiniu-bucket-domain', valueHelp: ''); argParser.addOption('qiniu-savekey-prefix', valueHelp: ''); // Vercel argParser.addSeparator('vercel'); argParser.addOption('vercel-org-id', valueHelp: ''); argParser.addOption('vercel-project-id', valueHelp: ''); } final FlutterDistributor distributor; @override String get name => 'publish'; @override String get description => 'Publish the current Flutter application'; @override Future run() async { String? path = argResults?['path']; List targets = '${argResults?['targets'] ?? ''}' .split(',') .where((t) => t.isNotEmpty) .toList(); // At least `path` and one `targets` is required for flutter build if (path == null) { print('\nThe \'path\' options is mandatory!'.red(bold: true)); exit(1); } print(targets); if (targets.isEmpty) { print('\nAt least one \'target\' must be specified!'.red(bold: true)); exit(1); } // Required parameters for firebase if (targets.contains('firebase')) { if (argResults?['firebase-app'] == null) { print('\nFirebase app identifier is required for target \'firebase\''); exit(1); } } Map publishArguments = { 'appcenter-owner-name': argResults?['appcenter-owner-name'], 'appcenter-app-name': argResults?['appcenter-app-name'], 'appcenter-distribution-group': argResults?['appcenter-distribution-group'], 'firebase-app': argResults?['firebase-app'], 'firebase-release-notes': argResults?['firebase-release-notes'], 'firebase-release-notes-file': argResults?['firebase-release-notes-file'], 'firebase-testers': argResults?['firebase-testers'], 'firebase-testers-file': argResults?['firebase-testers-file'], 'firebase-groups': argResults?['firebase-groups'], 'firebase-groups-file': argResults?['firebase-groups-file'], 'firebase-hosting-project-id': argResults?['firebase-hosting-project-id'], 'github-repo-owner': argResults?['github-repo-owner'], 'github-repo-name': argResults?['github-repo-name'], 'github-release-title': argResults?['github-release-title'], 'playstore-package-name': argResults?['playstore-package-name'], 'qiniu-bucket': argResults?['qiniu-bucket'], 'qiniu-bucket-domain': argResults?['qiniu-bucket-domain'], 'qiniu-savekey-prefix': argResults?['qiniu-savekey-prefix'], 'vercel-org-id': argResults?['vercel-org-id'], 'vercel-project-id': argResults?['vercel-project-id'], }..removeWhere((key, value) => value == null); final fileSystemEntity = await FileSystemEntity.type(path) == FileSystemEntityType.directory ? Directory(path) : File(path); return distributor.publish( fileSystemEntity, targets, publishArguments: publishArguments, ); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/bin/command_release.dart ================================================ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:flutter_distributor/flutter_distributor.dart'; import 'package:flutter_distributor/src/extensions/extensions.dart'; /// Release (package and publish) an application based on the config /// /// This command wrapper defines, parses and transforms all passed arguments, /// so that they may be passed to `flutter_distributor`. The distributor will /// then use the `distribute_options.yaml` file in the Flutter project root /// to run a release with one or multiple release jobs. /// /// Each release job will package and optionally also publish the application /// based on the configuration on `distribute_options.yaml`. class CommandRelease extends Command { CommandRelease(this.distributor) { argParser.addOption( 'name', valueHelp: '', help: 'The name of the release to run.', ); argParser.addOption( 'jobs', valueHelp: '', help: 'Comma-separated list of jobs to run for the specified release.', ); argParser.addOption( 'skip-jobs', valueHelp: '', help: 'Comma-separated list of jobs to skip for the specified release.', ); argParser.addFlag( 'skip-clean', help: 'Whether or not to skip \'flutter clean\' before packaging.', ); } final FlutterDistributor distributor; @override String get name => 'release'; @override String get description => 'Release the current Flutter application'; @override Future run() async { String? name = argResults?['name'] ?? ''; List jobNameList = (argResults?['jobs'] ?? '') .split(',') .where((String e) => e.isNotEmpty) .toList(); List skipJobNameList = (argResults?['skip-jobs'] ?? '') .split(',') .where((String e) => e.isNotEmpty) .toList(); bool isSkipClean = argResults?.wasParsed('skip-clean') ?? false; // At least `name` must be passed to select a release if (name == null) { print('\nThe \'name\' options is mandatory!'.red(bold: true)); exit(1); } return distributor.release( name, jobNameList: jobNameList, skipJobNameList: skipJobNameList, cleanBeforeBuild: !isSkipClean, ); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/bin/command_upgrade.dart ================================================ import 'package:args/command_runner.dart'; import 'package:flutter_distributor/flutter_distributor.dart'; /// Upgrade flutter_distributor to the latest version class CommandUpgrade extends Command { CommandUpgrade(this.distributor); final FlutterDistributor distributor; @override String get name => 'upgrade'; @override String get description => 'Upgrade your copy of Flutter Distributor.'; @override Future run() async { await distributor.upgrade(); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/bin/main.dart ================================================ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:flutter_distributor/flutter_distributor.dart'; import 'package:flutter_distributor/src/utils/logger.dart'; import 'command_package.dart'; import 'command_publish.dart'; import 'command_release.dart'; import 'command_upgrade.dart'; Future main(List args) async { FlutterDistributor distributor = FlutterDistributor(); final runner = CommandRunner('flutter_distributor', ''); runner.argParser ..addFlag( 'version', help: 'Reports the version of this tool.', negatable: false, ) ..addFlag( 'version-check', help: 'Check for updates when this command runs.', defaultsTo: true, negatable: true, ); runner.addCommand(CommandPackage(distributor)); runner.addCommand(CommandPublish(distributor)); runner.addCommand(CommandRelease(distributor)); runner.addCommand(CommandUpgrade(distributor)); ArgResults argResults = runner.parse(args); if (argResults.wasParsed('version')) { String? currentVersion = await distributor.getCurrentVersion(); if (currentVersion != null) { logger.info(currentVersion); return; } } // if (argResults['version-check']) { // // Check version of flutter_distributor on every run // await distributor.checkVersion(); // } return runner.runCommand(argResults); } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/flutter_distributor.dart ================================================ library flutter_distributor; export 'src/distribute_options.dart'; export 'src/flutter_distributor.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/distribute_options.dart ================================================ import 'dart:io'; import 'package:flutter_distributor/src/release.dart'; class DistributeOptions { DistributeOptions({ required this.output, this.variables, this.artifactName, required this.releases, }); factory DistributeOptions.fromJson(Map json) { Map variables = {}; if (json.containsKey('variables') && json['variables'] != null) { variables = Map.from(json['variables']); // 兼容老版本 } else if (json.containsKey('env') && json['env'] != null) { variables = Map.from(json['env']); } List releases = ((json['releases'] ?? []) as List) .map((item) => Release.fromJson(item)) .toList(); return DistributeOptions( output: json['output'], variables: variables, artifactName: json['artifact_name'], releases: releases, ); } final String output; final Map? variables; final String? artifactName; final List releases; Directory get outputDirectory => Directory(output); Map toJson() { return { 'output': output, 'variables': variables, 'artifact_name': artifactName, 'releases': releases.map((e) => e.toJson()).toList(), }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/extensions/extensions.dart ================================================ export 'string.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/extensions/string.dart ================================================ import 'package:ansicolor/ansicolor.dart'; AnsiPen _ansiPen = AnsiPen(); const ansiResetForeground = '${ansiEscape}39m'; extension StringExt on String { String _applyColor(int color, {bool bg = false, bool bold = false}) { String appliedColor = (_ansiPen..xterm(color, bg: bg))(this); if (bold) { return '${ansiEscape}1m$appliedColor$ansiDefault'; } return appliedColor; } String black({bool bg = false, bool bold = false}) { return _applyColor(0, bg: bg, bold: bold); } String red({bool bg = false, bool bold = false}) { return _applyColor(1, bg: bg, bold: bold); } String green({bool bg = false, bool bold = false}) { return _applyColor(2, bg: bg, bold: bold); } String yellow({bool bg = false, bool bold = false}) { return _applyColor(3, bg: bg, bold: bold); } String blue({bool bg = false, bool bold = false}) { return _applyColor(4, bg: bg, bold: bold); } String magenta({bool bg = false, bool bold = false}) { return _applyColor(5, bg: bg, bold: bold); } String cyan({bool bg = false, bool bold = false}) { return _applyColor(6, bg: bg, bold: bold); } String white({bool bg = false, bool bold = false}) { return _applyColor(7, bg: bg, bold: bold); } String brightBlack({bool bg = false, bool bold = false}) { return _applyColor(8, bg: bg, bold: bold); } String brightRed({bool bg = false, bool bold = false}) { return _applyColor(9, bg: bg, bold: bold); } String brightGreen({bool bg = false, bool bold = false}) { return _applyColor(10, bg: bg, bold: bold); } String brightYellow({bool bg = false, bool bold = false}) { return _applyColor(11, bg: bg, bold: bold); } String brightBlue({bool bg = false, bool bold = false}) { return _applyColor(12, bg: bg, bold: bold); } String brightMagenta({bool bg = false, bool bold = false}) { return _applyColor(13, bg: bg, bold: bold); } String brightCyan({bool bg = false, bool bold = false}) { return _applyColor(14, bg: bg, bold: bold); } String brightWhite({bool bg = false, bool bold = false}) { return _applyColor(15, bg: bg, bold: bold); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/flutter_distributor.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_builder/flutter_app_builder.dart'; import 'package:flutter_app_packager/flutter_app_packager.dart'; import 'package:flutter_app_publisher/flutter_app_publisher.dart'; import 'package:flutter_distributor/src/distribute_options.dart'; import 'package:flutter_distributor/src/extensions/extensions.dart'; import 'package:flutter_distributor/src/release.dart'; import 'package:flutter_distributor/src/release_job.dart'; import 'package:flutter_distributor/src/utils/utils.dart'; import 'package:path/path.dart' as p; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:shell_executor/shell_executor.dart'; import 'package:shell_uikit/shell_uikit.dart'; import 'package:yaml/yaml.dart'; class FlutterDistributor { FlutterDistributor() { ShellExecutor.global = DefaultShellExecutor(); } final FlutterAppBuilder _builder = FlutterAppBuilder(); final FlutterAppPackager _packager = FlutterAppPackager(); final FlutterAppPublisher _publisher = FlutterAppPublisher(); Pubspec? _pubspec; Pubspec get pubspec { if (_pubspec == null) { final yamlString = File('pubspec.yaml').readAsStringSync(); _pubspec = Pubspec.parse(yamlString); } return _pubspec!; } final Map _globalVariables = {}; Map get globalVariables { if (_globalVariables.keys.isEmpty) { for (String key in Platform.environment.keys) { String? value = Platform.environment[key]; if ((value ?? '').isNotEmpty) { _globalVariables[key] = value!; } } List keys = distributeOptions.variables?.keys.toList() ?? []; for (String key in keys) { String? value = distributeOptions.variables?[key]; if ((value ?? '').isNotEmpty) { _globalVariables[key] = value!; } } } return _globalVariables; } DistributeOptions? _distributeOptions; DistributeOptions get distributeOptions { if (_distributeOptions == null) { File file = File('distribute_options.yaml'); if (file.existsSync()) { final yamlString = File('distribute_options.yaml').readAsStringSync(); final yamlDoc = loadYaml(yamlString); _distributeOptions = DistributeOptions.fromJson( json.decode(json.encode(yamlDoc)), ); } else { _distributeOptions = DistributeOptions( output: 'dist/', releases: [], ); } } return _distributeOptions!; } Future _getCurrentVersion() async { try { var scriptFile = Platform.script.toFilePath(); var pathToPubSpecYaml = p.join(p.dirname(scriptFile), '../pubspec.yaml'); var pathToPubSpecLock = p.join(p.dirname(scriptFile), '../pubspec.lock'); var pubSpecYamlFile = File(pathToPubSpecYaml); var pubSpecLockFile = File(pathToPubSpecLock); if (pubSpecLockFile.existsSync()) { var yamlDoc = loadYaml(await pubSpecLockFile.readAsString()); if (yamlDoc['packages']['flutter_distributor'] == null) { var yamlDoc = loadYaml(await pubSpecYamlFile.readAsString()); return yamlDoc['version']; } return yamlDoc['packages']['flutter_distributor']['version']; } } catch (_) {} return null; } Future checkVersion() async { String? currentVersion = await _getCurrentVersion(); String? latestVersion = await PubDevApi.getLatestVersionFromPackage('flutter_distributor'); if (currentVersion != null && latestVersion != null && currentVersion.compareTo(latestVersion) < 0) { String msg = [ '╔════════════════════════════════════════════════════════════════════════════╗', '║ A new version of Flutter Distributor is available! ║', '║ ║', '║ To update to the latest version, run "flutter_distributor upgrade". ║', '╚════════════════════════════════════════════════════════════════════════════╝', '', ].join('\n'); print(msg); } return Future.value(); } Future getCurrentVersion() async { return await _getCurrentVersion(); } Future> package( String platform, List targets, { String? channel, String? artifactName, String? description, required bool cleanBeforeBuild, required Map buildArguments, Map? variables, }) async { List makeResultList = []; try { Directory outputDirectory = distributeOptions.outputDirectory; if (!outputDirectory.existsSync()) { outputDirectory.createSync(recursive: true); } if (cleanBeforeBuild) { await _builder.clean(); } bool isBuildOnlyOnce = platform != 'android'; BuildResult? buildResult; for (String target in targets) { logger.info('Packaging ${pubspec.name} ${pubspec.version} as $target:'); if (!isBuildOnlyOnce || (isBuildOnlyOnce && buildResult == null)) { try { buildResult = await _builder.build( platform, target: target, arguments: buildArguments, environment: variables ?? globalVariables, ); print( const JsonEncoder.withIndent(' ').convert(buildResult.toJson()), ); logger.info( 'Successfully built ${buildResult.outputDirectory} in ${buildResult.duration!.inSeconds}s' .brightGreen(), ); } on UnsupportedError catch (error) { logger.warning('Warning: ${error.message}'.yellow()); continue; } catch (error) { rethrow; } } if (buildResult != null) { String buildMode = buildArguments.containsKey('profile') ? 'profile' : 'release'; Map? arguments = { 'build_mode': buildMode, 'flavor': buildArguments['flavor'], 'channel': channel, 'artifact_name': artifactName, 'description': description, if (Platform.isWindows) 'arch': (buildResult as BuildWindowsResult).arch, }; MakeResult makeResult = await _packager.package( platform, target, arguments, outputDirectory, buildOutputDirectory: buildResult.outputDirectory, buildOutputFiles: buildResult.outputFiles, ); print( const JsonEncoder.withIndent(' ').convert(makeResult.toJson()), ); FileSystemEntity artifact = makeResult.artifacts.first; logger.info( 'Successfully packaged ${artifact.path}'.brightGreen(), ); makeResultList.add(makeResult); } } } catch (error) { logger.severe(error.toString().red()); if (error is Error) { logger.severe(error.stackTrace.toString().red()); } rethrow; } return makeResultList; } Future> publish( FileSystemEntity fileSystemEntity, List targets, { Map? publishArguments, Map? variables, }) async { List publishResultList = []; try { for (String target in targets) { ProgressBar progressBar = ProgressBar( format: 'Publishing to $target: {bar} {value}/{total} {percentage}%', ); Map? newPublishArguments = {}; if (publishArguments != null) { for (var key in publishArguments.keys) { if (!key.startsWith('$target-')) continue; dynamic value = publishArguments[key]; if (value is List) { // ignore: prefer_for_elements_to_map_fromiterable value = Map.fromIterable( value, key: (e) => e.split('=')[0], value: (e) => e.split('=')[1], ); } newPublishArguments.putIfAbsent( key.replaceAll('$target-', ''), () => value, ); } } if (newPublishArguments.keys.isEmpty) { newPublishArguments = publishArguments; } PublishResult publishResult = await _publisher.publish( fileSystemEntity, target: target, environment: variables ?? globalVariables, publishArguments: newPublishArguments, onPublishProgress: (sent, total) { if (!progressBar.isActive) { progressBar.start(total, sent); } else { progressBar.update(sent); } }, ); if (progressBar.isActive) progressBar.stop(); logger.info( 'Successfully published ${publishResult.url}'.brightGreen(), ); publishResultList.add(publishResult); } } on Error catch (error) { logger.severe(error.toString().brightRed()); logger.severe(error.stackTrace.toString().brightRed()); } return publishResultList; } Future release( String name, { required List jobNameList, required List skipJobNameList, required bool cleanBeforeBuild, }) async { final time = Stopwatch()..start(); try { Directory outputDirectory = distributeOptions.outputDirectory; if (!outputDirectory.existsSync()) { outputDirectory.createSync(recursive: true); } List releases = []; if (name.isNotEmpty) { releases = distributeOptions.releases.where((e) => e.name == name).toList(); } if (releases.isEmpty) { throw Exception('Missing/incomplete `distribute_options.yaml` file.'); } for (Release release in releases) { List filteredJobs = release.jobs.where((e) { if (jobNameList.isNotEmpty) { return jobNameList.contains(e.name); } if (skipJobNameList.isNotEmpty) { return !skipJobNameList.contains(e.name); } return true; }).toList(); if (filteredJobs.isEmpty) { throw Exception('No available jobs found in ${release.name}.'); } bool needCleanBeforeBuild = cleanBeforeBuild; for (ReleaseJob job in filteredJobs) { logger.info(''); logger.info( '${'===>'.blue()} ${'Releasing'.white(bold: true)} $name:${job.name.green(bold: true)}', ); Map variables = {} ..addAll(globalVariables) ..addAll(release.variables ?? {}) ..addAll(job.variables ?? {}); List makeResultList = await package( job.package.platform, [job.package.target], channel: job.package.channel, artifactName: distributeOptions.artifactName, cleanBeforeBuild: needCleanBeforeBuild, buildArguments: job.package.buildArgs ?? {}, variables: variables, ); // Clean only once needCleanBeforeBuild = false; if (job.publish != null || job.publishTo != null) { String? publishTarget = job.publishTo ?? job.publish?.target; MakeResult makeResult = makeResultList.first; FileSystemEntity artifact = makeResult.artifacts.first; await publish( artifact, [publishTarget!], publishArguments: job.publish?.args, variables: variables, ); } } } time.stop(); logger.info(''); logger.info( 'RELEASE SUCCESSFUL in ${time.elapsed.inSeconds}s'.green(bold: true), ); } catch (error, stacktrace) { time.stop(); logger.info(''); logger.severe( [ 'RELEASE FAILED in ${time.elapsed.inSeconds}s'.red(bold: true), error.toString().red(), stacktrace, ].join('\n'), ); } return Future.value(); } Future upgrade() async { await $( 'dart', ['pub', 'global', 'activate', 'flutter_distributor'], ); return Future.value(); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/release.dart ================================================ import 'package:flutter_distributor/src/release_job.dart'; class Release { Release({ this.variables, required this.name, required this.jobs, }); factory Release.fromJson(Map json) { Map variables = {}; if (json.containsKey('variables') && json['variables'] != null) { variables = Map.from(json['variables']); } List jobs = (json['jobs'] as List) .map((item) => ReleaseJob.fromJson(item)) .toList(); return Release( variables: variables, name: json['name'], jobs: jobs, ); } final Map? variables; final String name; final List jobs; Map toJson() { return { 'variables': variables, 'name': name, 'jobs': jobs.map((e) => e.toJson()).toList(), }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/release_job.dart ================================================ class ReleaseJobPackage { ReleaseJobPackage({ required this.platform, required this.target, this.channel, this.buildArgs, }); factory ReleaseJobPackage.fromJson(Map json) { return ReleaseJobPackage( platform: json['platform'], target: json['target'], channel: json['channel'], buildArgs: json['build_args'], ); } final String platform; final String target; final String? channel; final Map? buildArgs; Map toJson() { return { 'platform': platform, 'target': target, 'channel': channel, 'build_args': buildArgs, }..removeWhere((key, value) => value == null); } } class ReleaseJobPublish { ReleaseJobPublish({ required this.target, this.args, }); factory ReleaseJobPublish.fromJson(Map json) { return ReleaseJobPublish( target: json['target'], args: json['args'], ); } final String target; final Map? args; Map toJson() { return { 'target': target, 'args': args, }..removeWhere((key, value) => value == null); } } class ReleaseJob { ReleaseJob({ this.variables, required this.name, required this.package, this.publish, this.publishTo, }); factory ReleaseJob.fromJson(Map json) { Map variables = {}; if (json.containsKey('variables') && json['variables'] != null) { variables = Map.from(json['variables']); } return ReleaseJob( variables: variables, name: json['name'], package: ReleaseJobPackage.fromJson(json['package']), publish: json['publish'] != null ? ReleaseJobPublish.fromJson(json['publish']) : null, publishTo: json['publish_to'], ); } final Map? variables; final String name; final ReleaseJobPackage package; final ReleaseJobPublish? publish; final String? publishTo; Map toJson() { return { 'variables': variables, 'name': name, 'package': package.toJson(), 'publish': publish?.toJson(), 'publish_to': publishTo, }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/utils/default_shell_executor.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:charset/charset.dart'; import 'package:flutter_distributor/src/extensions/string.dart'; import 'package:flutter_distributor/src/utils/logger.dart'; import 'package:shell_executor/shell_executor.dart'; /// Convert bytes to string (UTF-8 or detected charset) String convertToString(List bytes) { if (Platform.isWindows) { final charset = Charset.detect(bytes); if (charset != null) { return charset.decode(bytes); } } return utf8.decode(bytes, allowMalformed: true); } class DefaultShellExecutor extends ShellExecutor { @override Future exec( String executable, List arguments, { String? workingDirectory, Map? environment, }) async { final Process process = await Process.start( executable, arguments, workingDirectory: workingDirectory, environment: environment, runInShell: true, ); logger.info('\$ $executable ${arguments.join(' ')}'.brightBlack()); String? stdoutStr; String? stderrStr; process.stdout.listen((data) { String msg = convertToString(data); stdoutStr = '${stdoutStr ?? ''}$msg'; stdout.write(msg.brightBlack()); }); process.stderr.listen((data) { String msg = convertToString(data); stderrStr = '${stderrStr ?? ''}$msg'; stderr.write(msg.brightRed()); }); int exitCode = await process.exitCode; return ProcessResult(process.pid, exitCode, stdoutStr, stderrStr); } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/utils/logger.dart ================================================ import 'package:logging/logging.dart'; Logger logger = Logger('flutter_distributor') ..onRecord.listen((record) { print(record.message); }); ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/utils/pub_dev_api.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; class PubDevApi { static Future getLatestVersionFromPackage(String package) async { String pubHostedUrl = Platform.environment['PUB_HOSTED_URL'] ?? ''; final pubSite = pubHostedUrl.isNotEmpty ? '$pubHostedUrl/api/packages/$package' : 'https://pub.dev/api/packages/$package'; final uri = Uri.parse(pubSite); try { final response = await Dio().get(uri.toString()); return response.data['latest']['version'] as String?; } catch (error) { rethrow; } } } ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/lib/src/utils/utils.dart ================================================ export 'default_shell_executor.dart'; export 'logger.dart'; export 'pub_dev_api.dart'; ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/pubspec.yaml ================================================ name: flutter_distributor description: A complete tool for packaging and publishing your Flutter apps. version: 0.3.7 homepage: https://distributor.leanflutter.org repository: https://github.com/leanflutter/flutter_distributor/tree/main/packages/flutter_distributor issue_tracker: https://github.com/leanflutter/flutter_distributor/issues platforms: linux: macos: windows: environment: sdk: ">=2.16.0 <4.0.0" dependencies: ansicolor: ^2.0.1 args: ^2.2.0 charset: ^2.0.1 dio: ^5.3.4 flutter_app_builder: path: ../flutter_app_builder flutter_app_packager: path: ../flutter_app_packager flutter_app_publisher: path: ../flutter_app_publisher logging: ^1.0.2 path: ^1.8.1 pubspec_parse: ^1.1.0 shell_executor: ^0.1.5 shell_uikit: ^0.1.1 yaml: ^3.1.0 dev_dependencies: dependency_validator: ^3.0.0 test: ^1.23.1 executables: flutter_distributor: main ================================================ FILE: plugins/flutter_distributor/packages/flutter_distributor/test/src/extensions/string_test.dart ================================================ import 'package:flutter_distributor/src/extensions/string.dart'; import 'package:test/test.dart'; void main() { // print('black '.black()); // print('red '.red()); // print('green '.green()); // print('yellow '.yellow()); // print('blue '.blue()); // print('magenta '.magenta()); // print('cyan '.cyan()); // print('white '.white()); // print('brightBlack '.brightBlack()); // print('brightRed '.brightRed()); // print('brightGreen '.brightGreen()); // print('brightYellow '.brightYellow()); // print('brightBlue '.brightBlue()); // print('brightMagenta'.brightMagenta()); // print('BrightCyan '.brightCyan()); // print('brightWhite '.brightWhite()); // print('black '.black(bold: true)); // print('red '.red(bold: true)); // print('green '.green(bold: true)); // print('yellow '.yellow(bold: true)); // print('blue '.blue(bold: true)); // print('magenta '.magenta(bold: true)); // print('cyan '.cyan(bold: true)); // print('white '.white(bold: true)); // print('brightBlack '.brightBlack(bold: true)); // print('brightRed '.brightRed(bold: true)); // print('brightGreen '.brightGreen(bold: true)); // print('brightYellow '.brightYellow(bold: true)); // print('brightBlue '.brightBlue(bold: true)); // print('brightMagenta'.brightMagenta(bold: true)); // print('BrightCyan '.brightCyan(bold: true)); // print('brightWhite '.brightWhite(bold: true)); // print('black '.black(bg: true)); // print('red '.red(bg: true)); // print('green '.green(bg: true)); // print('yellow '.yellow(bg: true)); // print('blue '.blue(bg: true)); // print('magenta '.magenta(bg: true)); // print('cyan '.cyan(bg: true)); // print('white '.white(bg: true)); // print('brightBlack '.brightBlack(bg: true)); // print('brightRed '.brightRed(bg: true)); // print('brightGreen '.brightGreen(bg: true)); // print('brightYellow '.brightYellow(bg: true)); // print('brightBlue '.brightBlue(bg: true)); // print('brightMagenta'.brightMagenta(bg: true)); // print('BrightCyan '.brightCyan(bg: true)); // print('brightWhite '.brightWhite(bg: true)); // print('black '.black(bold: true, bg: true)); // print('red '.red(bold: true, bg: true)); // print('green '.green(bold: true, bg: true)); // print('yellow '.yellow(bold: true, bg: true)); // print('blue '.blue(bold: true, bg: true)); // print('magenta '.magenta(bold: true, bg: true)); // print('cyan '.cyan(bold: true, bg: true)); // print('white '.white(bold: true, bg: true)); // print('brightBlack '.brightBlack(bold: true, bg: true)); // print('brightRed '.brightRed(bold: true, bg: true)); // print('brightGreen '.brightGreen(bold: true, bg: true)); // print('brightYellow '.brightYellow(bold: true, bg: true)); // print('brightBlue '.brightBlue(bold: true, bg: true)); // print('brightMagenta'.brightMagenta(bold: true, bg: true)); // print('BrightCyan '.brightCyan(bold: true, bg: true)); // print('brightWhite '.brightWhite(bold: true, bg: true)); group('StringExt', () { test('black', () { expect('test'.red(), 'test'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/bin/main.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; import 'package:parse_app_package/parse_app_package.dart'; JsonEncoder _encoder = const JsonEncoder.withIndent(' '); Future main(List args) async { ArgParser argParser = ArgParser(); ArgResults argResults = argParser.parse(args); String path = argResults.arguments.first; AppPackage appPackage = await parseAppPackage(File(path)); print(_encoder.convert(appPackage.toJson())); } ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/lib/parse_app_package.dart ================================================ library app_package_maker; export 'src/api/app_package_parser.dart'; export 'src/parse_app_package.dart'; ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/lib/src/api/app_package_parser.dart ================================================ import 'dart:io'; abstract class AppPackageParser { String get name => throw UnimplementedError(); bool get isSupportedOnCurrentPlatform => true; Future parse(File file); } class AppPackage { AppPackage({ required this.platform, required this.identifier, required this.name, required this.version, required this.buildNumber, }); final String platform; final String identifier; final String name; final String version; final int buildNumber; Map toJson() { return { 'platform': platform, 'identifier': identifier, 'name': name, 'version': version, 'buildNumber': buildNumber, }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/lib/src/parse_app_package.dart ================================================ import 'dart:io'; import 'package:parse_app_package/src/api/app_package_parser.dart'; import 'package:parse_app_package/src/parsers/parsers.dart'; final List _parsers = [ AppPackageParserApk(), AppPackageParserIpa(), ]; Future parseAppPackage(File file) { AppPackageParser parser = _parsers.firstWhere( (e) => file.path.endsWith('.${e.name}'), ); return parser.parse(file); } ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/lib/src/parsers/apk/app_package_parser_apk.dart ================================================ import 'dart:io'; import 'package:parse_app_package/src/api/app_package_parser.dart'; import 'package:shell_executor/shell_executor.dart'; class AppPackageParserApk extends AppPackageParser { @override String get name => 'apk'; @override Future parse(File file) async { String? androidHome = Platform.environment['ANDROID_HOME']; if ((androidHome ?? '').isEmpty) { throw Exception('Missing `ANDROID_HOME` environment variable.'); } String buildToolsDir = Directory('$androidHome/build-tools') .listSync() .firstWhere((element) => !element.path.contains('.DS_Store')) .path; ProcessResult processResult = await $( '$buildToolsDir/aapt', ['d', '--values', 'badging', file.path], ); String resultString = processResult.stdout; RegExpMatch? regExpMatch1 = RegExp(r"name='([^']+)'").firstMatch(resultString); RegExpMatch? regExpMatch2 = RegExp(r"application-label:'([^']+)'").firstMatch(resultString); RegExpMatch? regExpMatch3 = RegExp(r"versionName='([^']+)").firstMatch(resultString); RegExpMatch? regExpMatch4 = RegExp(r"versionCode='(\d+)'").firstMatch(resultString); AppPackage appPackage = AppPackage( platform: 'android', identifier: regExpMatch1!.group(1)!, name: regExpMatch2!.group(1)!, version: regExpMatch3!.group(1)!, buildNumber: int.parse(regExpMatch4!.group(1)!), ); return appPackage; } } ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/lib/src/parsers/ipa/app_package_parser_ipa.dart ================================================ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:archive/archive_io.dart'; import 'package:parse_app_package/src/api/app_package_parser.dart'; import 'package:plist_parser/plist_parser.dart'; class AppPackageParserIpa extends AppPackageParser { @override String get name => 'ipa'; @override Future parse(File file) async { final ipaBytes = file.readAsBytesSync(); final archive = ZipDecoder().decodeBytes(ipaBytes); Map? result; for (final item in archive) { if (item.isFile && item.name.endsWith('.app/Info.plist')) { final data = item.content; result = PlistParser().parseBytes(data); break; } } if (result == null) throw Exception('Can\'t parse .ipa file.'); return AppPackage( platform: 'ios', identifier: result['CFBundleIdentifier'], name: result['CFBundleDisplayName'] ?? result['CFBundleName'], version: result['CFBundleShortVersionString'], buildNumber: int.parse(result['CFBundleVersion']), ); } } ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/lib/src/parsers/parsers.dart ================================================ export 'apk/app_package_parser_apk.dart'; export 'ipa/app_package_parser_ipa.dart'; ================================================ FILE: plugins/flutter_distributor/packages/parse_app_package/pubspec.yaml ================================================ name: parse_app_package description: Parse app package version: 0.4.0 homepage: https://distributor.leanflutter.dev repository: https://github.com/leanflutter/flutter_distributor/tree/main/packages/parse_app_package environment: sdk: ">=2.16.0 <4.0.0" dependencies: archive: ^3.4.10 args: ^2.2.0 plist_parser: ^0.0.11 shell_executor: ^0.1.5 executables: parse_app_package: main dev_dependencies: dependency_validator: ^3.0.0 ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/lib/shell_executor.dart ================================================ library shell_executor; export 'src/command.dart'; export 'src/command_error.dart'; export 'src/shell_executor.dart'; export 'src/utils/path_expansion.dart'; ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/lib/src/command.dart ================================================ import 'dart:io'; import 'package:shell_executor/src/shell_executor.dart'; abstract class Command { String get executable => throw UnimplementedError(); Future install(); Future exec( List arguments, { String? workingDirectory, Map? environment, }) { return ShellExecutor.global.exec( executable, arguments, workingDirectory: workingDirectory, environment: environment, ); } ProcessResult execSync( List arguments, { String? workingDirectory, Map? environment, bool runInShell = false, }) { return ShellExecutor.global.execSync( executable, arguments, workingDirectory: workingDirectory, environment: environment, runInShell: runInShell, ); } } ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/lib/src/command_error.dart ================================================ import 'package:shell_executor/src/command.dart'; class CommandError extends Error { CommandError(this.command, [this.message]); final Command command; final String? message; @override String toString() { return (message != null) ? 'CommandError: $message' : 'CommandError'; } } ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/lib/src/shell_executor.dart ================================================ import 'dart:convert'; import 'dart:io'; Future $( String executable, List arguments, { String? workingDirectory, Map? environment, }) { return ShellExecutor.global.exec( executable, arguments, workingDirectory: workingDirectory, environment: environment, ); } class ShellExecutor { static ShellExecutor global = ShellExecutor(); Future exec( String executable, List arguments, { String? workingDirectory, Map? environment, }) async { final Process process = await Process.start( executable, arguments, workingDirectory: workingDirectory, environment: environment, ); String? stdoutStr; String? stderrStr; process.stdout.listen((event) { String msg = utf8.decoder.convert(event); stdoutStr = '${stdoutStr ?? ''}$msg'; stdout.write(msg); }); process.stderr.listen((event) { String msg = utf8.decoder.convert(event); stderrStr = '${stderrStr ?? ''}$msg'; stderr.write(msg); }); int exitCode = await process.exitCode; return ProcessResult(process.pid, exitCode, stdoutStr, stderrStr); } ProcessResult execSync( String executable, List arguments, { String? workingDirectory, Map? environment, bool runInShell = false, }) { final ProcessResult processResult = Process.runSync( executable, arguments, workingDirectory: workingDirectory, environment: environment, runInShell: runInShell, ); return processResult; } } ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/lib/src/utils/path_expansion.dart ================================================ String pathExpansion( String path, [ Map environment = const {}, ]) { if (path.startsWith('~/')) { final home = environment['HOME'] ?? environment['USERPROFILE']; path = '$home${path.substring(1)}'; } final matches = [ ...RegExp(r'\$(\w+)').allMatches(path), ...RegExp(r'\$\{(\w+)\}').allMatches(path), ]; for (final match in matches) { final envName = match.group(1); final envValue = environment[envName]; path = path.replaceFirst(match.group(0)!, envValue ?? ''); } return path; } ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/pubspec.yaml ================================================ name: shell_executor description: A simple shell commands executor. version: 0.1.6 homepage: https://distributor.leanflutter.dev repository: https://github.com/leanflutter/flutter_distributor/tree/main/packages/shell_executor environment: sdk: ">=2.16.0 <4.0.0" dev_dependencies: dependency_validator: ^3.0.0 test: ^1.23.1 ================================================ FILE: plugins/flutter_distributor/packages/shell_executor/test/src/utils/path_expansion_test.dart ================================================ import 'package:shell_executor/src/utils/path_expansion.dart'; import 'package:test/test.dart'; void main() { Map environment = { 'HOME': '/home/root', }; group('pathExpansion', () { test('~/Documents', () { final path = pathExpansion('~/Documents', environment); expect(path, '/home/root/Documents'); }); test('\$HOME/Documents', () { final r = pathExpansion('\$HOME/Documents', environment); expect(r, '/home/root/Documents'); }); test('\${HOME}/Documents', () { final r = pathExpansion('\${HOME}/Documents', environment); expect(r, '/home/root/Documents'); }); }); } ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/example/progress_bar_example.dart ================================================ import 'dart:async'; import 'dart:io'; import 'dart:math' as math; import 'package:shell_uikit/shell_uikit.dart'; /// This example demonstrates various ways to use the ProgressBar component. void main() async { // Clear the screen if (Platform.isWindows) { stdout.write('\x1B[2J\x1B[0f'); } else { stdout.write('\x1B[2J\x1B[H'); } stdout.writeln('ProgressBar Examples\n'); // Run examples one by one await basicExample(); await customFormatExample(); await customAppearanceExample(); await incrementExample(); await simulateDownloadExample(); await multipleProgressBarsExample(); stdout.writeln('\nAll examples completed!'); } /// Basic usage of ProgressBar Future basicExample() async { stdout.writeln('1. Basic Progress Bar:'); // Create a simple progress bar final progressBar = ProgressBar( format: '[{bar}] {percentage}% ({value}/{total})', ); // Start the progress bar with a total of 100 progressBar.start(100); // Update the progress bar in a loop for (int i = 0; i <= 100; i++) { await Future.delayed(Duration(milliseconds: 20)); progressBar.update(i); } // Progress bar will automatically stop when it reaches 100% await Future.delayed(Duration(milliseconds: 500)); stdout.writeln('\n'); } /// Example with custom format including duration and ETA Future customFormatExample() async { stdout.writeln('2. Custom Format with Duration and ETA:'); // Create a progress bar with custom format final progressBar = ProgressBar( format: '[{bar}] {percentage}% | Elapsed: {duration}s | ETA: {eta}s', ); // Start the progress bar with a total of 50 progressBar.start(50); // Update the progress bar with varying speed to demonstrate ETA changes for (int i = 0; i <= 50; i++) { // Simulate varying processing speed final delay = 30 + (math.sin(i / 5) * 20).round(); await Future.delayed(Duration(milliseconds: delay)); progressBar.update(i); } await Future.delayed(Duration(milliseconds: 500)); stdout.writeln('\n'); } /// Example with custom appearance Future customAppearanceExample() async { stdout.writeln('3. Custom Appearance:'); // Create a progress bar with custom characters and size final progressBar = ProgressBar( format: '[{bar}] {percentage}%', barCompleteChar: '█', // Full block barIncompleteChar: '░', // Light shade barSize: 30, // Shorter bar ); progressBar.start(100); for (int i = 0; i <= 100; i++) { await Future.delayed(Duration(milliseconds: 15)); progressBar.update(i); } await Future.delayed(Duration(milliseconds: 500)); stdout.writeln('\n'); } /// Example using increment method Future incrementExample() async { stdout.writeln('4. Using Increment Method:'); final progressBar = ProgressBar( format: '[{bar}] {percentage}% | {value}/{total} steps completed', ); // Start with a total of 20 steps progressBar.start(20); // Increment by different amounts for (int i = 0; i < 5; i++) { await Future.delayed(Duration(milliseconds: 300)); progressBar.increment(); // Increment by 1 (default) } for (int i = 0; i < 3; i++) { await Future.delayed(Duration(milliseconds: 500)); progressBar.increment(2); // Increment by 2 } await Future.delayed(Duration(milliseconds: 700)); progressBar.increment(5); // Increment by 5 await Future.delayed(Duration(milliseconds: 1000)); progressBar.update(20); // Complete await Future.delayed(Duration(milliseconds: 500)); stdout.writeln('\n'); } /// Example simulating a file download with changing total Future simulateDownloadExample() async { stdout.writeln('5. Simulating Download (with changing total):'); final progressBar = ProgressBar( format: '[{bar}] {percentage}% | {value}/{total} KB | {duration}s elapsed', ); // Start with an initial estimate progressBar.start(1000); int value = 0; // Simulate download with varying speed and changing total size for (int i = 0; i < 20; i++) { await Future.delayed(Duration(milliseconds: 200)); // Simulate downloaded chunk final chunk = 50 + (math.Random().nextInt(50)); value += chunk; progressBar.update(value); // Occasionally update the total size (simulating better estimates as download progresses) if (i == 5) { progressBar.setTotal(1200); stdout.writeln('\n (Download size updated to 1200 KB)'); } else if (i == 12) { progressBar.setTotal(1500); stdout.writeln('\n (Download size updated to 1500 KB)'); } } // Complete the download progressBar.update(progressBar.total); await Future.delayed(Duration(milliseconds: 500)); stdout.writeln('\n'); } /// Example showing multiple progress bars Future multipleProgressBarsExample() async { stdout.writeln('6. Multiple Progress Bars:'); stdout.writeln(' (Three tasks running at different speeds)'); // Create three progress bars with different formats final task1 = ProgressBar( format: 'Task 1: [{bar}] {percentage}%', barCompleteChar: '=', barIncompleteChar: ' ', ); final task2 = ProgressBar( format: 'Task 2: [{bar}] {percentage}%', barCompleteChar: '#', barIncompleteChar: '-', ); final task3 = ProgressBar( format: 'Task 3: [{bar}] {percentage}%', barCompleteChar: '■', barIncompleteChar: '□', ); // Start all tasks task1.start(100); stdout.writeln(); task2.start(100); stdout.writeln(); task3.start(100); // Update tasks at different speeds for (int i = 0; i <= 100; i++) { await Future.delayed(Duration(milliseconds: 30)); if (i <= 100) task1.update(i); if (i <= 100 && i % 2 == 0) task2.update(i ~/ 2); if (i <= 100 && i % 3 == 0) task3.update(i ~/ 3); } // Wait for task2 to complete for (int i = 51; i <= 100; i++) { await Future.delayed(Duration(milliseconds: 60)); task2.update(i); } // Wait for task3 to complete for (int i = 34; i <= 100; i++) { await Future.delayed(Duration(milliseconds: 90)); task3.update(i); } await Future.delayed(Duration(milliseconds: 500)); stdout.writeln('\n'); // Make sure to dispose all progress bars task1.dispose(); task2.dispose(); task3.dispose(); } ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/example/spinner_example.dart ================================================ import 'dart:async'; import 'package:shell_uikit/shell_uikit.dart'; /// This example demonstrates how to use various features of the Spinner component void main() async { print('Spinner Component Examples\n'); // Basic usage example await basicExample(); // Show all animation types await allSpinnerTypesExample(); // Demonstrate text updating functionality await updateTextExample(); // Show different status messages await statusMessagesExample(); print('\nAll examples completed!'); } /// Demonstrates basic usage of Spinner Future basicExample() async { print('1. Basic Usage Example:'); final spinner = Spinner(text: 'Loading...'); spinner.start(); // Simulate some time-consuming operation await Future.delayed(const Duration(seconds: 2)); spinner.success('Loading completed!'); await Future.delayed(const Duration(milliseconds: 500)); } /// Shows all available Spinner animation types Future allSpinnerTypesExample() async { print('\n2. All Animation Types Example:'); final spinnerTypes = SpinnerType.values; for (final type in spinnerTypes) { final spinner = Spinner( text: '${type.name} type animation', spinnerType: type, ); spinner.start(); await Future.delayed(const Duration(seconds: 2)); spinner.stop(); print(' - Displayed ${type.name} type'); await Future.delayed(const Duration(milliseconds: 300)); } } /// Demonstrates how to update text while Spinner is running Future updateTextExample() async { print('\n3. Text Update Example:'); final spinner = Spinner(text: 'Initial text'); spinner.start(); await Future.delayed(const Duration(seconds: 1)); spinner.updateText('Updated text 1'); await Future.delayed(const Duration(seconds: 1)); spinner.updateText('Updated text 2'); await Future.delayed(const Duration(seconds: 1)); spinner.updateText('Updated text 3'); await Future.delayed(const Duration(seconds: 1)); spinner.success('Text update example completed'); await Future.delayed(const Duration(milliseconds: 500)); } /// Shows different status messages (success, error, info, warning) Future statusMessagesExample() async { print('\n4. Status Messages Example:'); // Success message var spinner = Spinner(text: 'Processing success message'); spinner.start(); await Future.delayed(const Duration(seconds: 1)); spinner.success('Operation completed successfully'); await Future.delayed(const Duration(milliseconds: 500)); // Error message spinner = Spinner(text: 'Processing error message'); spinner.start(); await Future.delayed(const Duration(seconds: 1)); spinner.error('Operation failed'); await Future.delayed(const Duration(milliseconds: 500)); // Info message spinner = Spinner(text: 'Processing info message'); spinner.start(); await Future.delayed(const Duration(seconds: 1)); spinner.info('This is an information message'); await Future.delayed(const Duration(milliseconds: 500)); // Warning message spinner = Spinner(text: 'Processing warning message'); spinner.start(); await Future.delayed(const Duration(seconds: 1)); spinner.warn('This is a warning message'); await Future.delayed(const Duration(milliseconds: 500)); } ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/lib/shell_uikit.dart ================================================ library shell_uikit; export 'src/progress_bar.dart'; ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/lib/src/progress_bar.dart ================================================ import 'dart:async'; import 'dart:io'; class ProgressBar { ProgressBar({ required this.format, this.barCompleteChar = '\u2588', this.barIncompleteChar = '\u2591', }); final String format; final String barCompleteChar; final String barIncompleteChar; late int barSize = 40; Timer? timer; // the current bar value int value = 0; // the end value of the bar int total = 100; // start time (used for eta calculation) DateTime? startTime; // stop time (used for duration calculation) DateTime? stopTime; // progress bar active ? bool isActive = false; /// Starts the progress bar and set the total and initial value void start(int totalValue, [int? startValue]) { // set initial values value = startValue ?? 0; total = (totalValue >= 0) ? totalValue : 100; // store start time for duration+eta calculation startTime = DateTime.now(); // reset stop time for 're-start' scenario (used for duration calculation) stopTime = null; // set flag isActive = true; timer = Timer.periodic(const Duration(milliseconds: 10), (_) { render(); if (!isActive && timer?.isActive == true) { timer?.cancel(); timer = null; stdout.writeln(); } }); } void update(int currentValue) { if (value == currentValue) return; value = currentValue; if (currentValue >= total) { stop(); } } /// Gets the total progress value. int getTotal() { return total; } /// Sets the total progress value while progressbar is active. void setTotal(totalValue) { if (totalValue >= 0) { total = totalValue; } } /// Stops the progress bar and go to next line void stop() { // set flag isActive = false; // store stop timestamp to get total duration stopTime = DateTime.now(); } void render() { // calculate the bar complete size int barCompleteSize = ((value / total) * barSize).toInt(); String bar = '${barCompleteChar * barCompleteSize}${barIncompleteChar * (barSize - barCompleteSize)}'; String percentage = (value * 100 / total).toStringAsFixed(1); stdout.write('\r'); stdout.write( format .replaceAll('{bar}', bar) .replaceAll('{percentage}', percentage) .replaceAll('{value}', '$value') .replaceAll('{total}', '$total'), ); } } ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/lib/src/spinner.dart ================================================ import 'dart:async'; import 'dart:io'; /// A command-line spinner component that shows an animation /// to indicate a loading or processing state. class Spinner { /// Creates a new spinner with the specified configuration. /// /// [text] is the message displayed next to the spinner. /// [spinnerType] defines the animation style (default is 'dots'). Spinner({ String text = 'Loading', this.spinnerType = SpinnerType.dots, this.interval = const Duration(milliseconds: 80), }) : _text = text; /// The text displayed next to the spinner. String _text; /// Gets the current spinner text. String get text => _text; /// The type of spinner animation to display. final SpinnerType spinnerType; /// The interval between animation frames. final Duration interval; /// Timer that controls the animation. Timer? _timer; /// Current frame index in the animation sequence. int _frameIndex = 0; /// Whether the spinner is currently running. bool get isRunning => _timer != null; /// Starts the spinner animation. void start() { if (_timer != null) return; // Record the start time _frameIndex = 0; // Clear the current line stdout.write('\r'); // Start the animation timer _timer = Timer.periodic(interval, (_) { _draw(); }); } /// Stops the spinner animation. void stop() { _timer?.cancel(); _timer = null; // Clear the spinner line _clearLine(); } /// Stops the spinner and shows a success message. void success([String? message]) { stop(); if (message != null) { stdout.write('\r✓ $message\n'); } } /// Stops the spinner and shows an error message. void error([String? message]) { stop(); if (message != null) { stdout.write('\r✗ $message\n'); } } /// Stops the spinner and shows an info message. void info([String? message]) { stop(); if (message != null) { stdout.write('\rℹ $message\n'); } } /// Stops the spinner and shows a warning message. void warn([String? message]) { stop(); if (message != null) { stdout.write('\r⚠ $message\n'); } } /// Updates the spinner text while it's running. void updateText(String newText) { if (_text != newText) { _clearLine(); _text = newText; if (isRunning) { _draw(); } } } /// Draws the current frame of the spinner animation. void _draw() { final frames = _getFrames(); final frame = frames[_frameIndex % frames.length]; // Clear the current line and write the new frame stdout.write('\r$frame $_text'); // Move to the next frame _frameIndex++; } /// Clears the current line in the terminal. void _clearLine() { stdout.write('\r${' ' * (_text.length + 10)}\r'); } /// Gets the animation frames based on the selected spinner type. List _getFrames() { switch (spinnerType) { case SpinnerType.dots: return ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; case SpinnerType.line: return ['-', '\\', '|', '/']; case SpinnerType.growVertical: return ['▁', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃']; case SpinnerType.growHorizontal: return ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']; case SpinnerType.circle: return ['◜', '◠', '◝', '◞', '◡', '◟']; case SpinnerType.dots2: return ['. ', '.. ', '...', ' ..', ' .', ' ']; case SpinnerType.bounce: return ['⠁', '⠂', '⠄', '⠂']; case SpinnerType.arrows: return ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙']; } } } /// Defines different spinner animation styles. enum SpinnerType { /// Braille dots animation (default) dots, /// Simple line animation line, /// Growing vertical bar growVertical, /// Growing horizontal bar growHorizontal, /// Circle animation circle, /// Simple dots animation dots2, /// Bouncing animation bounce, /// Rotating arrows arrows, } ================================================ FILE: plugins/flutter_distributor/packages/shell_uikit/pubspec.yaml ================================================ name: shell_uikit description: A simple shell ui kit. version: 0.1.1 homepage: https://distributor.leanflutter.dev repository: https://github.com/leanflutter/flutter_distributor/tree/main/packages/shell_uikit environment: sdk: ">=2.16.0 <4.0.0" dev_dependencies: dependency_validator: ^3.0.0 ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/.gitignore ================================================ .dart_tool/ .packages build/ pubspec.lock # Except for application packages .flutter-plugins .flutter-plugins-dependencies ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/LICENSE ================================================ MIT License Copyright (c) 2021-present LiJianying Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/analysis_options.yaml ================================================ include: package:mostly_reasonable_lints/analysis_options.yaml linter: rules: avoid_print: false ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/check_version_result.dart ================================================ class CheckVersionResult { CheckVersionResult({ this.latestVersion, this.currentVersion, }); final String? latestVersion; final String? currentVersion; /// Whether a new version is available. bool get isNewVersionAvailable => currentVersion != null && latestVersion != null && currentVersion!.compareTo(latestVersion!) < 0; } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/cli/cli.dart ================================================ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:shell_uikit/shell_uikit.dart'; import 'package:unified_distributor/src/cli/command_package.dart'; import 'package:unified_distributor/src/cli/command_publish.dart'; import 'package:unified_distributor/src/cli/command_release.dart'; import 'package:unified_distributor/src/cli/command_upgrade.dart'; import 'package:unified_distributor/unified_distributor.dart'; class UnifiedDistributorCommandLineInterface { UnifiedDistributorCommandLineInterface( String executableName, String description, { String? packageName, String? displayName, }) { _distributor = UnifiedDistributor( packageName ?? executableName, displayName ?? executableName, ); if (packageName != 'fastforge') { String note = [ '╔════════════════════════════════════════════════════════════════════════════╗', '║ Important Notice: flutter_distributor has been renamed to fastforge. ║', '║ You can continue to use flutter_distributor, but we recommend migrating to ║', '║ fastforge for the latest features and updates. ║', '║ ║', '║ Please visit https://fastforge.dev for more information. ║', '╚════════════════════════════════════════════════════════════════════════════╝', ].join('\n').yellow(bold: true); description = '$note\n\n$description'; } _runner = CommandRunner(executableName, description); _runner.addCommand(CommandPackage(_distributor)); _runner.addCommand(CommandPublish(_distributor)); _runner.addCommand(CommandRelease(_distributor)); _runner.addCommand(CommandUpgrade(_distributor)); _runner.argParser ..addFlag( 'version', help: 'Reports the version of this tool.', negatable: false, ) ..addFlag( 'version-check', help: 'Check for updates when this command runs.', defaultsTo: true, negatable: true, ); } late final UnifiedDistributor _distributor; late final CommandRunner _runner; String get displayName => _distributor.displayName; String get packageName => _distributor.packageName; Future run(List args) async { ArgResults argResults = _runner.parse(args); if (argResults.wasParsed('version')) { String? currentVersion = await _distributor.getCurrentVersion(); if (currentVersion != null) { logger.info(currentVersion); return; } } if (argResults['version-check']) { Spinner spinner = Spinner(text: 'Checking for updates...'); spinner.start(); // Check if a newer version of the tool is available final result = await _distributor.checkVersion(); spinner.stop(); if (result.isNewVersionAvailable) { String msg = [ '🚀 New version of $displayName available! ' .brightYellow(bold: true) + '${result.currentVersion}'.brightRed() + ' → '.brightYellow() + '${result.latestVersion}'.brightGreen(bold: true), 'Update with: '.brightYellow() + '"$packageName upgrade"'.cyan(bold: true), ].join('\n'); logger.info(msg); } else { String msg = [ '🎉 You are using the latest version '.brightBlack() + '(${result.currentVersion})'.brightBlack(bold: true), ].join('\n'); logger.info(msg); } logger.info(''); } return _runner.runCommand(argResults); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/cli/command_package.dart ================================================ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:unified_distributor/src/extensions/string.dart'; import 'package:unified_distributor/src/unified_distributor.dart'; /// Package an application bundle for a specific platform and target /// /// This command wrapper defines, parses and transforms all passed arguments, /// so that they may be passed to `unified_distributor`. The distributor will /// then build an application bundle using `flutter_app_packager`. class CommandPackage extends Command { CommandPackage(this.distributor) { argParser.addOption( 'platform', valueHelp: [ 'android', 'ios', 'linux', 'macos', 'ohos', 'windows', 'web', ].join(','), help: 'The platform to package the application for', ); argParser.addOption( 'targets', aliases: ['target'], valueHelp: [ 'apk', 'aab', 'app', 'appimage', 'deb', 'dmg', 'exe', 'hap', 'ipa', 'msix', 'pkg', 'rpm', 'zip', ].join(','), help: 'Comma separated list of bundle types to build.', ); argParser.addOption('channel', valueHelp: ''); argParser.addOption('artifact-name', valueHelp: ''); argParser.addFlag( 'skip-clean', help: 'Whether or not to skip \'flutter clean\' before packaging.', ); argParser.addOption( 'flutter-build-args', valueHelp: 'verbose,obfuscate', help: 'Arguments to pass directly to flutter build', ); argParser.addOption( 'build-target', valueHelp: 'path', help: 'The --target argument passed to \'flutter build\'', ); argParser.addOption( 'build-flavor', valueHelp: '', help: 'The --flavor argument passed to \'flutter build\'', ); argParser.addOption( 'build-target-platform', valueHelp: '', help: 'The --target-platform argument passed to \'flutter build\'', ); argParser.addOption( 'build-export-options-plist', valueHelp: '', help: 'The --export-options-plist argument passed \'flutter build\'', ); argParser.addMultiOption( 'build-dart-define', valueHelp: 'foo=bar', help: [ 'The --dart-define argument(s) passed to \'flutter build\'', 'You may add multiple \'--build-dart-define key=value\' pairs', ].join('\n'), ); } final UnifiedDistributor distributor; @override String get name => 'package'; @override String get description => [ 'Package the current Flutter application for distribution', '', 'Options prefixed with --build- are passed directly to \'flutter build\'', 'For more details on build options, refer to the \'flutter build\' documentation.', ].join('\n'); @override Future run() async { final String? platform = argResults?['platform']; final List targets = '${argResults?['targets'] ?? ''}' .split(',') .where((e) => e.isNotEmpty) .toList(); final String? channel = argResults?['channel']; final String? artifactName = argResults?['artifact-name']; final String? flutterBuildArgs = argResults?['flutter-build-args']; final bool isSkipClean = argResults?.wasParsed('skip-clean') ?? false; final Map buildArguments = _generateBuildArgs(flutterBuildArgs); // At least `platform` and one `targets` is required for flutter build if (platform == null) { print('\nThe \'platform\' options is mandatory!'.red(bold: true)); exit(1); } if (targets.isEmpty) { print('\nAt least one \'target\' must be specified!'.red(bold: true)); exit(1); } return distributor.package( platform, targets, channel: channel, artifactName: artifactName, cleanBeforeBuild: !isSkipClean, buildArguments: buildArguments, ); } Map _generateBuildArgs(String? flutterBuildArgs) { Map buildArguments = {}; if (argResults?.options == null) return buildArguments; for (var option in argResults!.options) { if (!option.startsWith('build-')) continue; dynamic value = argResults?[option]; if (value is List) { // ignore: prefer_for_elements_to_map_fromiterable value = Map.fromIterable( value, key: (e) => e.split('=')[0], value: (e) => e.split('=')[1], ); } buildArguments.putIfAbsent( option.replaceAll('build-', ''), () => value, ); } for (var arg in flutterBuildArgs?.split(',') ?? []) { if (arg.split('=').length == 2) { buildArguments.putIfAbsent( arg.split('=').first, () => arg.split('=').last, ); } else if (arg.split('=').length == 1) { buildArguments.putIfAbsent( arg.split('=')[0], () => true, ); } else { buildArguments.putIfAbsent(arg, () => true); } } return buildArguments; } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/cli/command_publish.dart ================================================ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:unified_distributor/src/extensions/string.dart'; import 'package:unified_distributor/src/unified_distributor.dart'; /// Publish an application to a third party provider /// /// This command wrapper defines, parses and transforms all passed arguments, /// so that they may be passed to `unified_distributor`. The distributor will /// then publish an application bundle using `flutter_app_publisher`. class CommandPublish extends Command { CommandPublish(this.distributor) { argParser.addOption( 'path', valueHelp: '', help: 'The path to the application bundle to publish.', ); argParser.addOption( 'targets', aliases: ['target'], valueHelp: [ 'appcenter', 'appstore', 'fir', 'firebase', 'github', 'playstore', 'pgyer', 'qiniu', 'vercel', ].join(','), help: 'The target provider(s) to publish to.', ); // AppCenter argParser.addSeparator('appcenter'); argParser.addOption( 'appcenter-owner-name', valueHelp: '', help: 'The owner name for appcenter.', ); argParser.addOption( 'appcenter-app-name', valueHelp: '', help: 'The app name for appcenter.', ); argParser.addOption( 'appcenter-distribution-group', valueHelp: '', help: 'The distribution group for appcenter.', ); // Firebase argParser.addSeparator('firebase'); argParser.addOption( 'firebase-app', valueHelp: '', help: [ 'The unique ID of the application on Firebase.', 'This is NOT your bundle identifier', ].join('\n'), ); argParser.addOption( 'firebase-release-notes', valueHelp: '', help: 'The release notes for the published application.', ); argParser.addOption( 'firebase-release-notes-file', valueHelp: '', help: [ 'The path of a file containing the release notes', 'This is a more extensive alternative to firebase-release-notes', ].join('\n'), ); argParser.addOption( 'firebase-testers', valueHelp: '', help: 'The testers that will be notified about the published application.', ); argParser.addOption( 'firebase-testers-file', valueHelp: '', help: [ 'The path of a file containing testers that will be notified', 'This is a more extensive alternative to firebase-testers', ].join('\n'), ); argParser.addOption( 'firebase-groups', valueHelp: '', help: 'The groups that will be notified about the published application.', ); argParser.addOption( 'firebase-groups-file', valueHelp: '', help: [ 'The path of a file containing groups that will be notified', 'This is a more extensive alternative to firebase-groups', ].join('\n'), ); // Firebase Hosting argParser.addSeparator('firebase-hosting'); argParser.addOption('firebase-hosting-project-id', valueHelp: ''); // Github argParser.addSeparator('github'); argParser.addOption( 'github-repo-owner', valueHelp: '', help: 'The name of the target GitHub repository wner (namespace)', ); argParser.addOption( 'github-repo-name', valueHelp: '', help: 'The name of the target GitHub repository', ); argParser.addOption( 'github-release-title', valueHelp: '', help: 'The title of the new release on GitHub', ); // PlayStore argParser.addSeparator('playstore'); argParser.addOption('playstore-package-name', valueHelp: ''); argParser.addOption('playstore-track', valueHelp: ''); // Qiniu argParser.addSeparator('qiniu'); argParser.addOption('qiniu-bucket', valueHelp: ''); argParser.addOption('qiniu-bucket-domain', valueHelp: ''); argParser.addOption('qiniu-savekey-prefix', valueHelp: ''); // Vercel argParser.addSeparator('vercel'); argParser.addOption('vercel-org-id', valueHelp: ''); argParser.addOption('vercel-project-id', valueHelp: ''); } final UnifiedDistributor distributor; @override String get name => 'publish'; @override String get description => [ 'Publish the built Flutter application artifacts to distribution platforms', '', 'This command uploads your application bundle to specified target providers', 'Use --targets to specify one or more distribution platforms', ].join('\n'); @override Future run() async { String? path = argResults?['path']; List targets = '${argResults?['targets'] ?? ''}' .split(',') .where((t) => t.isNotEmpty) .toList(); // At least `path` and one `targets` is required for flutter build if (path == null) { print('\nThe \'path\' options is mandatory!'.red(bold: true)); exit(1); } print(targets); if (targets.isEmpty) { print('\nAt least one \'target\' must be specified!'.red(bold: true)); exit(1); } // Required parameters for firebase if (targets.contains('firebase')) { if (argResults?['firebase-app'] == null) { print('\nFirebase app identifier is required for target \'firebase\''); exit(1); } } Map publishArguments = { 'appcenter-owner-name': argResults?['appcenter-owner-name'], 'appcenter-app-name': argResults?['appcenter-app-name'], 'appcenter-distribution-group': argResults?['appcenter-distribution-group'], 'firebase-app': argResults?['firebase-app'], 'firebase-release-notes': argResults?['firebase-release-notes'], 'firebase-release-notes-file': argResults?['firebase-release-notes-file'], 'firebase-testers': argResults?['firebase-testers'], 'firebase-testers-file': argResults?['firebase-testers-file'], 'firebase-groups': argResults?['firebase-groups'], 'firebase-groups-file': argResults?['firebase-groups-file'], 'firebase-hosting-project-id': argResults?['firebase-hosting-project-id'], 'github-repo-owner': argResults?['github-repo-owner'], 'github-repo-name': argResults?['github-repo-name'], 'github-release-title': argResults?['github-release-title'], 'playstore-package-name': argResults?['playstore-package-name'], 'playstore-track': argResults?['playstore-track'], 'qiniu-bucket': argResults?['qiniu-bucket'], 'qiniu-bucket-domain': argResults?['qiniu-bucket-domain'], 'qiniu-savekey-prefix': argResults?['qiniu-savekey-prefix'], 'vercel-org-id': argResults?['vercel-org-id'], 'vercel-project-id': argResults?['vercel-project-id'], }..removeWhere((key, value) => value == null); final fileSystemEntity = await FileSystemEntity.type(path) == FileSystemEntityType.directory ? Directory(path) : File(path); return distributor.publish( fileSystemEntity, targets, publishArguments: publishArguments, ); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/cli/command_release.dart ================================================ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:unified_distributor/src/extensions/string.dart'; import 'package:unified_distributor/src/unified_distributor.dart'; /// Release (package and publish) an application based on the config /// /// This command wrapper defines, parses and transforms all passed arguments, /// so that they may be passed to `unified_distributor`. The distributor will /// then use the `distribute_options.yaml` file in the Flutter project root /// to run a release with one or multiple release jobs. /// /// Each release job will package and optionally also publish the application /// based on the configuration on `distribute_options.yaml`. class CommandRelease extends Command { CommandRelease(this.distributor) { argParser.addOption( 'name', valueHelp: '', help: 'The name of the release to run.', ); argParser.addOption( 'jobs', valueHelp: '', help: 'Comma-separated list of jobs to run for the specified release.', ); argParser.addOption( 'skip-jobs', valueHelp: '', help: 'Comma-separated list of jobs to skip for the specified release.', ); argParser.addFlag( 'skip-clean', help: 'Whether or not to skip \'flutter clean\' before packaging.', ); } final UnifiedDistributor distributor; @override String get name => 'release'; @override String get description => 'Release the current Flutter application'; @override Future run() async { String? name = argResults?['name'] ?? ''; List jobNameList = (argResults?['jobs'] ?? '') .split(',') .where((String e) => e.isNotEmpty) .toList(); List skipJobNameList = (argResults?['skip-jobs'] ?? '') .split(',') .where((String e) => e.isNotEmpty) .toList(); bool isSkipClean = argResults?.wasParsed('skip-clean') ?? false; // At least `name` must be passed to select a release if (name == null) { print('\nThe \'name\' options is mandatory!'.red(bold: true)); exit(1); } return distributor.release( name, jobNameList: jobNameList, skipJobNameList: skipJobNameList, cleanBeforeBuild: !isSkipClean, ); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/cli/command_upgrade.dart ================================================ import 'package:args/command_runner.dart'; import 'package:unified_distributor/src/unified_distributor.dart'; /// Upgrade unified_distributor to the latest version class CommandUpgrade extends Command { CommandUpgrade(this.distributor); final UnifiedDistributor distributor; @override String get name => 'upgrade'; @override String get description => 'Update ${distributor.displayName} to the latest version.'; @override Future run() async { await distributor.upgrade(); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/distribute_options.dart ================================================ import 'dart:io'; import 'package:unified_distributor/src/release.dart'; class DistributeOptions { DistributeOptions({ required this.output, this.variables, this.artifactName, required this.releases, }); factory DistributeOptions.fromJson(Map json) { Map variables = {}; if (json.containsKey('variables') && json['variables'] != null) { variables = Map.from(json['variables']); // 兼容老版本 } else if (json.containsKey('env') && json['env'] != null) { variables = Map.from(json['env']); } List releases = ((json['releases'] ?? []) as List) .map((item) => Release.fromJson(item)) .toList(); return DistributeOptions( output: json['output'], variables: variables, artifactName: json['artifact_name'], releases: releases, ); } final String output; final Map? variables; final String? artifactName; final List releases; Directory get outputDirectory => Directory(output); Map toJson() { return { 'output': output, 'variables': variables, 'artifact_name': artifactName, 'releases': releases.map((e) => e.toJson()).toList(), }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/extensions/string.dart ================================================ import 'package:ansicolor/ansicolor.dart'; AnsiPen _ansiPen = AnsiPen(); const ansiResetForeground = '${ansiEscape}39m'; extension StringExt on String { String _applyColor(int color, {bool bg = false, bool bold = false}) { String appliedColor = (_ansiPen..xterm(color, bg: bg))(this); if (bold) { return '${ansiEscape}1m$appliedColor$ansiDefault'; } return appliedColor; } String black({bool bg = false, bool bold = false}) { return _applyColor(0, bg: bg, bold: bold); } String red({bool bg = false, bool bold = false}) { return _applyColor(1, bg: bg, bold: bold); } String green({bool bg = false, bool bold = false}) { return _applyColor(2, bg: bg, bold: bold); } String yellow({bool bg = false, bool bold = false}) { return _applyColor(3, bg: bg, bold: bold); } String blue({bool bg = false, bool bold = false}) { return _applyColor(4, bg: bg, bold: bold); } String magenta({bool bg = false, bool bold = false}) { return _applyColor(5, bg: bg, bold: bold); } String cyan({bool bg = false, bool bold = false}) { return _applyColor(6, bg: bg, bold: bold); } String white({bool bg = false, bool bold = false}) { return _applyColor(7, bg: bg, bold: bold); } String brightBlack({bool bg = false, bool bold = false}) { return _applyColor(8, bg: bg, bold: bold); } String brightRed({bool bg = false, bool bold = false}) { return _applyColor(9, bg: bg, bold: bold); } String brightGreen({bool bg = false, bool bold = false}) { return _applyColor(10, bg: bg, bold: bold); } String brightYellow({bool bg = false, bool bold = false}) { return _applyColor(11, bg: bg, bold: bold); } String brightBlue({bool bg = false, bool bold = false}) { return _applyColor(12, bg: bg, bold: bold); } String brightMagenta({bool bg = false, bool bold = false}) { return _applyColor(13, bg: bg, bold: bold); } String brightCyan({bool bg = false, bool bold = false}) { return _applyColor(14, bg: bg, bold: bold); } String brightWhite({bool bg = false, bool bold = false}) { return _applyColor(15, bg: bg, bold: bold); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/release.dart ================================================ import 'package:unified_distributor/src/release_job.dart'; class Release { Release({ this.variables, required this.name, required this.jobs, }); factory Release.fromJson(Map json) { Map variables = {}; if (json.containsKey('variables') && json['variables'] != null) { variables = Map.from(json['variables']); } List jobs = (json['jobs'] as List? ?? []) .map((item) => ReleaseJob.fromJson(item)) .toList(); return Release( variables: variables, name: json['name'], jobs: jobs, ); } final Map? variables; final String name; final List jobs; Map toJson() { return { 'variables': variables, 'name': name, 'jobs': jobs.map((e) => e.toJson()).toList(), }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/release_job.dart ================================================ class ReleaseJobPackage { ReleaseJobPackage({ required this.platform, required this.target, this.channel, this.buildArgs, }); factory ReleaseJobPackage.fromJson(Map json) { return ReleaseJobPackage( platform: json['platform'], target: json['target'], channel: json['channel'], buildArgs: json['build_args'], ); } final String platform; final String target; final String? channel; final Map? buildArgs; Map toJson() { return { 'platform': platform, 'target': target, 'channel': channel, 'build_args': buildArgs, }..removeWhere((key, value) => value == null); } } class ReleaseJobPublish { ReleaseJobPublish({ required this.target, this.args, }); factory ReleaseJobPublish.fromJson(Map json) { return ReleaseJobPublish( target: json['target'], args: json['args'], ); } final String target; final Map? args; Map toJson() { return { 'target': target, 'args': args, }..removeWhere((key, value) => value == null); } } class ReleaseJob { ReleaseJob({ this.variables, required this.name, required this.package, this.publish, this.publishTo, }); factory ReleaseJob.fromJson(Map json) { Map variables = {}; if (json.containsKey('variables') && json['variables'] != null) { variables = Map.from(json['variables']); } return ReleaseJob( variables: variables, name: json['name'], package: ReleaseJobPackage.fromJson(json['package']), publish: json['publish'] != null ? ReleaseJobPublish.fromJson(json['publish']) : null, publishTo: json['publish_to'], ); } final Map? variables; final String name; final ReleaseJobPackage package; final ReleaseJobPublish? publish; final String? publishTo; Map toJson() { return { 'variables': variables, 'name': name, 'package': package.toJson(), 'publish': publish?.toJson(), 'publish_to': publishTo, }..removeWhere((key, value) => value == null); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/unified_distributor.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_app_builder/flutter_app_builder.dart'; import 'package:flutter_app_packager/flutter_app_packager.dart'; import 'package:flutter_app_publisher/flutter_app_publisher.dart'; import 'package:path/path.dart' as p; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:shell_executor/shell_executor.dart'; import 'package:shell_uikit/shell_uikit.dart'; import 'package:unified_distributor/src/check_version_result.dart'; import 'package:unified_distributor/src/distribute_options.dart'; import 'package:unified_distributor/src/extensions/string.dart'; import 'package:unified_distributor/src/release.dart'; import 'package:unified_distributor/src/release_job.dart'; import 'package:unified_distributor/src/utils/default_shell_executor.dart'; import 'package:unified_distributor/src/utils/logger.dart'; import 'package:unified_distributor/src/utils/pub_dev_api.dart'; import 'package:yaml/yaml.dart'; /// A class that provides a unified interface for distributing applications /// across different platforms. class UnifiedDistributor { /// Creates a new instance of the [UnifiedDistributor] class. /// /// The [packageName] parameter is the name of the package to be distributed. /// The [displayName] parameter is the display name of the package. UnifiedDistributor( this.packageName, this.displayName, ) { ShellExecutor.global = DefaultShellExecutor(); } /// The name of the package final String packageName; /// The display name of the package final String displayName; final FlutterAppBuilder _builder = FlutterAppBuilder(); final FlutterAppPackager _packager = FlutterAppPackager(); final FlutterAppPublisher _publisher = FlutterAppPublisher(); Pubspec? _pubspec; Pubspec get pubspec { if (_pubspec == null) { final yamlString = File('pubspec.yaml').readAsStringSync(); _pubspec = Pubspec.parse(yamlString); } return _pubspec!; } final Map _globalVariables = {}; Map get globalVariables { if (_globalVariables.keys.isEmpty) { for (String key in Platform.environment.keys) { String? value = Platform.environment[key]; if ((value ?? '').isNotEmpty) { _globalVariables[key] = value!; } } List keys = distributeOptions.variables?.keys.toList() ?? []; for (String key in keys) { String? value = distributeOptions.variables?[key]; if ((value ?? '').isNotEmpty) { _globalVariables[key] = value!; } } } return _globalVariables; } DistributeOptions? _distributeOptions; /// Get the distribute options DistributeOptions get distributeOptions { if (_distributeOptions == null) { File file = File('distribute_options.yaml'); if (file.existsSync()) { final yamlString = File('distribute_options.yaml').readAsStringSync(); final yamlDoc = loadYaml(yamlString); _distributeOptions = DistributeOptions.fromJson( json.decode(json.encode(yamlDoc)), ); } else { _distributeOptions = DistributeOptions( output: 'dist/', releases: [], ); } } return _distributeOptions!; } Future _getCurrentVersion() async { try { var scriptFile = Platform.script.toFilePath(); var pathToPubSpecYaml = p.join(p.dirname(scriptFile), '../pubspec.yaml'); var pathToPubSpecLock = p.join(p.dirname(scriptFile), '../pubspec.lock'); var pubSpecYamlFile = File(pathToPubSpecYaml); var pubSpecLockFile = File(pathToPubSpecLock); if (pubSpecLockFile.existsSync()) { var yamlDoc = loadYaml(await pubSpecLockFile.readAsString()); if (yamlDoc['packages'][packageName] == null) { var yamlDoc = loadYaml(await pubSpecYamlFile.readAsString()); return yamlDoc['version']; } return yamlDoc['packages'][packageName]['version']; } } catch (_) {} return null; } /// Check the version of the package /// /// This method checks the version of the package against the latest version /// available on pub.dev. Future checkVersion() async { String? currentVersion = await _getCurrentVersion(); String? latestVersion = await PubDevApi.getLatestVersionFromPackage(packageName); return CheckVersionResult( currentVersion: currentVersion, latestVersion: latestVersion, ); } /// Get the current version of the package Future getCurrentVersion() async { return await _getCurrentVersion(); } /// Package an application for a specific platform and target Future> package( String platform, List targets, { String? channel, String? artifactName, required bool cleanBeforeBuild, required Map buildArguments, Map? variables, }) async { List makeResultList = []; try { Directory outputDirectory = distributeOptions.outputDirectory; if (!outputDirectory.existsSync()) { outputDirectory.createSync(recursive: true); } if (cleanBeforeBuild) { await _builder.clean(); } bool isBuildOnlyOnce = platform != 'android'; BuildResult? buildResult; for (String target in targets) { logger.info('Packaging ${pubspec.name} ${pubspec.version} as $target:'); if (!isBuildOnlyOnce || (isBuildOnlyOnce && buildResult == null)) { try { buildResult = await _builder.build( platform, target: target, arguments: buildArguments, environment: variables ?? globalVariables, ); print( const JsonEncoder.withIndent(' ').convert(buildResult.toJson()), ); logger.info( 'Successfully built ${buildResult.outputDirectory} in ${buildResult.duration!.inSeconds}s' .brightGreen(), ); } on UnsupportedError catch (error) { logger.warning('Warning: ${error.message}'.yellow()); continue; } catch (error) { rethrow; } } if (buildResult != null) { String buildMode = buildArguments.containsKey('profile') ? 'profile' : 'release'; Map? arguments = { 'build_mode': buildMode, 'flavor': buildArguments['flavor'], 'channel': channel, 'artifact_name': artifactName, }; MakeResult makeResult = await _packager.package( platform, target, arguments, outputDirectory, buildOutputDirectory: buildResult.outputDirectory, buildOutputFiles: buildResult.outputFiles, ); print( const JsonEncoder.withIndent(' ').convert(makeResult.toJson()), ); FileSystemEntity artifact = makeResult.artifacts.first; logger.info( 'Successfully packaged ${artifact.path}'.brightGreen(), ); makeResultList.add(makeResult); } } } catch (error) { logger.severe(error.toString().red()); if (error is Error) { logger.severe(error.stackTrace.toString().red()); } rethrow; } return makeResultList; } /// Publish an application to a third party provider /// /// This method publishes an application to a third party provider. Future> publish( FileSystemEntity fileSystemEntity, List targets, { Map? publishArguments, Map? variables, }) async { List publishResultList = []; try { for (String target in targets) { ProgressBar progressBar = ProgressBar( format: 'Publishing to $target: {bar} {value}/{total} {percentage}%', ); Map? newPublishArguments = {}; if (publishArguments != null) { for (var key in publishArguments.keys) { if (!key.startsWith('$target-')) continue; dynamic value = publishArguments[key]; if (value is List) { // ignore: prefer_for_elements_to_map_fromiterable value = Map.fromIterable( value, key: (e) => e.split('=')[0], value: (e) => e.split('=')[1], ); } newPublishArguments.putIfAbsent( key.replaceAll('$target-', ''), () => value, ); } } if (newPublishArguments.keys.isEmpty) { newPublishArguments = publishArguments; } PublishResult publishResult = await _publisher.publish( fileSystemEntity, target: target, environment: variables ?? globalVariables, publishArguments: newPublishArguments, onPublishProgress: (sent, total) { if (!progressBar.isActive) { progressBar.start(total, sent); } else { progressBar.update(sent); } }, ); if (progressBar.isActive) progressBar.stop(); logger.info( 'Successfully published ${publishResult.url}'.brightGreen(), ); publishResultList.add(publishResult); } } on Error catch (error) { logger.severe(error.toString().brightRed()); logger.severe(error.stackTrace.toString().brightRed()); rethrow; } return publishResultList; } /// Release an application to a third party provider /// /// This method releases an application to a third party provider. /// /// The [name] parameter is the name of the release to be released. /// The [jobNameList] parameter is the list of job names to be released. /// The [skipJobNameList] parameter is the list of job names to be skipped. /// The [cleanBeforeBuild] parameter is a boolean that indicates whether to clean the build directory before building. Future release( String name, { required List jobNameList, required List skipJobNameList, required bool cleanBeforeBuild, }) async { final time = Stopwatch()..start(); try { Directory outputDirectory = distributeOptions.outputDirectory; if (!outputDirectory.existsSync()) { outputDirectory.createSync(recursive: true); } List releases = distributeOptions.releases; if (name.isNotEmpty) { releases = distributeOptions.releases.where((e) => e.name == name).toList(); } if (releases.isEmpty) { throw Exception('Missing/incomplete `distribute_options.yaml` file.'); } for (Release release in releases) { List filteredJobs = release.jobs.where((e) { if (jobNameList.isNotEmpty) { return jobNameList.contains(e.name); } if (skipJobNameList.isNotEmpty) { return !skipJobNameList.contains(e.name); } return true; }).toList(); if (filteredJobs.isEmpty) { throw Exception('No available jobs found in ${release.name}.'); } bool needCleanBeforeBuild = cleanBeforeBuild; for (ReleaseJob job in filteredJobs) { logger.info(''); logger.info( '${'===>'.blue()} ${'Releasing'.white(bold: true)} $name:${job.name.green(bold: true)}', ); Map variables = {} ..addAll(globalVariables) ..addAll(release.variables ?? {}) ..addAll(job.variables ?? {}); List makeResultList = await package( job.package.platform, [job.package.target], channel: job.package.channel, artifactName: distributeOptions.artifactName, cleanBeforeBuild: needCleanBeforeBuild, buildArguments: job.package.buildArgs ?? {}, variables: variables, ); // Clean only once needCleanBeforeBuild = false; if (job.publish != null || job.publishTo != null) { String? publishTarget = job.publishTo ?? job.publish?.target; MakeResult makeResult = makeResultList.first; FileSystemEntity artifact = makeResult.artifacts.first; await publish( artifact, [publishTarget!], publishArguments: job.publish?.args, variables: variables, ); } } } time.stop(); logger.info(''); logger.info( 'RELEASE SUCCESSFUL in ${time.elapsed.inSeconds}s'.green(bold: true), ); } catch (error, stacktrace) { time.stop(); logger.info(''); logger.severe( [ 'RELEASE FAILED in ${time.elapsed.inSeconds}s'.red(bold: true), error.toString().red(), stacktrace, ].join('\n'), ); rethrow; } return Future.value(); } /// Upgrade the package to the latest version Future upgrade() async { await $( 'dart', ['pub', 'global', 'activate', packageName], ); return Future.value(); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/utils/default_shell_executor.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:charset/charset.dart'; import 'package:shell_executor/shell_executor.dart'; import 'package:unified_distributor/src/extensions/string.dart'; import 'package:unified_distributor/src/utils/logger.dart'; /// Convert bytes to string (UTF-8 or detected charset) String convertToString(List bytes) { if (Platform.isWindows) { final charset = Charset.detect(bytes); if (charset != null) { return charset.decode(bytes); } } return utf8.decode(bytes, allowMalformed: true); } class DefaultShellExecutor extends ShellExecutor { @override Future exec( String executable, List arguments, { String? workingDirectory, Map? environment, }) async { final Process process = await Process.start( executable, arguments, workingDirectory: workingDirectory, environment: environment, runInShell: true, ); logger.info('\$ $executable ${arguments.join(' ')}'.brightBlack()); String? stdoutStr; String? stderrStr; process.stdout.listen((data) { String msg = convertToString(data); stdoutStr = '${stdoutStr ?? ''}$msg'; stdout.write(msg.brightBlack()); }); process.stderr.listen((data) { String msg = convertToString(data); stderrStr = '${stderrStr ?? ''}$msg'; stderr.write(msg.brightRed()); }); int exitCode = await process.exitCode; return ProcessResult(process.pid, exitCode, stdoutStr, stderrStr); } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/utils/logger.dart ================================================ import 'package:logging/logging.dart'; Logger logger = Logger('unified_distributor') ..onRecord.listen((record) { print(record.message); }); ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/src/utils/pub_dev_api.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; class PubDevApi { static Future getLatestVersionFromPackage(String package) async { String pubHostedUrl = Platform.environment['PUB_HOSTED_URL'] ?? ''; final pubSite = pubHostedUrl.isNotEmpty ? '$pubHostedUrl/api/packages/$package' : 'https://pub.dev/api/packages/$package'; final uri = Uri.parse(pubSite); try { final response = await Dio().get(uri.toString()); Map data = {}; if (response.data is String) { data = json.decode(response.data); } else { data = response.data; } if (data['latest'] == null) return null; return data['latest']['version'] as String?; } catch (error) { rethrow; } } } ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/lib/unified_distributor.dart ================================================ library unified_distributor; export 'src/check_version_result.dart'; export 'src/cli/cli.dart'; export 'src/distribute_options.dart'; export 'src/extensions/string.dart'; export 'src/unified_distributor.dart'; export 'src/utils/default_shell_executor.dart'; export 'src/utils/logger.dart'; ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/pubspec.yaml ================================================ name: unified_distributor description: A powerful and efficient tool for packaging and publishing your applications with ease. version: 0.2.0 homepage: https://fastforge.dev repository: https://github.com/fastforgedev/fastforge/tree/main/packages/unified_distributor issue_tracker: https://github.com/fastforgedev/fastforge/issues platforms: linux: macos: windows: environment: sdk: ">=2.16.0 <4.0.0" dependencies: ansicolor: ^2.0.1 args: ^2.2.0 charset: ^2.0.1 dio: ^5.3.4 flutter_app_builder: ^0.6.0 flutter_app_packager: ^0.6.0 flutter_app_publisher: ^0.6.0 logging: ^1.0.2 path: ^1.8.1 pubspec_parse: ^1.1.0 shell_executor: ^0.3.0 shell_uikit: ^0.3.0 yaml: ^3.1.0 dev_dependencies: dependency_validator: ^3.0.0 mostly_reasonable_lints: ^0.1.2 test: ^1.23.1 ================================================ FILE: plugins/flutter_distributor/packages/unified_distributor/test/src/extensions/string_test.dart ================================================ import 'package:test/test.dart'; import 'package:unified_distributor/src/extensions/string.dart'; void main() { // print('black '.black()); // print('red '.red()); // print('green '.green()); // print('yellow '.yellow()); // print('blue '.blue()); // print('magenta '.magenta()); // print('cyan '.cyan()); // print('white '.white()); // print('brightBlack '.brightBlack()); // print('brightRed '.brightRed()); // print('brightGreen '.brightGreen()); // print('brightYellow '.brightYellow()); // print('brightBlue '.brightBlue()); // print('brightMagenta'.brightMagenta()); // print('BrightCyan '.brightCyan()); // print('brightWhite '.brightWhite()); // print('black '.black(bold: true)); // print('red '.red(bold: true)); // print('green '.green(bold: true)); // print('yellow '.yellow(bold: true)); // print('blue '.blue(bold: true)); // print('magenta '.magenta(bold: true)); // print('cyan '.cyan(bold: true)); // print('white '.white(bold: true)); // print('brightBlack '.brightBlack(bold: true)); // print('brightRed '.brightRed(bold: true)); // print('brightGreen '.brightGreen(bold: true)); // print('brightYellow '.brightYellow(bold: true)); // print('brightBlue '.brightBlue(bold: true)); // print('brightMagenta'.brightMagenta(bold: true)); // print('BrightCyan '.brightCyan(bold: true)); // print('brightWhite '.brightWhite(bold: true)); // print('black '.black(bg: true)); // print('red '.red(bg: true)); // print('green '.green(bg: true)); // print('yellow '.yellow(bg: true)); // print('blue '.blue(bg: true)); // print('magenta '.magenta(bg: true)); // print('cyan '.cyan(bg: true)); // print('white '.white(bg: true)); // print('brightBlack '.brightBlack(bg: true)); // print('brightRed '.brightRed(bg: true)); // print('brightGreen '.brightGreen(bg: true)); // print('brightYellow '.brightYellow(bg: true)); // print('brightBlue '.brightBlue(bg: true)); // print('brightMagenta'.brightMagenta(bg: true)); // print('BrightCyan '.brightCyan(bg: true)); // print('brightWhite '.brightWhite(bg: true)); // print('black '.black(bold: true, bg: true)); // print('red '.red(bold: true, bg: true)); // print('green '.green(bold: true, bg: true)); // print('yellow '.yellow(bold: true, bg: true)); // print('blue '.blue(bold: true, bg: true)); // print('magenta '.magenta(bold: true, bg: true)); // print('cyan '.cyan(bold: true, bg: true)); // print('white '.white(bold: true, bg: true)); // print('brightBlack '.brightBlack(bold: true, bg: true)); // print('brightRed '.brightRed(bold: true, bg: true)); // print('brightGreen '.brightGreen(bold: true, bg: true)); // print('brightYellow '.brightYellow(bold: true, bg: true)); // print('brightBlue '.brightBlue(bold: true, bg: true)); // print('brightMagenta'.brightMagenta(bold: true, bg: true)); // print('BrightCyan '.brightCyan(bold: true, bg: true)); // print('brightWhite '.brightWhite(bold: true, bg: true)); group('StringExt', () { test('black', () { expect('test'.red(), 'test'); }); }); } ================================================ FILE: plugins/flutter_distributor/pubspec.yaml ================================================ name: flutter_distributor_workspace publish_to: "none" environment: sdk: ">=2.16.0 <4.0.0" dev_dependencies: flutter_lints: ^2.0.0 melos: ^3.1.0 ================================================ FILE: plugins/flutter_distributor/website/.gitignore ================================================ # build output dist/ # generated types .astro/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS-specific files .DS_Store .vercel ================================================ FILE: plugins/flutter_distributor/website/astro.config.mjs ================================================ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; import tailwind from "@astrojs/tailwind"; import partytown from "@astrojs/partytown"; const googleAnalyticsId = "G-EC75P3JKR5"; // https://astro.build/config export default defineConfig({ integrations: [ starlight({ title: "Flutter Distributor", logo: { src: "./src/assets/logo.png", }, editLink: { baseUrl: "https://github.com/leanflutter/flutter_distributor/tree/main/website/", }, defaultLocale: "root", locales: { root: { label: "English", lang: "en", }, "zh-hans": { label: "简体中文", lang: "zh-hans", }, }, social: { github: "https://github.com/leanflutter/flutter_distributor", discord: "https://discord.com/invite/zPa6EZ2jqb", }, sidebar: [ { label: "Guides", translations: { "zh-hans": "指南" }, items: [ { label: "Getting started", link: "/getting-started/", translations: { "zh-hans": "开始" }, }, { label: "Distribute Options", link: "/distribute-options/", translations: { "zh-hans": "分发选项" }, }, { label: "CLI", link: "/cli/" }, ], }, { label: "Makers", translations: { "zh-hans": "制作器" }, items: [ { label: "aab", link: "/makers/aab/" }, { label: "apk", link: "/makers/apk/" }, { label: "appimage", link: "/makers/appimage/" }, { label: "deb", link: "/makers/deb/" }, { label: "dmg", link: "/makers/dmg/" }, { label: "exe", link: "/makers/exe/" }, { label: "ipa", link: "/makers/ipa/" }, { label: "msix", link: "/makers/msix/" }, { label: "pkg", link: "/makers/pkg/" }, { label: "rpm", link: "/makers/rpm/" }, { label: "zip", link: "/makers/zip/" }, ], }, { label: "Publishers", translations: { "zh-hans": "发布器" }, items: [ { label: "appcenter", link: "/publishers/appcenter/" }, { label: "appstore", link: "/publishers/appstore/" }, { label: "fir", link: "/publishers/fir/" }, { label: "firebase-hosting", link: "/publishers/firebase-hosting/", }, { label: "firebase", link: "/publishers/firebase/" }, { label: "github", link: "/publishers/github/" }, { label: "pgyer", link: "/publishers/pgyer/" }, { label: "playstore", link: "/publishers/playstore/" }, { label: "qiniu", link: "/publishers/qiniu/" }, { label: "vercel", link: "/publishers/vercel/" }, ], }, { label: "Tools", translations: { "zh-hans": "工具" }, items: [ { label: "Parse App Package", link: "/tools/parse-app-package/" }, ], }, ], components: { TableOfContents: "./src/components/starlight/TableOfContents.astro", }, head: [ { tag: "script", attrs: { type: "text/partytown", src: `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`, async: true, }, }, { tag: "script", attrs: { type: "text/partytown", }, content: ` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '${googleAnalyticsId}'); `, }, { tag: "script", attrs: { src: `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6049036475236211`, async: true, crossorigin: "anonymous", }, }, ], }), tailwind({ // Disable the default base styles: applyBaseStyles: false, }), partytown({ // Adds dataLayer.push as a forwarding-event. config: { forward: ["dataLayer.push"], }, }), ], }); ================================================ FILE: plugins/flutter_distributor/website/package.json ================================================ { "name": "website", "type": "module", "version": "0.0.1", "scripts": { "dev": "astro dev", "start": "astro dev", "build": "astro check && astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/check": "^0.3.2", "@astrojs/partytown": "^2.0.4", "@astrojs/starlight": "^0.15.0", "@astrojs/starlight-tailwind": "^2.0.1", "@astrojs/tailwind": "^5.1.0", "astro": "^4.5.12", "sharp": ">=0.32.6", "tailwindcss": "^3.4.1", "typescript": "^5.3.3" } } ================================================ FILE: plugins/flutter_distributor/website/src/components/starlight/TableOfContents.astro ================================================ --- import type { Props } from "@astrojs/starlight/props"; import AstrolightTableOfContents from "@astrojs/starlight/components/TableOfContents.astro"; ---
================================================ FILE: plugins/flutter_distributor/website/src/content/config.ts ================================================ import { defineCollection } from 'astro:content'; import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; export const collections = { docs: defineCollection({ schema: docsSchema() }), i18n: defineCollection({ type: 'data', schema: i18nSchema() }), }; ================================================ FILE: plugins/flutter_distributor/website/src/content/docs/index.mdx ================================================ --- title: Flutter Distributor description: An all-in-one Flutter application packaging and distribution tool, providing you with a one-stop solution to meet various distribution needs. template: splash hero: tagline: An all-in-one Flutter application packaging and distribution tool, providing you with a one-stop solution to meet various distribution needs. actions: - text: Getting Started link: /getting-started/ icon: right-arrow variant: primary - text: GitHub link: https://github.com/leanflutter/flutter_distributor icon: external --- import { Card, CardGrid } from "@astrojs/starlight/components"; Build platform-specific distributable files such as APK, IPA, and desktop installation packages. Support major app stores and internal testing distribution platforms such as `Google Play Store` and `Apple App Store`, simplifying the publishing process. Flexibly customize the packaging and publishing process through simple yet powerful configuration options. Actively maintained to adapt to the latest Flutter framework and platform requirements at any time. ================================================ FILE: plugins/flutter_distributor/website/src/content/docs/zh-hans/index.mdx ================================================ --- title: Flutter Distributor description: 一款全能的 Flutter 应用打包和发布工具,为您提供一站式解决方案,满足各种分发需求。 template: splash hero: tagline: 一款全能的 Flutter 应用打包和发布工具,为您提供一站式解决方案,满足各种分发需求。 actions: - text: 开始使用 link: /zh-hans/getting-started/ icon: right-arrow variant: primary - text: GitHub link: https://github.com/leanflutter/flutter_distributor icon: external --- import { Card, CardGrid } from "@astrojs/starlight/components"; 构建 APK、IPA、桌面端安装包文件等特定于平台的可分发文件。 支持 `Google Play Store`、`Apple App Store` 等主流应用商店及内测分发平台,简化发布流程。 通过简单而强大的配置选项,灵活地定制打包和发布的过程。 活跃维护,随时适应最新的 Flutter 框架和平台要求。 ================================================ FILE: plugins/flutter_distributor/website/src/content/i18n/en.json ================================================ {} ================================================ FILE: plugins/flutter_distributor/website/src/content/i18n/zh-cn.json ================================================ {} ================================================ FILE: plugins/flutter_distributor/website/src/env.d.ts ================================================ /// /// declare let adsbygoogle: any; interface Window { adsbygoogle: any; } ================================================ FILE: plugins/flutter_distributor/website/src/tailwind.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: plugins/flutter_distributor/website/tailwind.config.js ================================================ import starlightPlugin from "@astrojs/starlight-tailwind"; /** @type {import('tailwindcss').Config} */ export default { content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], theme: { extend: {}, }, plugins: [starlightPlugin()], }; ================================================ FILE: plugins/flutter_distributor/website/tsconfig.json ================================================ { "extends": "astro/tsconfigs/strict" } ================================================ FILE: plugins/proxy/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. /pubspec.lock **/doc/api/ .dart_tool/ .packages build/ ================================================ FILE: plugins/proxy/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "e1e47221e86272429674bec4f1bd36acc4fc7b77" channel: "stable" project_type: plugin # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - platform: android create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - platform: windows create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: plugins/proxy/LICENSE ================================================ TODO: Add your license here. ================================================ FILE: plugins/proxy/analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: plugins/proxy/lib/proxy.dart ================================================ import 'dart:io'; import "package:path/path.dart"; import 'proxy_platform_interface.dart'; enum ProxyTypes { http, https, socks } class Proxy extends ProxyPlatform { static String url = "127.0.0.1"; @override Future startProxy( int port, [ List bypassDomain = const [], ]) async { return switch (Platform.operatingSystem) { "macos" => await _startProxyWithMacos(port, bypassDomain), "linux" => await _startProxyWithLinux(port, bypassDomain), "windows" => await ProxyPlatform.instance.startProxy(port, bypassDomain), String() => false, }; } @override Future stopProxy() async { return switch (Platform.operatingSystem) { "macos" => await _stopProxyWithMacos(), "linux" => await _stopProxyWithLinux(), "windows" => await ProxyPlatform.instance.stopProxy(), String() => false, }; } Future _startProxyWithLinux(int port, List bypassDomain) async { try { final homeDir = Platform.environment['HOME']!; final configDir = join(homeDir, ".config"); final cmdList = List>.empty(growable: true); final desktop = Platform.environment['XDG_CURRENT_DESKTOP']; final isKDE = desktop == "KDE"; if (isKDE) { cmdList.add( [ "kwriteconfig5", "--file", "$configDir/kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1" ], ); cmdList.add( [ "kwriteconfig5", "--file", "$configDir/kioslaverc", "--group", "Proxy Settings", "--key", "NoProxyFor", bypassDomain.join(",") ], ); } else { cmdList.add( ["gsettings", "set", "org.gnome.system.proxy", "mode", "manual"], ); final ignoreHosts = "\"['${bypassDomain.join("', '")}']\""; cmdList.add( [ "gsettings", "set", "org.gnome.system.proxy", "ignore-hosts", ignoreHosts ], ); } for (final type in ProxyTypes.values) { if (!isKDE) { cmdList.add( [ "gsettings", "set", "org.gnome.system.proxy.${type.name}", "host", url ], ); cmdList.add( [ "gsettings", "set", "org.gnome.system.proxy.${type.name}", "port", "$port" ], ); cmdList.add( [ "gsettings", "set", "org.gnome.system.proxy.${type.name}", "port", "$port" ], ); cmdList.add( [ "gsettings", "set", "org.gnome.system.proxy.${type.name}", "port", "$port" ], ); } if (isKDE) { cmdList.add( [ "kwriteconfig5", "--file", "$configDir/kioslaverc", "--group", "Proxy Settings", "--key", "${type.name}Proxy", "${type.name}://$url:$port" ], ); } } for (final cmd in cmdList) { await Process.run(cmd[0], cmd.sublist(1), runInShell: true); } return true; } catch (_) { return false; } } Future _stopProxyWithLinux() async { try { final homeDir = Platform.environment['HOME']!; final configDir = join(homeDir, ".config/"); final cmdList = List>.empty(growable: true); final desktop = Platform.environment['XDG_CURRENT_DESKTOP']; final isKDE = desktop == "KDE"; if (isKDE) { cmdList.add( [ "kwriteconfig5", "--file", "$configDir/kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0" ], ); } else { cmdList.add( ["gsettings", "set", "org.gnome.system.proxy", "mode", "none"], ); } for (final cmd in cmdList) { await Process.run(cmd[0], cmd.sublist(1)); } return true; } catch (_) { return false; } } Future _startProxyWithMacos(int port, List bypassDomain) async { try { final devices = await _getNetworkDeviceListWithMacos(); for (final dev in devices) { await Future.wait([ Process.run( "/usr/sbin/networksetup", ["-setwebproxystate", dev, "on"], ), Process.run( "/usr/sbin/networksetup", ["-setwebproxy", dev, url, "$port"], ), Process.run( "/usr/sbin/networksetup", ["-setsecurewebproxystate", dev, "on"], ), Process.run( "/usr/sbin/networksetup", ["-setsecurewebproxy", dev, url, "$port"], ), Process.run( "/usr/sbin/networksetup", ["-setsocksfirewallproxystate", dev, "on"], ), Process.run( "/usr/sbin/networksetup", ["-setsocksfirewallproxy", dev, url, "$port"], ), Process.run( "/usr/sbin/networksetup", [ "-setproxybypassdomains", dev, bypassDomain.join(","), ], ), ]); } return true; } catch (e) { return false; } } Future _stopProxyWithMacos() async { try { final devices = await _getNetworkDeviceListWithMacos(); for (final dev in devices) { await Future.wait([ Process.run( "/usr/sbin/networksetup", ["-setautoproxystate", dev, "off"], ), Process.run( "/usr/sbin/networksetup", ["-setwebproxystate", dev, "off"], ), Process.run( "/usr/sbin/networksetup", ["-setsecurewebproxystate", dev, "off"], ), Process.run( "/usr/sbin/networksetup", ["-setsocksfirewallproxystate", dev, "off"], ), Process.run( "/usr/sbin/networksetup", ["-setproxybypassdomains", dev, ""], ), ]); } return true; } catch (e) { return false; } } Future> _getNetworkDeviceListWithMacos() async { final res = await Process.run( "/usr/sbin/networksetup", ["-listallnetworkservices"]); final lines = res.stdout.toString().split("\n"); lines.removeWhere((element) => element.contains("*")); return lines; } } ================================================ FILE: plugins/proxy/lib/proxy_method_channel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'proxy_platform_interface.dart'; /// An implementation of [ProxyPlatform] that uses method channels. class MethodChannelProxy extends ProxyPlatform { /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('proxy'); MethodChannelProxy(); @override Future startProxy(int port, List bypassDomain) async { return await methodChannel.invokeMethod("StartProxy", { 'port': port, 'bypassDomain': bypassDomain, }); } @override Future stopProxy() async { return await methodChannel.invokeMethod("StopProxy"); } } ================================================ FILE: plugins/proxy/lib/proxy_platform_interface.dart ================================================ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'proxy_method_channel.dart'; abstract class ProxyPlatform extends PlatformInterface { /// Constructs a ProxyPlatform. ProxyPlatform() : super(token: _token); static final Object _token = Object(); static ProxyPlatform _instance = MethodChannelProxy(); /// The default instance of [ProxyPlatform] to use. /// /// Defaults to [MethodChannelProxy]. static ProxyPlatform get instance => _instance; static set instance(ProxyPlatform instance) { PlatformInterface.verifyToken(instance, _token); _instance = instance; } Future startProxy(int port, List bypassDomain) { throw UnimplementedError('startProxy() has not been implemented.'); } Future stopProxy() { throw UnimplementedError('stopProxy() has not been implemented.'); } } ================================================ FILE: plugins/proxy/pubspec.yaml ================================================ name: proxy description: A new Flutter plugin project. version: 0.0.1 homepage: environment: sdk: '>=3.1.0 <4.0.0' flutter: '>=3.3.0' dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.0.2 path: ^1.8.3 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # This section identifies this Flutter project as a plugin project. # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) # which should be registered in the plugin registry. This is required for # using method channels. # The Android 'package' specifies package in which the registered class is. # This is required for using method channels on Android. # The 'ffiPlugin' specifies that native code should be built and bundled. # This is required for using `dart:ffi`. # All these are used by the tooling to maintain consistency when # adding or updating assets for this project. plugin: platforms: windows: pluginClass: ProxyPluginCApi # To add assets to your plugin package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # # For details regarding assets in packages, see # https://flutter.dev/assets-and-images/#from-packages # # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # To add custom fonts to your plugin package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts in packages, see # https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: plugins/proxy/windows/.gitignore ================================================ flutter/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: plugins/proxy/windows/CMakeLists.txt ================================================ # The Flutter tooling requires that developers have a version of Visual Studio # installed that includes CMake 3.14 or later. You should not increase this # version, as doing so will cause the plugin to fail to compile for some # customers of the plugin. cmake_minimum_required(VERSION 3.14) # Project-level configuration. set(PROJECT_NAME "proxy") project(${PROJECT_NAME} LANGUAGES CXX) # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) # This value is used when generating builds using this plugin, so it must # not be changed set(PLUGIN_NAME "proxy_plugin") # Any new source files that you add to the plugin should be added here. list(APPEND PLUGIN_SOURCES "proxy_plugin.cpp" "proxy_plugin.h" ) # Define the plugin library target. Its name must not be changed (see comment # on PLUGIN_NAME above). add_library(${PLUGIN_NAME} SHARED "include/proxy/proxy_plugin_c_api.h" "proxy_plugin_c_api.cpp" ${PLUGIN_SOURCES} ) # Apply a standard set of build settings that are configured in the # application-level CMakeLists.txt. This can be removed for plugins that want # full control over build settings. apply_standard_settings(${PLUGIN_NAME}) # Symbols are hidden by default to reduce the chance of accidental conflicts # between plugins. This should not be removed; any symbols that should be # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) # Source include directories and library dependencies. Add any plugin-specific # dependencies here. target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an # external build triggered from this build file. set(proxy_bundled_libraries "" PARENT_SCOPE ) # === Tests === # These unit tests can be run from a terminal after building the example, or # from Visual Studio after opening the generated solution file. # Only enable test builds when building the example (which sets this variable) # so that plugin clients aren't building the tests. if (${include_${PROJECT_NAME}_tests}) set(TEST_RUNNER "${PROJECT_NAME}_test") enable_testing() # Add the Google Test dependency. include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/release-1.11.0.zip ) # Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Disable install commands for gtest so it doesn't end up in the bundle. set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) FetchContent_MakeAvailable(googletest) # The plugin's C API is not very useful for unit testing, so build the sources # directly into the test binary rather than using the DLL. add_executable(${TEST_RUNNER} test/proxy_plugin_test.cpp ${PLUGIN_SOURCES} ) apply_standard_settings(${TEST_RUNNER}) target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) # flutter_wrapper_plugin has link dependencies on the Flutter DLL. add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FLUTTER_LIBRARY}" $ ) # Enable automatic test discovery. include(GoogleTest) gtest_discover_tests(${TEST_RUNNER}) endif() ================================================ FILE: plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h ================================================ #ifndef FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ #define FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ #include #ifdef FLUTTER_PLUGIN_IMPL #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) #else #define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) #endif #if defined(__cplusplus) extern "C" { #endif FLUTTER_PLUGIN_EXPORT void ProxyPluginCApiRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); #if defined(__cplusplus) } // extern "C" #endif #endif // FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ ================================================ FILE: plugins/proxy/windows/proxy_plugin.cpp ================================================ #include "proxy_plugin.h" // This must be included before many other Windows headers. #include #include #include #include #include #include #pragma comment(lib, "wininet") #pragma comment(lib, "Rasapi32") // For getPlatformVersion; remove unless needed for your plugin implementation. #include #include #include #include #include #include void startProxy(const int port, const flutter::EncodableList& bypassDomain) { INTERNET_PER_CONN_OPTION_LIST list; DWORD dwBufSize = sizeof(list); list.dwSize = sizeof(list); list.pszConnection = nullptr; auto url = "127.0.0.1:" + std::to_string(port); auto wUrl = std::wstring(url.begin(), url.end()); auto fullAddr = new WCHAR[url.length() + 1]; wcscpy_s(fullAddr, url.length() + 1, wUrl.c_str()); std::wstring wBypassList; for (const auto& domain : bypassDomain) { if (!wBypassList.empty()) { wBypassList += L";"; } wBypassList += std::wstring(std::get(domain).begin(), std::get(domain).end()); } auto bypassAddr = new WCHAR[wBypassList.length() + 1]; wcscpy_s(bypassAddr, wBypassList.length() + 1, wBypassList.c_str()); list.dwOptionCount = 3; list.pOptions = new INTERNET_PER_CONN_OPTION[3]; if (!list.pOptions) { return; } list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT | PROXY_TYPE_PROXY; list.pOptions[1].dwOption = INTERNET_PER_CONN_PROXY_SERVER; list.pOptions[1].Value.pszValue = fullAddr; list.pOptions[2].dwOption = INTERNET_PER_CONN_PROXY_BYPASS; list.pOptions[2].Value.pszValue = bypassAddr; InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); RASENTRYNAME entry; entry.dwSize = sizeof(entry); std::vector entries; DWORD size = sizeof(entry), count; LPRASENTRYNAME entryAddr = &entry; auto ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); if (ret == ERROR_BUFFER_TOO_SMALL) { entries.resize(count); entries[0].dwSize = sizeof(RASENTRYNAME); entryAddr = entries.data(); ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); } if (ret != ERROR_SUCCESS) { return; } for (DWORD i = 0; i < count; i++) { list.pszConnection = entryAddr[i].szEntryName; InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); } delete[] fullAddr; delete[] bypassAddr; delete[] list.pOptions; InternetSetOption(nullptr, INTERNET_OPTION_SETTINGS_CHANGED, nullptr, 0); InternetSetOption(nullptr, INTERNET_OPTION_REFRESH, nullptr, 0); } void stopProxy() { INTERNET_PER_CONN_OPTION_LIST list; DWORD dwBufSize = sizeof(list); list.dwSize = sizeof(list); list.pszConnection = nullptr; list.dwOptionCount = 1; list.pOptions = new INTERNET_PER_CONN_OPTION[1]; if (nullptr == list.pOptions) { return; } list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT; InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); RASENTRYNAME entry; entry.dwSize = sizeof(entry); std::vector entries; DWORD size = sizeof(entry), count; LPRASENTRYNAME entryAddr = &entry; auto ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); if (ret == ERROR_BUFFER_TOO_SMALL) { entries.resize(count); entries[0].dwSize = sizeof(RASENTRYNAME); entryAddr = entries.data(); ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); } if (ret != ERROR_SUCCESS) { return; } for (DWORD i = 0; i < count; i++) { list.pszConnection = entryAddr[i].szEntryName; InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); } delete[] list.pOptions; InternetSetOption(nullptr, INTERNET_OPTION_SETTINGS_CHANGED, nullptr, 0); InternetSetOption(nullptr, INTERNET_OPTION_REFRESH, nullptr, 0); } namespace proxy { // static void ProxyPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows *registrar) { auto channel = std::make_unique>( registrar->messenger(), "proxy", &flutter::StandardMethodCodec::GetInstance()); auto plugin = std::make_unique(); channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto &call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); registrar->AddPlugin(std::move(plugin)); } ProxyPlugin::ProxyPlugin() {} ProxyPlugin::~ProxyPlugin() {} void ProxyPlugin::HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { if (method_call.method_name().compare("StopProxy") == 0) { stopProxy(); result->Success(true); } else if (method_call.method_name().compare("StartProxy") == 0) { auto *arguments = std::get_if(method_call.arguments()); auto port = std::get(arguments->at(flutter::EncodableValue("port"))); auto bypassDomain = std::get(arguments->at(flutter::EncodableValue("bypassDomain"))); startProxy(port, bypassDomain); result->Success(true); } else { result->NotImplemented(); } } } // namespace proxy ================================================ FILE: plugins/proxy/windows/proxy_plugin.h ================================================ #ifndef FLUTTER_PLUGIN_PROXY_PLUGIN_H_ #define FLUTTER_PLUGIN_PROXY_PLUGIN_H_ #include #include #include namespace proxy { class ProxyPlugin : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); ProxyPlugin(); virtual ~ProxyPlugin(); // Disallow copy and assign. ProxyPlugin(const ProxyPlugin&) = delete; ProxyPlugin& operator=(const ProxyPlugin&) = delete; // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result); }; } // namespace proxy #endif // FLUTTER_PLUGIN_PROXY_PLUGIN_H_ ================================================ FILE: plugins/proxy/windows/proxy_plugin_c_api.cpp ================================================ #include "include/proxy/proxy_plugin_c_api.h" #include #include "proxy_plugin.h" void ProxyPluginCApiRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { proxy::ProxyPlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); } ================================================ FILE: plugins/proxy/windows/test/proxy_plugin_test.cpp ================================================ #include #include #include #include #include #include #include #include #include "proxy_plugin.h" namespace proxy { namespace test { namespace { using flutter::EncodableMap; using flutter::EncodableValue; using flutter::MethodCall; using flutter::MethodResultFunctions; } // namespace TEST(ProxyPlugin, GetPlatformVersion) { ProxyPlugin plugin; // Save the reply value from the success callback. std::string result_string; plugin.HandleMethodCall( MethodCall("getPlatformVersion", std::make_unique()), std::make_unique>( [&result_string](const EncodableValue* result) { result_string = std::get(*result); }, nullptr, nullptr)); // Since the exact string varies by host, just ensure that it's a string // with the expected format. EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0); } } // namespace test } // namespace proxy ================================================ FILE: plugins/window_ext/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. /pubspec.lock **/doc/api/ .dart_tool/ build/ ================================================ FILE: plugins/window_ext/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "603104015dd692ea3403755b55d07813d5cf8965" channel: "stable" project_type: plugin # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 603104015dd692ea3403755b55d07813d5cf8965 base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - platform: macos create_revision: 603104015dd692ea3403755b55d07813d5cf8965 base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - platform: windows create_revision: 603104015dd692ea3403755b55d07813d5cf8965 base_revision: 603104015dd692ea3403755b55d07813d5cf8965 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: plugins/window_ext/LICENSE ================================================ TODO: Add your license here. ================================================ FILE: plugins/window_ext/analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: plugins/window_ext/lib/window_ext.dart ================================================ export 'window_ext_listener.dart'; export 'window_ext_manager.dart'; ================================================ FILE: plugins/window_ext/lib/window_ext_listener.dart ================================================ abstract mixin class WindowExtListener { void onTaskbarCreated() {} void onShouldTerminate() {} } ================================================ FILE: plugins/window_ext/lib/window_ext_manager.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'window_ext_listener.dart'; class WindowExtManager { WindowExtManager._() { _channel.setMethodCallHandler(_methodCallHandler); } static final WindowExtManager instance = WindowExtManager._(); final MethodChannel _channel = const MethodChannel('window_ext'); final ObserverList _listeners = ObserverList(); Future _methodCallHandler(MethodCall call) async { for (final WindowExtListener listener in _listeners) { switch (call.method) { case "taskbarCreated": listener.onTaskbarCreated(); break; case "shouldTerminate": listener.onShouldTerminate(); break; } } } bool get hasListeners { return _listeners.isNotEmpty; } void addListener(WindowExtListener listener) { _listeners.add(listener); } void removeListener(WindowExtListener listener) { _listeners.remove(listener); } } final windowExtManager = WindowExtManager.instance; ================================================ FILE: plugins/window_ext/macos/Classes/WindowExtPlugin.swift ================================================ import Cocoa import FlutterMacOS public class WindowExtPlugin: NSObject, FlutterPlugin { public static var instance:WindowExtPlugin? public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "window_ext", binaryMessenger: registrar.messenger) instance = WindowExtPlugin(registrar, channel) registrar.addMethodCallDelegate(instance!, channel: channel) } private var registrar: FlutterPluginRegistrar! private var channel: FlutterMethodChannel! public init(_ registrar: FlutterPluginRegistrar, _ channel: FlutterMethodChannel) { super.init() self.registrar = registrar self.channel = channel } public func handleShouldTerminate(){ channel.invokeMethod("shouldTerminate", arguments: nil) } } ================================================ FILE: plugins/window_ext/macos/window_ext.podspec ================================================ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # Run `pod lib lint window_ext.podspec` to validate before publishing. # Pod::Spec.new do |s| s.name = 'window_ext' s.version = '0.0.1' s.summary = 'A new Flutter plugin project.' s.description = <<-DESC A new Flutter plugin project. DESC s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' s.platform = :osx, '10.11' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end ================================================ FILE: plugins/window_ext/pubspec.yaml ================================================ name: window_ext description: "A new Flutter plugin project." version: 0.0.1 homepage: environment: sdk: '>=3.4.4 <4.0.0' flutter: '>=3.3.0' dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.0.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # This section identifies this Flutter project as a plugin project. # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) # which should be registered in the plugin registry. This is required for # using method channels. # The Android 'package' specifies package in which the registered class is. # This is required for using method channels on Android. # The 'ffiPlugin' specifies that native code should be built and bundled. # This is required for using `dart:ffi`. # All these are used by the tooling to maintain consistency when # adding or updating assets for this project. plugin: platforms: windows: pluginClass: WindowExtPluginCApi macos: pluginClass: WindowExtPlugin # To add assets to your plugin package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # # For details regarding assets in packages, see # https://flutter.dev/assets-and-images/#from-packages # # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # To add custom fonts to your plugin package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts in packages, see # https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: plugins/window_ext/windows/.gitignore ================================================ flutter/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: plugins/window_ext/windows/CMakeLists.txt ================================================ # The Flutter tooling requires that developers have a version of Visual Studio # installed that includes CMake 3.14 or later. You should not increase this # version, as doing so will cause the plugin to fail to compile for some # customers of the plugin. cmake_minimum_required(VERSION 3.14) # Project-level configuration. set(PROJECT_NAME "window_ext") project(${PROJECT_NAME} LANGUAGES CXX) # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) # This value is used when generating builds using this plugin, so it must # not be changed set(PLUGIN_NAME "window_ext_plugin") # Any new source files that you add to the plugin should be added here. list(APPEND PLUGIN_SOURCES "window_ext_plugin.cpp" "window_ext_plugin.h" ) # Define the plugin library target. Its name must not be changed (see comment # on PLUGIN_NAME above). add_library(${PLUGIN_NAME} SHARED "include/window_ext/window_ext_plugin_c_api.h" "window_ext_plugin_c_api.cpp" ${PLUGIN_SOURCES} ) # Apply a standard set of build settings that are configured in the # application-level CMakeLists.txt. This can be removed for plugins that want # full control over build settings. apply_standard_settings(${PLUGIN_NAME}) # Symbols are hidden by default to reduce the chance of accidental conflicts # between plugins. This should not be removed; any symbols that should be # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) # Source include directories and library dependencies. Add any plugin-specific # dependencies here. target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an # external build triggered from this build file. set(window_ext_bundled_libraries "" PARENT_SCOPE ) # === Tests === # These unit tests can be run from a terminal after building the example, or # from Visual Studio after opening the generated solution file. # Only enable test builds when building the example (which sets this variable) # so that plugin clients aren't building the tests. if (${include_${PROJECT_NAME}_tests}) set(TEST_RUNNER "${PROJECT_NAME}_test") enable_testing() # Add the Google Test dependency. include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/release-1.11.0.zip ) # Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Disable install commands for gtest so it doesn't end up in the bundle. set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) FetchContent_MakeAvailable(googletest) # The plugin's C API is not very useful for unit testing, so build the sources # directly into the test binary rather than using the DLL. add_executable(${TEST_RUNNER} test/window_ext_plugin_test.cpp ${PLUGIN_SOURCES} ) apply_standard_settings(${TEST_RUNNER}) target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) # flutter_wrapper_plugin has link dependencies on the Flutter DLL. add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FLUTTER_LIBRARY}" $ ) # Enable automatic test discovery. include(GoogleTest) gtest_discover_tests(${TEST_RUNNER}) endif() ================================================ FILE: plugins/window_ext/windows/include/window_ext/window_ext_plugin_c_api.h ================================================ #ifndef FLUTTER_PLUGIN_WINDOW_EXT_PLUGIN_C_API_H_ #define FLUTTER_PLUGIN_WINDOW_EXT_PLUGIN_C_API_H_ #include #ifdef FLUTTER_PLUGIN_IMPL #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) #else #define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) #endif #if defined(__cplusplus) extern "C" { #endif FLUTTER_PLUGIN_EXPORT void WindowExtPluginCApiRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); #if defined(__cplusplus) } // extern "C" #endif #endif // FLUTTER_PLUGIN_WINDOW_EXT_PLUGIN_C_API_H_ ================================================ FILE: plugins/window_ext/windows/test/window_ext_plugin_test.cpp ================================================ #include #include #include #include #include #include #include #include #include "window_ext_plugin.h" namespace window_ext { namespace test { namespace { using flutter::EncodableMap; using flutter::EncodableValue; using flutter::MethodCall; using flutter::MethodResultFunctions; } // namespace TEST(WindowExtPlugin, GetPlatformVersion) { WindowExtPlugin plugin; // Save the reply value from the success callback. std::string result_string; plugin.HandleMethodCall( MethodCall("getPlatformVersion", std::make_unique()), std::make_unique>( [&result_string](const EncodableValue* result) { result_string = std::get(*result); }, nullptr, nullptr)); // Since the exact string varies by host, just ensure that it's a string // with the expected format. EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0); } } // namespace test } // namespace window_ext ================================================ FILE: plugins/window_ext/windows/window_ext_plugin.cpp ================================================ #include "window_ext_plugin.h" // This must be included before many other Windows headers. #include // For getPlatformVersion; remove unless needed for your plugin implementation. #include #include #include #include #include #include namespace window_ext { std::unique_ptr< flutter::MethodChannel, std::default_delete>> channel = nullptr; // static void WindowExtPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows *registrar) { channel = std::make_unique>( registrar->messenger(), "window_ext", &flutter::StandardMethodCodec::GetInstance()); auto plugin = std::make_unique(registrar); channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto &call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); registrar->AddPlugin(std::move(plugin)); } WindowExtPlugin::WindowExtPlugin(flutter::PluginRegistrarWindows* registrar) : registrar(registrar) { WM_TASKBARCREATED = RegisterWindowMessage(TEXT("TaskbarCreated")); window_proc_id = registrar->RegisterTopLevelWindowProcDelegate( [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { return HandleWindowProc(hwnd, message, wparam, lparam); }); } WindowExtPlugin::~WindowExtPlugin() { registrar->UnregisterTopLevelWindowProcDelegate(window_proc_id); } std::optional WindowExtPlugin::HandleWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { std::optional result; if(message == WM_TASKBARCREATED){ channel -> InvokeMethod("taskbarCreated", std::make_unique()); } return result; } void WindowExtPlugin::HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { if (method_call.method_name().compare("getPlatformVersion") == 0) { std::ostringstream version_stream; version_stream << "Windows "; if (IsWindows10OrGreater()) { version_stream << "10+"; } else if (IsWindows8OrGreater()) { version_stream << "8"; } else if (IsWindows7OrGreater()) { version_stream << "7"; } result->Success(flutter::EncodableValue(version_stream.str())); } else { result->NotImplemented(); } } } // namespace window_ext ================================================ FILE: plugins/window_ext/windows/window_ext_plugin.h ================================================ #ifndef FLUTTER_PLUGIN_WINDOW_EXT_PLUGIN_H_ #define FLUTTER_PLUGIN_WINDOW_EXT_PLUGIN_H_ #include #include #include namespace window_ext { class WindowExtPlugin : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); WindowExtPlugin(flutter::PluginRegistrarWindows *registrar); virtual ~WindowExtPlugin(); // Disallow copy and assign. WindowExtPlugin(const WindowExtPlugin&) = delete; WindowExtPlugin& operator=(const WindowExtPlugin&) = delete; // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result); std::optional HandleWindowProc( HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); int window_proc_id = -1; UINT WM_TASKBARCREATED = 0; flutter::PluginRegistrarWindows *registrar; }; } // namespace window_ext #endif // FLUTTER_PLUGIN_WINDOW_EXT_PLUGIN_H_ ================================================ FILE: plugins/window_ext/windows/window_ext_plugin_c_api.cpp ================================================ #include "include/window_ext/window_ext_plugin_c_api.h" #include #include "window_ext_plugin.h" void WindowExtPluginCApiRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { window_ext::WindowExtPlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); } ================================================ FILE: pubspec.yaml ================================================ name: bett_box description: A multi-platform proxy client based on Mihomo, simple and easy to use, open-source and ad-free. publish_to: 'none' version: 1.17.3+2026050901 environment: sdk: '>=3.9.0 <4.0.0' dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter intl: ^0.20.0 path_provider: ^2.1.4 path: ^1.9.0 shared_preferences: ^2.5.5 window_manager: ^0.5.1 dynamic_color: ^1.8.1 vclibs: ^0.1.3 proxy: path: plugins/proxy window_ext: path: plugins/window_ext launch_at_startup: ^0.5.1 json_annotation: ^4.9.0 file_picker: ^8.3.7 mobile_scanner: ^7.2.0 app_links: ^6.3.3 win32_registry: ^2.0.0 tray_manager: ^0.5.2 collection: ^1.18.0 animations: ^2.1.2 package_info_plus: ^9.0.1 url_launcher: ^6.3.2 freezed_annotation: ^3.1.0 image_picker: ^1.2.1 webdav_client: ^1.2.2 dio: ^5.9.2 win32: ^5.15.0 ffi: ^2.2.0 re_editor: ^0.8.0 re_highlight: ^0.0.3 archive: ^4.0.7 lpinyin: ^2.0.3 emoji_regex: ^0.0.5 hotkey_manager: ^0.2.3 uni_platform: ^0.1.3 device_info_plus: ^12.4.0 sqflite: ^2.3.0 sqflite_common_ffi: ^2.3.7 yaml: ^3.1.2 connectivity_plus: 7.0.0 screen_retriever: ^0.2.0 defer_pointer: ^0.0.2 flutter_riverpod: ^2.6.1 riverpod_annotation: ^2.6.1 riverpod: ^2.6.1 material_color_utilities: ^0.11.1 flutter_js: ^0.8.7 flutter_svg: ^2.2.4 xml: ^6.5.0 flutter_cache_manager: ^3.4.1 crypto: ^3.0.7 flutter_acrylic: ^1.1.4 google_nav_bar: ^5.0.7 wakelock_plus: ^1.4.0 flutter_spinkit: ^5.2.2 synchronized: ^3.4.0 flutter_displaymode: ^0.7.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 ffigen: ^20.1.1 json_serializable: ^6.9.5 build_runner: ^2.4.15 args: ^2.4.2 freezed: ^3.1.0 riverpod_generator: ^2.6.4 custom_lint: ^0.7.6 intl_utils: ^2.8.11 flutter: generate: true uses-material-design: true assets: - assets/data/ - assets/fonts/ - assets/images/ - assets/images/avatars/ fonts: - family: JetBrainsMono fonts: - asset: assets/fonts/JetBrainsMono-Regular.ttf - family: Twemoji fonts: - asset: assets/fonts/Twemoji.Mozilla.ttf - family: Icons fonts: - asset: assets/fonts/Icons.ttf - family: HarmonyOS_Sans fonts: - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf ffigen: name: "ClashFFI" output: 'lib/clash/generated/clash_ffi.dart' headers: entry-points: - 'libclash/android/arm64-v8a/libclash.h' flutter_intl: enabled: true class_name: AppLocalizations arb_dir: arb output_dir: lib/l10n ================================================ FILE: services/helper/Cargo.toml ================================================ [package] name = "helper" version = "0.1.0" edition = "2021" [[bin]] name = "helper" path = "src/main.rs" [dependencies] windows-service = { version = "0.7.0", optional = true } tokio = { version = "1", features = ["full"] } anyhow = "1.0.93" warp = "0.3.7" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0" once_cell = "1.20.2" sha2 = "0.10.8" fs2 = "0.4" hmac = "0.12" hex = "0.4" bytes = "1" [profile.release] panic = "abort" codegen-units = 1 lto = true opt-level = "s" ================================================ FILE: services/helper/build.rs ================================================ fn main() { let version = std::env::var("TOKEN").unwrap_or_default(); println!("cargo:rustc-env=TOKEN={}", version); println!("cargo:rerun-if-env-changed=TOKEN"); } ================================================ FILE: services/helper/src/main.rs ================================================ #[cfg(not(all(feature = "windows-service", target_os = "windows")))] use tokio::runtime::Runtime; #[cfg(not(all(feature = "windows-service", target_os = "windows")))] use crate::service::hub::run_service; mod service; #[cfg(all(feature = "windows-service", target_os = "windows"))] pub fn main() -> windows_service::Result<()> { service::windows::main() } #[cfg(not(all(feature = "windows-service", target_os = "windows")))] fn main() { if let Ok(rt) = Runtime::new() { rt.block_on(async { let _ = run_service().await; }); } } ================================================ FILE: services/helper/src/service/hub.rs ================================================ use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::VecDeque; use std::fs::Metadata; use std::fs::{File, OpenOptions}; use std::io::{BufRead, BufReader, Error, Read}; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; use std::{io, thread}; use warp::{Filter, Reply}; use hmac::{Hmac, Mac}; use std::time::{SystemTime, UNIX_EPOCH}; use bytes::Bytes; #[allow(unused_imports)] use fs2::FileExt; const LISTEN_PORT: u16 = 45678; const TIME_WINDOW_SECS: u64 = 5; const MAX_LOG_ENTRIES: usize = 100; const HASH_BUFFER_SIZE: usize = 65536; type HmacSha256 = Hmac; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct StartParams { pub path: String, pub arg: String, pub home_dir: Option, } #[derive(Debug, Clone)] struct CachedFileHash { path: String, size: u64, modified_nanos: u128, hash: String, } fn metadata_signature(metadata: &Metadata) -> Result<(u64, u128), Error> { let modified_nanos = metadata .modified()? .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_nanos(); Ok((metadata.len(), modified_nanos)) } fn sha256_file_with_lock(file: &File, path: &str) -> Result { let metadata = file.metadata()?; let (size, modified_nanos) = metadata_signature(&metadata)?; if let Ok(cache_guard) = FILE_HASH_CACHE.lock() { if let Some(cache) = cache_guard.as_ref() { if cache.path == path && cache.size == size && cache.modified_nanos == modified_nanos { return Ok(cache.hash.clone()); } } } let mut hasher = Sha256::new(); let mut buffer = [0; HASH_BUFFER_SIZE]; let mut reader = BufReader::new(file); loop { let bytes_read = reader.read(&mut buffer)?; if bytes_read == 0 { break; } hasher.update(&buffer[..bytes_read]); } let hash = format!("{:x}", hasher.finalize()); if let Ok(mut cache_guard) = FILE_HASH_CACHE.lock() { *cache_guard = Some(CachedFileHash { path: path.to_string(), size, modified_nanos, hash: hash.clone(), }); } Ok(hash) } static LOGS: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(VecDeque::with_capacity(MAX_LOG_ENTRIES)))); static PROCESS: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); static AUTH_KEY: Lazy>>>> = Lazy::new(|| Arc::new(Mutex::new(None))); static FILE_HASH_CACHE: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); fn init_auth_key() { if let Ok(key_hex) = std::env::var("HELPER_AUTH_KEY") { if let Ok(key) = hex::decode(&key_hex) { if let Ok(mut auth_key) = AUTH_KEY.lock() { *auth_key = Some(key); log_message("Auth key initialized".to_string()); } } } } fn verify_request(timestamp: u64, signature: &str, body: &str) -> bool { let key = match AUTH_KEY.lock() { Ok(guard) => match guard.as_ref() { Some(k) => k.clone(), None => { log_message("Auth key not initialized, skipping verification".to_string()); return true; // Backward compatible: allow if no key } }, Err(_) => return false, }; let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); if now.abs_diff(timestamp) > TIME_WINDOW_SECS { log_message(format!("Request timestamp out of window: {} vs {}", timestamp, now)); return false; } let message = format!("{}:{}", timestamp, body); let mut mac = match HmacSha256::new_from_slice(&key) { Ok(m) => m, Err(_) => return false, }; mac.update(message.as_bytes()); let expected_signature = hex::encode(mac.finalize().into_bytes()); signature == expected_signature } fn start(start_params: StartParams) -> String { // Open file and add shared lock let file = match OpenOptions::new() .read(true) .open(&start_params.path) { Ok(f) => f, Err(e) => { let msg = format!("Failed to open file: {}", e); log_message(msg.clone()); return msg; } }; // Lock (prevent other processes from modifying) if let Err(e) = file.lock_shared() { let msg = format!("Failed to lock file: {}", e); log_message(msg.clone()); return msg; } // Calculate SHA256 while holding lock let sha256 = match sha256_file_with_lock(&file, &start_params.path) { Ok(hash) => hash, Err(e) => { let _ = file.unlock(); let msg = format!("Failed to calculate SHA256: {}", e); log_message(msg.clone()); return msg; } }; // Verify hash if sha256 != env!("TOKEN") { let _ = file.unlock(); let msg = format!("The SHA256 hash of the program requesting execution is: {}. The helper program only allows execution of applications with the SHA256 hash: {}.", sha256, env!("TOKEN")); log_message(msg.clone()); return msg; } let _ = file.unlock(); drop(file); // Start process after lock is released stop(); let mut process = PROCESS.lock().unwrap(); let mut command = Command::new(&start_params.path); command.stderr(Stdio::piped()).arg(&start_params.arg); if let Some(home_dir) = &start_params.home_dir { command.env("SAFE_PATHS", home_dir); } let result = match command.spawn() { Ok(child) => { *process = Some(child); // Start log collection thread if let Some(ref mut child) = *process { if let Some(stderr) = child.stderr.take() { let reader = io::BufReader::new(stderr); thread::spawn(move || { for line in reader.lines() { match line { Ok(output) => { log_message(output); } Err(_) => { break; } } } }); } } "".to_string() } Err(e) => { log_message(e.to_string()); e.to_string() } }; result } fn stop() -> String { let mut process = PROCESS.lock().unwrap(); if let Some(mut child) = process.take() { let _ = child.kill(); let _ = child.wait(); } *process = None; "".to_string() } fn log_message(message: String) { let mut log_buffer = LOGS.lock().unwrap(); if log_buffer.len() == MAX_LOG_ENTRIES { log_buffer.pop_front(); } log_buffer.push_back(message); } fn check_authentication(timestamp: Option, signature: Option, body: &[u8]) -> bool { let auth_enabled = AUTH_KEY.lock().unwrap().is_some(); if !auth_enabled { return true; } if let (Some(ts), Some(sig)) = (timestamp, signature) { let body_str = String::from_utf8_lossy(body); if verify_request(ts, &sig, &body_str) { return true; } } log_message("Authentication failed".to_string()); false } pub async fn run_service() -> anyhow::Result<()> { init_auth_key(); let api_ping = warp::get().and(warp::path("ping")).map(|| env!("TOKEN")); let api_start = warp::post() .and(warp::path("start")) .and(warp::header::optional::("X-Timestamp")) .and(warp::header::optional::("X-Signature")) .and(warp::body::bytes()) .and_then(|timestamp: Option, signature: Option, body_bytes: Bytes| async move { if !check_authentication(timestamp, signature, &body_bytes) { return Ok::<_, warp::Rejection>(warp::reply::with_status( "Unauthorized".to_string(), warp::http::StatusCode::UNAUTHORIZED ).into_response()); } let start_params: StartParams = match serde_json::from_slice(&body_bytes) { Ok(p) => p, Err(_) => return Ok(warp::reply::with_status( "Invalid JSON body".to_string(), warp::http::StatusCode::BAD_REQUEST ).into_response()), }; let result = tokio::task::spawn_blocking(move || { start(start_params) }).await.unwrap_or_else(|e| e.to_string()); Ok(warp::reply::with_status(result, warp::http::StatusCode::OK).into_response()) }); let api_stop = warp::post() .and(warp::path("stop")) .and(warp::header::optional::("X-Timestamp")) .and(warp::header::optional::("X-Signature")) .map(|timestamp: Option, signature: Option| { if !check_authentication(timestamp, signature, b"") { return warp::reply::with_status("Unauthorized", warp::http::StatusCode::UNAUTHORIZED).into_response(); } warp::reply::with_status(stop(), warp::http::StatusCode::OK).into_response() }); let api_logs = warp::get() .and(warp::path("logs")) .and(warp::header::optional::("X-Timestamp")) .and(warp::header::optional::("X-Signature")) .map(|timestamp: Option, signature: Option| { if !check_authentication(timestamp, signature, b"") { return warp::reply::with_status("Unauthorized".to_string(), warp::http::StatusCode::UNAUTHORIZED).into_response(); } let log_str = LOGS.lock().unwrap() .iter() .cloned() .collect::>() .join("\n"); warp::reply::with_header(log_str, "Content-Type", "text/plain").into_response() }); let routes = api_ping .or(api_start) .or(api_stop) .or(api_logs); warp::serve(routes) .run(([127, 0, 0, 1], LISTEN_PORT)) .await; Ok(()) } ================================================ FILE: services/helper/src/service/mod.rs ================================================ pub mod hub; #[cfg(all(feature = "windows-service", target_os = "windows"))] pub mod windows; ================================================ FILE: services/helper/src/service/windows.rs ================================================ use crate::service::hub::run_service; use std::ffi::OsString; use std::time::Duration; use tokio::runtime::Runtime; use windows_service::{ define_windows_service, service::{ ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, }, service_control_handler::{self, ServiceControlHandlerResult}, service_dispatcher, Result, }; const SERVICE_NAME: &str = "BettboxHelperService"; const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; pub fn main() -> Result<()> { start_service() } pub fn start_service() -> Result<()> { service_dispatcher::start(SERVICE_NAME, service_entry) } define_windows_service!(service_entry, service_main); pub fn service_main(_arguments: Vec) { if let Ok(rt) = Runtime::new() { rt.block_on(async { if let Err(e) = run_windows_service().await { let log_path = std::env::temp_dir().join("bettbox_helper_error.log"); let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); let msg = format!("[{}] service error: {}\n", ts, e); let _ = std::fs::OpenOptions::new() .create(true).append(true) .open(log_path) .map(|mut f| { use std::io::Write; let _ = f.write_all(msg.as_bytes()); }); } }); } } async fn run_windows_service() -> anyhow::Result<()> { let status_handle = service_control_handler::register( SERVICE_NAME, move |event| -> ServiceControlHandlerResult { match event { ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, ServiceControl::Stop => std::process::exit(0), _ => ServiceControlHandlerResult::NotImplemented, } }, )?; status_handle.set_service_status(ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::StartPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::from_secs(5), process_id: None, })?; status_handle.set_service_status(ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP, exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, })?; let result = run_service().await; let exit_code = if result.is_ok() { ServiceExitCode::Win32(0) } else { ServiceExitCode::ServiceSpecific(1) }; let _ = status_handle.set_service_status(ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code, checkpoint: 0, wait_hint: Duration::default(), process_id: None, }); result } ================================================ FILE: setup.dart ================================================ // ignore_for_file: avoid_print import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:crypto/crypto.dart'; import 'package:path/path.dart'; enum Target { windows, linux, android, macos } extension TargetExt on Target { String get os { if (this == Target.macos) { return 'darwin'; } return name; } bool get same { if (this == Target.android) { return true; } if (Platform.isWindows && this == Target.windows) { return true; } if (Platform.isLinux && this == Target.linux) { return true; } if (Platform.isMacOS && this == Target.macos) { return true; } return false; } String get dynamicLibExtensionName { final String extensionName; switch (this) { case Target.android || Target.linux: extensionName = '.so'; break; case Target.windows: extensionName = '.dll'; break; case Target.macos: extensionName = '.dylib'; break; } return extensionName; } String get executableExtensionName { final String extensionName; switch (this) { case Target.windows: extensionName = '.exe'; break; default: extensionName = ''; break; } return extensionName; } } enum Mode { core, lib } enum Arch { amd64, arm64, arm } class BuildItem { Target target; Arch? arch; String? archName; BuildItem({required this.target, this.arch, this.archName}); @override String toString() { return 'BuildLibItem{target: $target, arch: $arch, archName: $archName}'; } } class Build { static List get buildItems => [ BuildItem(target: Target.macos, arch: Arch.arm64), BuildItem(target: Target.macos, arch: Arch.amd64), BuildItem(target: Target.linux, arch: Arch.arm64), BuildItem(target: Target.linux, arch: Arch.amd64), BuildItem(target: Target.windows, arch: Arch.amd64), BuildItem(target: Target.windows, arch: Arch.arm64), BuildItem(target: Target.android, arch: Arch.arm, archName: 'armeabi-v7a'), BuildItem(target: Target.android, arch: Arch.arm64, archName: 'arm64-v8a'), BuildItem(target: Target.android, arch: Arch.amd64, archName: 'x86_64'), ]; static String get appName => 'Bettbox'; static String get coreName => 'BettboxCore'; static String get libName => 'libclash'; static String get outDir => join(current, libName); static String get _coreDir => join(current, 'core'); static String get _servicesDir => join(current, 'services', 'helper'); static String get distPath => join(current, 'dist'); static String _getCc(BuildItem buildItem) { final environment = Platform.environment; if (buildItem.target == Target.android) { final ndk = environment['ANDROID_NDK']; assert(ndk != null); final prebuiltDir = Directory( join(ndk!, 'toolchains', 'llvm', 'prebuilt'), ); final prebuiltDirList = prebuiltDir.listSync(); final map = { 'armeabi-v7a': 'armv7a-linux-androideabi21-clang', 'arm64-v8a': 'aarch64-linux-android21-clang', 'x86': 'i686-linux-android21-clang', 'x86_64': 'x86_64-linux-android21-clang', }; return join(prebuiltDirList.first.path, 'bin', map[buildItem.archName]); } return 'gcc'; } static String getTags(BuildItem buildItem) { final baseTags = 'with_gvisor,no_fake_tcp'; if (buildItem.target == Target.android && buildItem.archName == 'armeabi-v7a') { return '$baseTags,with_low_memory'; } return baseTags; } static Future exec( List executable, { String? name, Map? environment, String? workingDirectory, bool runInShell = true, }) async { if (name != null) print('run $name'); final process = await Process.start( executable[0], executable.sublist(1), environment: environment, workingDirectory: workingDirectory, runInShell: runInShell, ); process.stdout.listen((data) { print(utf8.decode(data)); }); process.stderr.listen((data) { print(utf8.decode(data)); }); final exitCode = await process.exitCode; if (exitCode != 0 && name != null) throw '$name error'; } static Future calcSha256(String filePath) async { final file = File(filePath); if (!await file.exists()) { throw 'File not exists'; } final stream = file.openRead(); return sha256.convert(await stream.reduce((a, b) => a + b)).toString(); } static Future> buildCore({ required Mode mode, required Target target, Arch? arch, bool compatible = false, }) async { final isLib = mode == Mode.lib; final items = buildItems.where((element) { return element.target == target && (arch == null ? true : element.arch == arch); }).toList(); final List corePaths = []; for (final item in items) { final outFileDir = join(outDir, item.target.name, item.archName); final file = File(outFileDir); if (file.existsSync()) { file.deleteSync(recursive: true); } final fileName = isLib ? '$libName${item.target.dynamicLibExtensionName}' : '$coreName${item.target.executableExtensionName}'; final outPath = join(outFileDir, fileName); corePaths.add(outPath); final Map env = {}; env['GOOS'] = item.target.os; if (item.arch != null) { env['GOARCH'] = item.arch!.name; } if (item.arch == Arch.amd64 && (item.target == Target.windows || item.target == Target.linux || item.target == Target.macos)) { env['GOAMD64'] = compatible ? 'v1' : 'v3'; } if (isLib) { env['CGO_ENABLED'] = '1'; env['CC'] = _getCc(item); env['CFLAGS'] = '-O3 -Werror'; } else { env['CGO_ENABLED'] = '0'; } final buildTags = getTags(item); await exec( ['go', 'mod', 'tidy'], name: 'go mod tidy', environment: env, workingDirectory: _coreDir, ); final execLines = [ 'go', 'build', '-trimpath', '-ldflags=-w -s${item.target == Target.android && (item.arch == Arch.arm64 || item.arch == Arch.amd64) ? ' -extldflags "-Wl,-z,max-page-size=16384"' : ''}', '-tags=$buildTags', if (isLib) '-buildmode=c-shared', '-o', outPath, ]; await exec( execLines, name: 'build core', environment: env, workingDirectory: _coreDir, ); } return corePaths; } static Future buildHelper(Target target, String token) async { await exec( ['cargo', 'build', '--release', '--features', 'windows-service'], environment: {'TOKEN': token}, name: 'build helper', workingDirectory: _servicesDir, ); final outPath = join( _servicesDir, 'target', 'release', 'helper${target.executableExtensionName}', ); final targetPath = join( outDir, target.name, 'BettboxHelperService${target.executableExtensionName}', ); await File(outPath).copy(targetPath); } static List getExecutable(String command) { return command.split(' '); } static Future getDistributor() async { final distributorDir = join( current, 'plugins', 'flutter_distributor', 'packages', 'flutter_distributor', ); await exec( name: 'clean distributor', Build.getExecutable('flutter clean'), workingDirectory: distributorDir, ); await exec( name: 'upgrade distributor', Build.getExecutable('flutter pub upgrade'), workingDirectory: distributorDir, ); await exec( name: 'get distributor', Build.getExecutable('dart pub global activate -s path $distributorDir'), ); } static void copyFile(String sourceFilePath, String destinationFilePath) { final sourceFile = File(sourceFilePath); if (!sourceFile.existsSync()) { throw 'SourceFilePath not exists'; } final destinationFile = File(destinationFilePath); final destinationDirectory = destinationFile.parent; if (!destinationDirectory.existsSync()) { destinationDirectory.createSync(recursive: true); } try { sourceFile.copySync(destinationFilePath); print('File copied successfully!'); } catch (e) { print('Failed to copy file: $e'); } } } class BuildCommand extends Command { Target target; BuildCommand({required this.target}) { if (target == Target.android || target == Target.linux) { argParser.addOption( 'arch', valueHelp: arches.map((e) => e.name).join(','), help: 'The $name build desc', ); } else { argParser.addOption('arch', help: 'The $name build archName'); } argParser.addOption( 'out', valueHelp: [if (target.same) 'app', 'core'].join(','), help: 'The $name build arch', ); argParser.addOption( 'env', valueHelp: ['pre', 'stable'].join(','), help: 'The $name build env', ); argParser.addFlag( 'compatible', help: 'Build with GOAMD64=v2 for broader compatibility on amd64', ); } @override String get description => 'build $name application'; @override String get name => target.name; List get arches => Build.buildItems .where((element) => element.target == target && element.arch != null) .map((e) => e.arch!) .toList(); Future _getLinuxDependencies(Arch arch) async { await Build.exec(Build.getExecutable('sudo apt update -y')); await Build.exec( Build.getExecutable('sudo apt install -y ninja-build libgtk-3-dev'), ); await Build.exec( Build.getExecutable('sudo apt install -y libayatana-appindicator3-dev'), ); await Build.exec( Build.getExecutable('sudo apt-get install -y libkeybinder-3.0-dev'), ); await Build.exec(Build.getExecutable('sudo apt install -y locate')); if (arch == Arch.amd64) { await Build.exec( Build.getExecutable('sudo apt install -y rpm patchelf libfuse2'), ); final downloadName = arch == Arch.amd64 ? 'x86_64' : 'aarch64'; await Build.exec( Build.getExecutable( 'wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage', ), ); await Build.exec(Build.getExecutable('chmod +x appimagetool')); await Build.exec( Build.getExecutable('sudo mv appimagetool /usr/local/bin/'), ); } } Future _getMacosDependencies() async { await Build.exec(Build.getExecutable('npm install -g appdmg')); } Future _setMacOSCompatibleBuild(bool enable) async { final infoPlistPath = 'macos/Runner/Info.plist'; final file = File(infoPlistPath); if (!await file.exists()) { print('Warning: Info.plist not found at $infoPlistPath'); return; } var content = await file.readAsString(); // Check if FLTDisableImpeller key exists if (content.contains('FLTDisableImpeller')) { // Update existing key if (enable) { content = content.replaceAll( RegExp(r'FLTDisableImpeller\s*<(?:true|false)/>'), 'FLTDisableImpeller\n\t', ); } else { content = content.replaceAll( RegExp(r'FLTDisableImpeller\s*<(?:true|false)/>'), 'FLTDisableImpeller\n\t', ); } } else { // Add new key before final impellerEntry = enable ? '\tFLTDisableImpeller\n\t\n' : '\tFLTDisableImpeller\n\t\n'; content = content.replaceFirst( '\n', '$impellerEntry\n', ); } await file.writeAsString(content); print('macOS ${enable ? "Compatible" : "Standard"} build: FLTDisableImpeller set to $enable'); } Future _buildDistributor({ required Target target, required String targets, String args = '', required String env, }) async { final sentryDsn = Platform.environment['SENTRY_DSN'] ?? ''; final sentryArg = sentryDsn.isNotEmpty ? ' --build-dart-define=SENTRY_DSN=$sentryDsn' : ''; await Build.getDistributor(); await Build.exec( name: name, Build.getExecutable( 'flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose$args$sentryArg --build-dart-define=APP_ENV=$env', ), ); } Future get systemArch async { if (Platform.isWindows) { return Platform.environment['PROCESSOR_ARCHITECTURE']; } else if (Platform.isLinux || Platform.isMacOS) { final result = await Process.run('uname', ['-m']); return result.stdout.toString().trim(); } return null; } @override Future run() async { final mode = target == Target.android ? Mode.lib : Mode.core; final String out = argResults?['out'] ?? (target.same ? 'app' : 'core'); final archName = argResults?['arch']; final env = argResults?['env'] ?? 'pre'; final currentArches = arches .where((element) => element.name == archName) .toList(); final arch = currentArches.isEmpty ? null : currentArches.first; if (arch == null && target != Target.android) { throw 'Invalid arch parameter'; } final bool compatible = argResults?['compatible'] ?? false; final corePaths = await Build.buildCore( target: target, arch: arch, mode: mode, compatible: compatible, ); if (out != 'app') { return; } final String desc = compatible ? '$archName-compatible' : (archName ?? ''); switch (target) { case Target.windows: final token = target != Target.android ? await Build.calcSha256(corePaths.first) : null; Build.buildHelper(target, token!); _buildDistributor( target: target, targets: 'exe', args: ' --description $desc --build-dart-define=CORE_SHA256=$token', env: env, ); return; case Target.linux: final targetMap = {Arch.arm64: 'linux-arm64', Arch.amd64: 'linux-x64'}; final targets = [ 'deb', if (arch == Arch.amd64) 'appimage', if (arch == Arch.amd64) 'rpm', ].join(','); final defaultTarget = targetMap[arch]; await _getLinuxDependencies(arch!); _buildDistributor( target: target, targets: targets, args: ' --description $desc --build-target-platform $defaultTarget', env: env, ); return; case Target.android: final targetMap = { Arch.arm: 'android-arm', Arch.arm64: 'android-arm64', Arch.amd64: 'android-x64', }; final defaultArches = [Arch.arm, Arch.arm64, Arch.amd64]; final defaultTargets = defaultArches .where((element) => arch == null ? true : element == arch) .map((e) => targetMap[e]) .toList(); final buildArgs = archName == 'universal' ? ' --build-target-platform ${defaultTargets.join(",")} --description universal' : ',split-per-abi --build-target-platform ${defaultTargets.join(",")}'; _buildDistributor( target: target, targets: 'apk', args: buildArgs, env: env, ); return; case Target.macos: await _getMacosDependencies(); // For compatible build, disable Impeller and use Skia renderer if (compatible) { await _setMacOSCompatibleBuild(true); } else { await _setMacOSCompatibleBuild(false); } _buildDistributor( target: target, targets: 'dmg', args: ' --description $desc', env: env, ); return; } } } Future main(Iterable args) async { final runner = CommandRunner('setup', 'build Application'); runner.addCommand(BuildCommand(target: Target.android)); runner.addCommand(BuildCommand(target: Target.linux)); runner.addCommand(BuildCommand(target: Target.windows)); runner.addCommand(BuildCommand(target: Target.macos)); runner.run(args); } ================================================ FILE: windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ build/ out/ .idea/ .vs/ .vscode/ ================================================ FILE: windows/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.14) project(Bettbox LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "Bettbox") add_definitions(-DFLUTTER_DISABLE_IMPELLER=1) # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() # Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # libclash.so set(CLASH_DIR "../libclash/windows") # if(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") # elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM" OR CMAKE_SYSTEM_PROCESSOR MATCHES "armv[0-9]+") # elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86") # set(CLASH_DIR "../libclash/windows/x86") # endif() install(PROGRAMS "${CLASH_DIR}/BettboxCore.exe" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) install(PROGRAMS "${CLASH_DIR}/BettboxHelperService.exe" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) install(FILES "WindowsLoopbackManager.exe" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) set(BETTBOX_FLUTTER_JS_ARM64_BRIDGE "${CMAKE_CURRENT_SOURCE_DIR}/overrides/flutter_js/arm64/quickjs_c_bridge.dll") file(GLOB BETTBOX_FLUTTER_JS_ARM64_COMPANION_DLLS "${CMAKE_CURRENT_SOURCE_DIR}/overrides/flutter_js/arm64/*.dll") list(REMOVE_ITEM BETTBOX_FLUTTER_JS_ARM64_COMPANION_DLLS "${BETTBOX_FLUTTER_JS_ARM64_BRIDGE}") set(BETTBOX_IS_WINDOWS_ARM64 FALSE) if(DEFINED FLUTTER_TARGET_PLATFORM AND FLUTTER_TARGET_PLATFORM STREQUAL "windows-arm64") set(BETTBOX_IS_WINDOWS_ARM64 TRUE) elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64") set(BETTBOX_IS_WINDOWS_ARM64 TRUE) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") set(BETTBOX_IS_WINDOWS_ARM64 TRUE) endif() if(BETTBOX_IS_WINDOWS_ARM64) if(NOT EXISTS "${BETTBOX_FLUTTER_JS_ARM64_BRIDGE}") message(FATAL_ERROR "Missing ARM64 quickjs_c_bridge.dll: ${BETTBOX_FLUTTER_JS_ARM64_BRIDGE}") endif() install(CODE "file(REMOVE_RECURSE \"${INSTALL_BUNDLE_LIB_DIR}/quickjs_c_bridge.dll\")" COMPONENT Runtime) install(FILES "${BETTBOX_FLUTTER_JS_ARM64_BRIDGE}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(BETTBOX_FLUTTER_JS_ARM64_COMPANION_DLLS) install(FILES ${BETTBOX_FLUTTER_JS_ARM64_COMPANION_DLLS} DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() endif() ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterAcrylicPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); FlutterJsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterJsPlugin")); HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); ProxyPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ProxyPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); TrayManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); VclibsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("VclibsPluginCApi")); WindowExtPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowExtPluginCApi")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } ================================================ FILE: windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST app_links connectivity_plus dynamic_color file_selector_windows flutter_acrylic flutter_js hotkey_manager_windows proxy screen_retriever_windows sentry_flutter tray_manager url_launcher_windows vclibs window_ext window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST jni ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: windows/packaging/exe/ChineseSimplified.isl ================================================ ; *** Inno Setup version 6.1.0+ Chinese Simplified messages *** ; ; To download user-contributed translations of this file, go to: ; https://jrsoftware.org/files/istrans/ ; ; Note: When translating this text, do not add periods (.) to the end of ; messages that didn't have them already, because on those messages Inno ; Setup adds the periods automatically (appending a period would result in ; two periods being displayed). ; ; Maintained by Zhenghan Yang ; Email: 847320916@QQ.com ; Translation based on network resource ; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation ; [LangOptions] ; The following three entries are very important. Be sure to read and ; understand the '[LangOptions] section' topic in the help file. LanguageName=简体中文 ; If Language Name display incorrect, uncomment next line ; LanguageName=<7B80><4F53><4E2D><6587> ; About LanguageID, to reference link: ; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c LanguageID=$0804 LanguageCodePage=936 ; If the language you are translating to requires special font faces or ; sizes, uncomment any of the following entries and change them accordingly. DialogFontName=Microsoft YaHei UI ;DialogFontSize=8 WelcomeFontName=Microsoft YaHei UI ;WelcomeFontSize=12 TitleFontName=Microsoft YaHei UI ;TitleFontSize=29 ;CopyrightFontName=Arial ;CopyrightFontSize=8 [Messages] ; *** 应用程序标题 SetupAppTitle=安装 SetupWindowTitle=安装 - %1 UninstallAppTitle=卸载 UninstallAppFullTitle=%1 卸载 ; *** Misc. common InformationTitle=信息 ConfirmTitle=确认 ErrorTitle=错误 ; *** SetupLdr messages SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? LdrCannotCreateTemp=不能创建临时文件。安装中断。 LdrCannotExecTemp=不能执行临时目录中的文件。安装中断。 HelpTextNote= ; *** 启动错误消息 LastErrorMessage=%1.%n%n错误 %2: %3 SetupFileMissing=安装目录中的文件 %1 丢失。请修正这个问题或者获取程序的新副本。 SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 InvalidParameter=无效的命令行参数:%n%n%1 SetupAlreadyRunning=安装程序正在运行。 WindowsVersionNotSupported=这个程序不支持当前计算机运行的 Windows 版本。 WindowsServicePackRequired=这个程序需要 %1 服务包 %2 或更高。 NotOnThisPlatform=这个程序将不能运行于 %1。 OnlyOnThisPlatform=这个程序必须运行于 %1。 OnlyOnTheseArchitectures=这个程序只能在为下列处理器架构的 Windows 版本中进行安装:%n%n%1 WinVersionTooLowError=这个程序需要 %1 版本 %2 或更高。 WinVersionTooHighError=这个程序不能安装于 %1 版本 %2 或更高。 AdminPrivilegesRequired=在安装这个程序时您必须以管理员身份登录。 PowerUserPrivilegesRequired=在安装这个程序时您必须以管理员身份或有权限的用户组身份登录。 SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭所有运行的窗口,然后点击“确定”继续,或按“取消”退出。 UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭所有运行的窗口,然后点击“确定”继续,或按“取消”退出。 ; *** 启动问题 PrivilegesRequiredOverrideTitle=选择安装程序模式 PrivilegesRequiredOverrideInstruction=选择安装模式 PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M) PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项) ; *** 其它错误 ErrorCreatingDir=安装程序不能创建目录“%1”。 ErrorTooManyFilesInDir=不能在目录“%1”中创建文件,因为里面的文件太多 ; *** 安装程序公共消息 ExitSetupTitle=退出安装程序 ExitSetupMessage=安装程序尚未完成安装。如果您现在退出,程序将不能安装。%n%n您可以以后再运行安装程序完成安装。%n%n现在退出安装程序吗? AboutSetupMenuItem=关于安装程序(&A)... AboutSetupTitle=关于安装程序 AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 AboutSetupNote= TranslatorNote=Translated by Zhenghan Yang. ; *** 按钮 ButtonBack=< 上一步(&B) ButtonNext=下一步(&N) > ButtonInstall=安装(&I) ButtonOK=确定 ButtonCancel=取消 ButtonYes=是(&Y) ButtonYesToAll=全是(&A) ButtonNo=否(&N) ButtonNoToAll=全否(&O) ButtonFinish=完成(&F) ButtonBrowse=浏览(&B)... ButtonWizardBrowse=浏览(&R)... ButtonNewFolder=新建文件夹(&M) ; *** “选择语言”对话框消息 SelectLanguageTitle=选择安装语言 SelectLanguageLabel=选择安装时要使用的语言。 ; *** 公共向导文字 ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 BeveledLabel= BrowseDialogTitle=浏览文件夹 BrowseDialogLabel=在下列列表中选择一个文件夹,然后点击“确定”。 NewFolderName=新建文件夹 ; *** “欢迎”向导页 WelcomeLabel1=欢迎使用 [name] 安装向导 WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n推荐您在继续安装前关闭所有其它应用程序。 ; *** “密码”向导页 WizardPassword=密码 PasswordLabel1=这个安装程序有密码保护。 PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 PasswordEditLabel=密码(&P): IncorrectPassword=您所输入的密码不正确,请重试。 ; *** “许可协议”向导页 WizardLicense=许可协议 LicenseLabel=继续安装前请阅读下列重要信息。 LicenseLabel3=请仔细阅读下列许可协议。您在继续安装前必须同意这些协议条款。 LicenseAccepted=我同意此协议(&A) LicenseNotAccepted=我拒绝此协议(&D) ; *** “信息”向导页 WizardInfoBefore=信息 InfoBeforeLabel=请在继续安装前阅读下列重要信息。 InfoBeforeClickLabel=如果您想继续安装,点击“下一步”。 WizardInfoAfter=信息 InfoAfterLabel=请在继续安装前阅读下列重要信息。 InfoAfterClickLabel=如果您想继续安装,点击“下一步”。 ; *** “用户信息”向导页 WizardUserInfo=用户信息 UserInfoDesc=请输入您的信息。 UserInfoName=用户名(&U): UserInfoOrg=组织(&O): UserInfoSerial=序列号(&S): UserInfoNameRequired=您必须输入用户名。 ; *** “选择目标目录”向导页 WizardSelectDir=选择目标位置 SelectDirDesc=您想将 [name] 安装在哪里? SelectDirLabel3=安装程序将安装 [name] 到下列文件夹中。 SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其它文件夹,点击“浏览”。 DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 CannotInstallToUNCPath=安装程序无法安装到一个UNC路径。 InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或下列形式的UNC路径:%n%n\\server\share InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选选择其它位置。 DiskSpaceWarningTitle=没有足够的磁盘空间 DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? DirNameTooLong=文件夹名称或路径太长。 InvalidDirName=文件夹名称无效。 BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 DirExistsTitle=文件夹已存在 DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? DirDoesntExistTitle=文件夹不存在 DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? ; *** “选择组件”向导页 WizardSelectComponents=选择组件 SelectComponentsDesc=您想安装哪些程序的组件? SelectComponentsLabel2=选择您想要安装的组件;清除您不想安装的组件。然后点击“下一步”继续。 FullInstallation=完全安装 ; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) CompactInstallation=简洁安装 CustomInstallation=自定义安装 NoUninstallWarningTitle=组件已存在 NoUninstallWarning=安装程序检测到下列组件已在您的电脑中安装:%n%n%1%n%n取消选定这些组件将不能卸载它们。%n%n您一定要继续吗? ComponentSize1=%1 KB ComponentSize2=%1 MB ComponentsDiskSpaceGBLabel=当前选择的组件至少需要 [gb] GB 的磁盘空间。 ComponentsDiskSpaceMBLabel=当前选择的组件至少需要 [mb] MB 的磁盘空间。 ; *** “选择附加任务”向导页 WizardSelectTasks=选择附加任务 SelectTasksDesc=您想要安装程序执行哪些附加任务? SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 ; *** “选择开始菜单文件夹”向导页 WizardSelectProgramGroup=选择开始菜单文件夹 SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? SelectStartMenuFolderLabel3=安装程序现在将在下列开始菜单文件夹中创建程序的快捷方式。 SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其它文件夹,点击“浏览”。 MustEnterGroupName=您必须输入一个文件夹名。 GroupNameTooLong=文件夹名或路径太长。 InvalidGroupName=文件夹名无效。 BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 NoProgramGroupCheck2=不创建开始菜单文件夹(&D) ; *** “准备安装”向导页 WizardReady=准备安装 ReadyLabel1=安装程序现在准备开始安装 [name] 到您的电脑中。 ReadyLabel2a=点击“安装”继续此安装程序。如果您想要回顾或修改设置,请点击“上一步”。 ReadyLabel2b=点击“安装”继续此安装程序? ReadyMemoUserInfo=用户信息: ReadyMemoDir=目标位置: ReadyMemoType=安装类型: ReadyMemoComponents=选定组件: ReadyMemoGroup=开始菜单文件夹: ReadyMemoTasks=附加任务: ; *** TDownloadWizardPage wizard page and DownloadTemporaryFile DownloadingLabel=正在下载附加文件... ButtonStopDownload=停止下载(&S) StopDownload=您确定要停止下载吗? ErrorDownloadAborted=下载已中止 ErrorDownloadFailed=下载失败:%1 %2 ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 ErrorFileHash1=校验文件哈希失败:%1 ErrorFileHash2=无效的文件哈希:预期为 %1,实际为 %2 ErrorProgress=无效的进度:%1,总共%2 ErrorFileSize=文件大小错误:预期为 %1,实际为 %2 ; *** “正在准备安装”向导页 WizardPreparing=正在准备安装 PreparingDesc=安装程序正在准备安装 [name] 到您的电脑中。 PreviousInstallNotCompleted=先前程序的安装/卸载未完成。您需要重新启动您的电脑才能完成安装。%n%n在重新启动电脑后,再运行安装完成 [name] 的安装。 CannotContinue=安装程序不能继续。请点击“取消”退出。 ApplicationsFound=下列应用程序正在使用的文件需要更新设置。它是建议您允许安装程序自动关闭这些应用程序。 ApplicationsFound2=下列应用程序正在使用的文件需要更新设置。它是建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动应用程序。 CloseApplications=自动关闭该应用程序(&A) DontCloseApplications=不要关闭该应用程序(&D) ErrorCloseApplications=安装程序无法自动关闭所有应用程序。在继续之前,我们建议您关闭所有使用需要更新的安装程序文件。 PrepareToInstallNeedsRestart=安装程序必须重新启动计算机。重新启动计算机后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? ; *** “正在安装”向导页 WizardInstalling=正在安装 InstallingLabel=安装程序正在安装 [name] 到您的电脑中,请稍等。 ; *** “安装完成”向导页 FinishedHeadingLabel=[name] 安装完成 FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 FinishedLabel=安装程序已在您的电脑中安装了 [name]。此应用程序可以通过选择安装的快捷方式运行。 ClickFinish=点击“完成”退出安装程序。 FinishedRestartLabel=要完成 [name] 的安装,安装程序必须重新启动您的电脑。您想要立即重新启动吗? FinishedRestartMessage=要完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n您想要立即重新启动吗? ShowReadmeCheck=是,我想查阅自述文件 YesRadio=是,立即重新启动电脑(&Y) NoRadio=否,稍后重新启动电脑(&N) ; used for example as 'Run MyProg.exe' RunEntryExec=运行 %1 ; used for example as 'View Readme.txt' RunEntryShellExec=查阅 %1 ; *** “安装程序需要下一张磁盘”提示 ChangeDiskTitle=安装程序需要下一张磁盘 SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 PathLabel=路径(&P): FileNotInDir2=文件“%1”不能在“%2”定位。请插入正确的磁盘或选择其它文件夹。 SelectDirectoryLabel=请指定下一张磁盘的位置。 ; *** 安装状态消息 SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 AbortRetryIgnoreSelectAction=选择操作 AbortRetryIgnoreRetry=重试(&T) AbortRetryIgnoreIgnore=忽略错误并继续(&I) AbortRetryIgnoreCancel=关闭安装程序 ; *** 安装状态消息 StatusClosingApplications=正在关闭应用程序... StatusCreateDirs=正在创建目录... StatusExtractFiles=正在解压缩文件... StatusCreateIcons=正在创建快捷方式... StatusCreateIniEntries=正在创建 INI 条目... StatusCreateRegistryEntries=正在创建注册表条目... StatusRegisterFiles=正在注册文件... StatusSavingUninstall=正在保存卸载信息... StatusRunProgram=正在完成安装... StatusRestartingApplications=正在重启应用程序... StatusRollback=正在撤销更改... ; *** 其它错误 ErrorInternal2=内部错误:%1 ErrorFunctionFailedNoCode=%1 失败 ErrorFunctionFailed=%1 失败;错误代码 %2 ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 ErrorExecutingProgram=不能执行文件:%n%1 ; *** 注册表错误 ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 ; *** INI 错误 ErrorIniEntry=在文件“%1”中创建INI条目时出错。 ; *** 文件复制错误 FileAbortRetryIgnoreSkipNotRecommended=跳过这个文件(&S) (不推荐) FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) SourceIsCorrupted=源文件已损坏 SourceDoesntExist=源文件“%1”不存在 ExistingFileReadOnly2=无法替换现有文件,因为它是只读的。 ExistingFileReadOnlyRetry=移除只读属性并重试(&R) ExistingFileReadOnlyKeepExisting=保留现有文件(&K) ErrorReadingExistingDest=尝试读取现有文件时出错: FileExistsSelectAction=选择操作 FileExists2=文件已经存在。 FileExistsOverwriteExisting=覆盖已经存在的文件(&O) FileExistsKeepExisting=保留现有的文件(&K) FileExistsOverwriteOrKeepAll=为所有的冲突文件执行此操作(&D) ExistingFileNewerSelectAction=选择操作 ExistingFileNewer2=现有的文件比安装程序将要安装的文件更新。 ExistingFileNewerOverwriteExisting=覆盖已经存在的文件(&O) ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) ExistingFileNewerOverwriteOrKeepAll=为所有的冲突文件执行此操作(&D) ErrorChangingAttr=尝试改变下列现有的文件的属性时出错: ErrorCreatingTemp=尝试在目标目录创建文件时出错: ErrorReadingSource=尝试读取下列源文件时出错: ErrorCopying=尝试复制下列文件时出错: ErrorReplacingExistingFile=尝试替换现有的文件时出错: ErrorRestartReplace=重新启动替换失败: ErrorRenamingTemp=尝试重新命名以下目标目录中的一个文件时出错: ErrorRegisterServer=无法注册 DLL/OCX:%1 ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 ErrorRegisterTypeLib=无法注册类型库:%1 ; *** 卸载显示名字标记 ; used for example as 'My Program (32-bit)' UninstallDisplayNameMark=%1 (%2) ; used for example as 'My Program (32-bit, All users)' UninstallDisplayNameMarks=%1 (%2, %3) UninstallDisplayNameMark32Bit=32位 UninstallDisplayNameMark64Bit=64位 UninstallDisplayNameMarkAllUsers=所有用户 UninstallDisplayNameMarkCurrentUser=当前用户 ; *** 安装后错误 ErrorOpeningReadme=尝试打开自述文件时出错。 ErrorRestartingComputer=安装程序不能重新启动电脑,请手动重启。 ; *** 卸载消息 UninstallNotFound=文件“%1”不存在。无法卸载。 UninstallOpenError=文件“%1”不能打开。无法卸载。 UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 UninstallUnknownEntry=在卸载日志中遇到一个未知的条目 (%1) ConfirmUninstall=您确认想要完全删除 %1 及它的所有组件吗? UninstallOnlyOnWin64=这个安装程序只能在64位Windows中进行卸载。 OnlyAdminCanUninstall=这个安装的程序需要有管理员权限的用户才能卸载。 UninstallStatusLabel=正在从您的电脑中删除 %1,请稍等。 UninstalledAll=%1 已顺利地从您的电脑中删除。 UninstalledMost=%1 卸载完成。%n%n有一些内容无法被删除。您可以手动删除它们。 UninstalledAndNeedsRestart=要完成 %1 的卸载,您的电脑必须重新启动。%n%n您想立即重新启动电脑吗? UninstallDataCorrupted=文件“%1”已损坏,无法卸载 ; *** 卸载状态消息 ConfirmDeleteSharedFileTitle=删除共享文件吗? ConfirmDeleteSharedFile2=系统中包含的下列共享文件已经不再被其它程序使用。您想要卸载程序删除这些共享文件吗?%n%n如果这些文件被删除,但还有程序正在使用这些文件,这些程序可能不能正确执行。如果您不能确定,选择“否”。把这些文件保留在系统中以免引起问题。 SharedFileNameLabel=文件名: SharedFileLocationLabel=位置: WizardUninstalling=卸载状态 StatusUninstalling=正在卸载 %1... ; *** Shutdown block reasons ShutdownBlockReasonInstallingApp=正在安装 %1。 ShutdownBlockReasonUninstallingApp=正在卸载 %1。 ; The custom messages below aren't used by Setup itself, but if you make ; use of them in your scripts, you'll want to translate them. [CustomMessages] NameAndVersion=%1 版本 %2 AdditionalIcons=附加快捷方式: CreateDesktopIcon=创建桌面快捷方式(&D) CreateQuickLaunchIcon=创建快速运行栏快捷方式(&Q) ProgramOnTheWeb=%1 网站 UninstallProgram=卸载 %1 LaunchProgram=运行 %1 AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... AutoStartProgramGroupDescription=启动组: AutoStartProgram=自动启动 %1 AddonHostProgramNotFound=%1无法找到您所选择的文件夹。%n%n您想要继续吗? ================================================ FILE: windows/packaging/exe/inno_setup.iss ================================================ [Setup] AppId={{APP_ID}} AppVersion={{APP_VERSION}} AppName={{DISPLAY_NAME}} AppPublisher={{PUBLISHER_NAME}} AppPublisherURL={{PUBLISHER_URL}} AppSupportURL={{PUBLISHER_URL}} AppUpdatesURL={{PUBLISHER_URL}} DefaultDirName={{INSTALL_DIR_NAME}} DisableProgramGroupPage=yes OutputDir=. OutputBaseFilename={{OUTPUT_BASE_FILENAME}} Compression=lzma SolidCompression=yes SetupIconFile={{SETUP_ICON_FILE}} UninstallDisplayIcon={app}\{{EXECUTABLE_NAME}} WizardStyle=modern PrivilegesRequired={{PRIVILEGES_REQUIRED}} ArchitecturesAllowed={{ARCH}} ArchitecturesInstallIn64BitMode={{ARCH}} CloseApplications=yes CloseApplicationsFilter={{EXECUTABLE_NAME}},BettboxCore.exe,BettboxHelperService.exe SetupLogging=yes [Code] var ShouldCleanUserData: Boolean; function IsProcessRunning(ProcessName: String): Boolean; var ResultCode: Integer; begin Exec('cmd.exe', '/c tasklist /fi "imagename eq ' + ProcessName + '" 2>nul | find /i "' + ProcessName + '" >nul', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Result := (ResultCode = 0); end; procedure ForceKillProcesses; var ResultCode: Integer; Processes: TArrayOfString; i: Integer; WaitCount: Integer; begin if IsProcessRunning('BettboxHelperService.exe') then begin Exec('sc', 'stop BettboxHelperService', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); WaitCount := 0; while (WaitCount < 5) and IsProcessRunning('BettboxHelperService.exe') do begin Sleep(400); WaitCount := WaitCount + 1; end; if IsProcessRunning('BettboxHelperService.exe') then Exec('taskkill', '/f /im BettboxHelperService.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; Processes := ['Bettbox.exe', 'BettboxCore.exe']; for i := 0 to GetArrayLength(Processes)-1 do begin if IsProcessRunning(Processes[i]) then Exec('taskkill', '/f /im ' + Processes[i], '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; end; procedure CleanWintunDevices; var ResultCode: Integer; PowerShellScript: String; TempScriptPath: String; begin PowerShellScript := '$ErrorActionPreference = ''SilentlyContinue'';' + #13#10 + 'Write-Host "Cleaning old Wintun/Bettbox/LiClash network adapters...";' + #13#10 + '$adapters = Get-NetAdapter | Where-Object {' + #13#10 + '$_.InterfaceDescription -like "*Bettbox*" -or' + #13#10 + ' $_.Name -like "*Bettbox*"' + #13#10 + '};' + #13#10 + 'if ($adapters) {' + #13#10 + ' foreach ($adapter in $adapters) {' + #13#10 + ' Write-Host "Removing adapter: $($adapter.Name) - $($adapter.InterfaceDescription)";' + #13#10 + ' try {' + #13#10 + ' $adapter | Disable-NetAdapter -Confirm:$false -ErrorAction Stop;' + #13#10 + ' Start-Sleep -Milliseconds 500;' + #13#10 + ' $adapter | Remove-NetAdapter -Confirm:$false -ErrorAction Stop;' + #13#10 + ' Write-Host "Successfully removed: $($adapter.Name)";' + #13#10 + ' } catch {' + #13#10 + ' Write-Host "Failed to remove $($adapter.Name): $_";' + #13#10 + ' }' + #13#10 + ' }' + #13#10 + '} else {' + #13#10 + ' Write-Host "No old adapters found.";' + #13#10 + '}' + #13#10 + 'Write-Host "Cleanup completed.";'; TempScriptPath := ExpandConstant('{tmp}\clean_wintun.ps1'); SaveStringToFile(TempScriptPath, PowerShellScript, False); Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + TempScriptPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); DeleteFile(TempScriptPath); end; procedure RegisterHelperService; var ResultCode: Integer; HelperPath: String; ServiceName: String; begin ServiceName := 'BettboxHelperService'; HelperPath := ExpandConstant('{app}\BettboxHelperService.exe'); Exec('sc', 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec('sc', 'delete ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec('sc', 'create ' + ServiceName + ' binPath= "' + HelperPath + '" start= auto', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec('sc', 'start ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; procedure UnregisterHelperService; var ResultCode: Integer; ServiceName: String; begin ServiceName := 'BettboxHelperService'; Exec('sc', 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec('sc', 'delete ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; procedure UnregisterTaskScheduler; var ResultCode: Integer; TaskNames: TArrayOfString; i: Integer; begin TaskNames := ['Bettbox']; for i := 0 to GetArrayLength(TaskNames)-1 do begin Exec('schtasks', '/Delete /TN ' + TaskNames[i] + ' /F', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; end; procedure CleanRegistry; var RegistryKeys: TArrayOfString; i: Integer; begin SetArrayLength(RegistryKeys, 2); RegistryKeys[0] := 'Software\com.appshub.bettbox'; RegistryKeys[1] := 'Software\com.appshub\Bettbox'; for i := 0 to GetArrayLength(RegistryKeys)-1 do begin RegDeleteKeyIncludingSubkeys(HKCU, RegistryKeys[i]); end; end; procedure CleanUserData; var UserDataPaths: TArrayOfString; i: Integer; AppDataPath: String; begin AppDataPath := ExpandConstant('{userappdata}'); SetArrayLength(UserDataPaths, 2); UserDataPaths[0] := AppDataPath + '\com.appshub.bettbox'; UserDataPaths[1] := AppDataPath + '\com.appshub\Bettbox'; for i := 0 to GetArrayLength(UserDataPaths)-1 do begin if DirExists(UserDataPaths[i]) then begin DelTree(UserDataPaths[i], True, True, True); end; end; if DirExists(AppDataPath + '\com.appshub') then begin RemoveDir(AppDataPath + '\com.appshub'); end; end; function InitializeSetup(): Boolean; begin Result := True; end; function InitializeUninstall(): Boolean; var Response: Integer; begin Response := MsgBox(CustomMessage('RemoveUserDataPrompt'), mbConfirmation, MB_YESNOCANCEL); if Response = IDCANCEL then begin Result := False; end else begin ShouldCleanUserData := (Response = IDYES); Result := True; end; end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssInstall then begin { Let Inno Setup try CloseApplications first; force-kill any leftovers before files are copied. } ForceKillProcesses; end; if CurStep = ssPostInstall then begin RegisterHelperService; end; end; procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); begin if CurUninstallStep = usUninstall then begin ForceKillProcesses; CleanWintunDevices; end; if CurUninstallStep = usPostUninstall then begin UnregisterHelperService; UnregisterTaskScheduler; if ShouldCleanUserData then begin CleanUserData; CleanRegistry; end; end; end; [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" {% if LOCALES %} {% for locale in LOCALES %} {% if locale.lang == 'zh' %} Name: "chineseSimplified"; MessagesFile: {% if locale.file %}{{ locale.file }}{% else %}"compiler:Languages\\ChineseSimplified.isl"{% endif %} {% endif %} {% endfor %} {% endif %} [CustomMessages] english.RemoveUserDataPrompt=Do you want to remove all user data?%n%nThis will remove:%n• Configuration files%n• Profiles and subscriptions%n• Settings and preferences%n• Registry entries%n%nThis action cannot be undone. {% if LOCALES %} {% for locale in LOCALES %} {% if locale.lang == 'zh' %} chineseSimplified.RemoveUserDataPrompt=是否要删除所有用户数据?%n%n这将删除:%n• 配置文件%n• 代理配置与订阅%n• 设置和偏好%n• 注册表记录%n%n此操作无法撤销。 {% endif %} {% endfor %} {% endif %} [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %} [Files] Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}" Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon [Run] Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent ================================================ FILE: windows/packaging/exe/make_config.yaml ================================================ script_template: inno_setup.iss app_id: 728B3532-C74B-4870-9068-BE70FE1LITES app_name: Bettbox publisher: appshub.cc publisher_url: https://github.com/appshubcc/Bettbox display_name: Bettbox executable_name: Bettbox.exe output_base_file_name: Bettbox.exe setup_icon_file: ..\windows\runner\resources\app_icon.ico locales: - lang: zh file: ..\windows\packaging\exe\ChineseSimplified.isl - lang: en privileges_required: admin ================================================ FILE: windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the build version. target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.appshub" "\0" VALUE "FileDescription", "Bettbox" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "bettbox" "\0" VALUE "LegalCopyright", "Copyright (C) 2025 com.appshub. All rights reserved." "\0" VALUE "OriginalFilename", "Bettbox.exe" "\0" VALUE "ProductName", "Bettbox" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include #include #include // For ITaskbarList3 #include "flutter/generated_plugin_registrant.h" #include "resource.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); // Register app method channel SetupAppMethodChannel(); // Load and apply saved icon preference bool use_light_icon = LoadIconPreference(); SetWindowIcon(use_light_icon); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } void FlutterWindow::SetupAppMethodChannel() { auto channel = std::make_unique>( flutter_controller_->engine()->messenger(), "app", &flutter::StandardMethodCodec::GetInstance()); channel->SetMethodCallHandler( [this](const auto& call, auto result) { if (call.method_name() == "setLauncherIcon") { const auto* arguments = std::get_if(call.arguments()); if (arguments) { auto use_light_icon_it = arguments->find(flutter::EncodableValue("useLightIcon")); if (use_light_icon_it != arguments->end()) { bool use_light_icon = std::get(use_light_icon_it->second); bool success = SetWindowIcon(use_light_icon); result->Success(flutter::EncodableValue(success)); return; } } result->Error("INVALID_ARGUMENT", "Missing useLightIcon argument"); } else { result->NotImplemented(); } }); } bool FlutterWindow::SetWindowIcon(bool use_light_icon) { HWND hwnd = GetHandle(); if (!hwnd) { return false; } std::wstring icon_name = use_light_icon ? L"icon_light.ico" : L"icon.ico"; wchar_t exe_path_buf[MAX_PATH] = {0}; DWORD exe_path_len = GetModuleFileNameW(NULL, exe_path_buf, MAX_PATH); std::wstring exe_path = exe_path_len > 0 ? std::wstring(exe_path_buf) : L""; std::wstring base_dir = L"."; size_t last_slash = exe_path.find_last_of(L"\\/"); if (!exe_path.empty() && last_slash != std::wstring::npos) { base_dir = exe_path.substr(0, last_slash); } std::wstring icon_path = base_dir + L"\\data\\flutter_assets\\assets\\images\\" + icon_name; // Load icon file HICON hIcon = (HICON)LoadImageW( NULL, icon_path.c_str(), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED ); if (!hIcon) { // Fallback to app resource if load fails hIcon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_APP_ICON)); if (!hIcon) { return false; } } // Set window icon (title bar) SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon); SetClassLongPtr(hwnd, GCLP_HICON, (LONG_PTR)hIcon); SetClassLongPtr(hwnd, GCLP_HICONSM, (LONG_PTR)hIcon); // Update taskbar icon // Method: Refresh via ITaskbarList3 interface ITaskbarList3* pTaskbarList = nullptr; HRESULT hr = CoCreateInstance( CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_ITaskbarList3, (void**)&pTaskbarList ); if (SUCCEEDED(hr) && pTaskbarList) { pTaskbarList->HrInit(); // Refresh taskbar button to update icon pTaskbarList->AddTab(hwnd); pTaskbarList->DeleteTab(hwnd); pTaskbarList->AddTab(hwnd); pTaskbarList->Release(); } RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW | RDW_ALLCHILDREN); // Save preference to registry SaveIconPreference(use_light_icon); return true; } void FlutterWindow::SaveIconPreference(bool use_light_icon) { HKEY hKey; LONG result = RegCreateKeyExW( HKEY_CURRENT_USER, L"Software\\Bettbox", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL ); if (result == ERROR_SUCCESS) { DWORD value = use_light_icon ? 1 : 0; RegSetValueExW(hKey, L"UseLightIcon", 0, REG_DWORD, (BYTE*)&value, sizeof(DWORD)); RegCloseKey(hKey); } } bool FlutterWindow::LoadIconPreference() { HKEY hKey; LONG result = RegOpenKeyExW( HKEY_CURRENT_USER, L"Software\\Bettbox", 0, KEY_READ, &hKey ); if (result == ERROR_SUCCESS) { DWORD value = 0; DWORD size = sizeof(DWORD); result = RegQueryValueExW(hKey, L"UseLightIcon", NULL, NULL, (BYTE*)&value, &size); RegCloseKey(hKey); if (result == ERROR_SUCCESS) { return value != 0; } } return false; } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; // Setup app method channel void SetupAppMethodChannel(); // Set window icon bool SetWindowIcon(bool use_light_icon); // Save icon preference void SaveIconPreference(bool use_light_icon); // Load icon preference bool LoadIconPreference(); }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.Create(L"Bettbox", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character int input_length = (int)wcslen(utf16_string); std::string utf8_string; if (target_length <= 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include "app_links/app_links_plugin_c_api.h" #include #include #include "resource.h" namespace { /// Window attribute that enables dark mode window decorations. /// /// Redefined in case the developer's machine has a Windows SDK older than /// version 10.0.22000.0. /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; /// Registry key for app theme preference. /// /// A value of 0 indicates apps should use dark mode. A non-zero or missing /// value indicates apps should use light mode. constexpr const wchar_t kGetPreferredBrightnessRegKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); } FreeLibrary(user32_module); } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registrar instance. static WindowClassRegistrar *GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t *GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar *instance_; bool class_registered_ = false; }; WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr; const wchar_t *WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::Create(const std::wstring &title, const Point &origin, const Size &size) { if (SendAppLinkToInstance(title)) { return false; } Destroy(); const wchar_t *window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } UpdateTheme(window); return OnCreate(); } bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } bool Win32Window::SendAppLinkToInstance(const std::wstring &title) { // Find our exact window HWND hwnd = ::FindWindow(kWindowClassName, title.c_str()); if (hwnd) { // Dispatch new link to current window SendAppLink(hwnd); // (Optional) Restore our window to front in same state WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)}; GetWindowPlacement(hwnd, &place); switch (place.showCmd) { case SW_SHOWMAXIMIZED: ShowWindow(hwnd, SW_SHOWMAXIMIZED); break; case SW_SHOWMINIMIZED: ShowWindow(hwnd, SW_RESTORE); break; default: ShowWindow(hwnd, SW_NORMAL); break; } SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); SetForegroundWindow(hwnd); // Window has been found, don't create another one. return true; } return false; } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window *that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; case WM_DWMCOLORIZATIONCOLORCHANGED: UpdateTheme(hwnd); return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } void Win32Window::UpdateTheme(HWND const window) { DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode_size); if (result == ERROR_SUCCESS) { BOOL enable_dark_mode = light_mode == 0; DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode)); } } ================================================ FILE: windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size this function will scale the inputted width and height as // as appropriate for the default monitor. The window is invisible until // |Show| is called. Returns true if the window was created successfully. bool Create(const std::wstring &title, const Point &origin, const Size &size); // Show the current window. Returns true if the window was successfully shown. bool Show(); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; bool SendAppLinkToInstance(const std::wstring &title); // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window *GetThisFromHandle(HWND const window) noexcept; // Update the window frame's theme to match the system theme. static void UpdateTheme(HWND const window); bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_