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 社区交流
[](https://t.me/appshub_chat) [](https://t.me/appshub_channel)
---
### ⬇️ Download / 下载链接
**Note: For desktop CPUs from 2012 or earlier, please download the Compatible version**
**注意:桌面端2012年同期及之前的CPU,需要使用Compatible兼容版本**
---
### 🐛 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 社区交流
[](https://t.me/appshub_chat) [](https://t.me/appshub_channel)
---
### ⬇️ Download / 下载链接
**Note: For desktop CPUs from 2012 or earlier, please download the Compatible version**
**注意:桌面端2012年同期及之前的CPU,需要使用Compatible兼容版本**
---
### 🐛 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