├── rustfmt.toml ├── .cargo ├── config.toml └── publish.config.toml ├── ytflow └── src │ ├── plugin │ ├── dyn_outbound │ │ ├── config.rs │ │ ├── config │ │ │ └── v1.rs │ │ └── dyn_outbound.rs │ ├── obfs.rs │ ├── vmess │ │ ├── protocol.rs │ │ └── protocol │ │ │ ├── body │ │ │ ├── factory.rs │ │ │ ├── shake.rs │ │ │ └── none.rs │ │ │ ├── body.rs │ │ │ └── header │ │ │ └── crypto.rs │ ├── rule_dispatcher │ │ ├── rules.rs │ │ ├── rules │ │ │ ├── geoip.rs │ │ │ ├── ip.rs │ │ │ └── domain.rs │ │ ├── builder │ │ │ ├── geoip.rs │ │ │ └── surge_domainset.rs │ │ └── builder.rs │ ├── tls │ │ ├── mod.rs │ │ ├── initial_data_extract_stream.rs │ │ └── load_certs_windows.rs │ ├── forward.rs │ ├── dyn_outbound.rs │ ├── forward │ │ ├── stats.rs │ │ └── responder.rs │ ├── reject.rs │ ├── netif │ │ ├── sys │ │ │ ├── mod.rs │ │ │ └── apple │ │ │ │ └── bind.rs │ │ ├── mod.rs │ │ └── responder.rs │ ├── simple_dispatcher.rs │ ├── simple_dispatcher │ │ ├── stream.rs │ │ ├── datagram.rs │ │ └── rule.rs │ ├── http_proxy │ │ └── util.rs │ ├── null.rs │ ├── resolve_dest │ │ └── mod.rs │ ├── shadowsocks │ │ ├── factory │ │ │ └── datagram.rs │ │ ├── crypto │ │ │ ├── plain.rs │ │ │ ├── ctor.rs │ │ │ ├── stream.rs │ │ │ ├── cfb128.rs │ │ │ └── aead.rs │ │ ├── crypto.rs │ │ ├── mod.rs │ │ ├── datagram.rs │ │ └── util.rs │ ├── switch │ │ ├── mod.rs │ │ └── responder.rs │ ├── vmess.rs │ ├── dns_server │ │ └── mod.rs │ ├── system_resolver.rs │ ├── rule_dispatcher.rs │ ├── trojan.rs │ ├── ip_stack │ │ └── tcp_socket_entry.rs │ ├── obfs │ │ └── simple_tls │ │ │ ├── template.rs │ │ │ └── packet.rs │ ├── socket │ │ └── udp_listener.rs │ └── fallback.rs │ ├── control.rs │ ├── config │ ├── loader.rs │ ├── mod.rs │ ├── param.rs │ ├── verify.rs │ ├── plugin │ │ ├── system_resolver.rs │ │ ├── reject.rs │ │ ├── null.rs │ │ ├── vpntun.rs │ │ ├── tls_obfs.rs │ │ ├── tls.rs │ │ ├── fakeip.rs │ │ ├── http_proxy.rs │ │ ├── trojan.rs │ │ ├── ip_stack.rs │ │ ├── socket.rs │ │ └── socket_listener.rs │ ├── plugin.rs │ ├── error.rs │ └── loader │ │ └── profile.rs │ ├── data │ ├── migrations │ │ ├── V5__resource_triggers.sql │ │ ├── V3__plugin_cache.sql │ │ ├── V6__proxy_subscription.sql │ │ ├── V2__proxy.sql │ │ ├── V4__resource.sql │ │ └── V1__initial.sql │ ├── error.rs │ ├── plugin_cache.rs │ ├── mod.rs │ ├── db.rs │ └── profile.rs │ ├── flow │ ├── error.rs │ ├── manager.rs │ ├── resolver.rs │ ├── datagram.rs │ ├── tun.rs │ ├── stream.rs │ └── multiplexed_datagram.rs │ ├── flow.rs │ ├── lib.rs │ ├── log │ └── mod.rs │ ├── control │ ├── hub.rs │ └── plugin.rs │ └── plugin.rs ├── ytflow-bin-shared ├── src │ ├── edit │ │ ├── views │ │ │ ├── utils.rs │ │ │ ├── utils │ │ │ │ └── cbor_editor.rs │ │ │ └── new_profile.rs │ │ ├── gen.rs │ │ └── views.rs │ ├── lib.rs │ ├── core │ │ └── fs_resource_loader.rs │ └── edit.rs └── Cargo.toml ├── rust-toolchain.toml ├── ytflow-bin ├── src │ ├── core.rs │ └── edit.rs ├── build.rs └── Cargo.toml ├── ytflow-app-util ├── src │ ├── proxy │ │ ├── obfs │ │ │ ├── tls_obfs.rs │ │ │ ├── http_obfs.rs │ │ │ └── ws.rs │ │ ├── protocol │ │ │ ├── trojan.rs │ │ │ ├── http.rs │ │ │ ├── socks5.rs │ │ │ ├── vmess.rs │ │ │ └── shadowsocks.rs │ │ ├── data.rs │ │ ├── tls.rs │ │ ├── obfs.rs │ │ ├── protocol.rs │ │ └── data │ │ │ └── analyze.rs │ ├── lib.rs │ ├── profile.rs │ ├── share_link.rs │ ├── share_link │ │ ├── vmess.rs │ │ ├── vmess │ │ │ └── v2rayn │ │ │ │ └── stupid_value.rs │ │ └── shadowsocks.rs │ ├── proxy.rs │ ├── subscription.rs │ ├── ffi │ │ ├── cbor.rs │ │ ├── config.rs │ │ ├── share_link.rs │ │ ├── interop.rs │ │ ├── runtime.rs │ │ ├── proxy.rs │ │ └── subscription.rs │ ├── cbor.rs │ ├── subscription │ │ ├── b64_links.rs │ │ └── userinfo.rs │ ├── cbor │ │ └── json.rs │ └── ffi.rs ├── build.rs ├── cbindgen.toml └── Cargo.toml ├── Cargo.toml ├── .gitignore ├── .github └── workflows │ ├── build-tests.yml │ └── build-uwp.yml └── README.md /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [unstable] 2 | bindeps = true 3 | -------------------------------------------------------------------------------- /ytflow/src/plugin/dyn_outbound/config.rs: -------------------------------------------------------------------------------- 1 | pub mod v1; 2 | -------------------------------------------------------------------------------- /ytflow/src/plugin/obfs.rs: -------------------------------------------------------------------------------- 1 | pub mod simple_http; 2 | pub mod simple_tls; 3 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/edit/views/utils.rs: -------------------------------------------------------------------------------- 1 | mod cbor_editor; 2 | 3 | pub use cbor_editor::open_editor_for_cbor; 4 | -------------------------------------------------------------------------------- /ytflow/src/control.rs: -------------------------------------------------------------------------------- 1 | mod hub; 2 | mod plugin; 3 | pub mod rpc; 4 | 5 | pub use hub::*; 6 | pub use plugin::*; 7 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-02-10" 3 | components = ["rustfmt", "rust-src"] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /ytflow/src/plugin/vmess/protocol.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod body; 2 | pub(super) mod header; 3 | 4 | pub(crate) const USER_ID_LEN: usize = 16; 5 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher/rules.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod domain; 2 | pub(super) mod geoip; 3 | pub(super) mod ip; 4 | 5 | pub use geoip::GeoIpSet; 6 | -------------------------------------------------------------------------------- /ytflow/src/plugin/tls/mod.rs: -------------------------------------------------------------------------------- 1 | mod initial_data_extract_stream; 2 | #[cfg(windows)] 3 | mod load_certs_windows; 4 | mod stream; 5 | 6 | pub use stream::SslStreamFactory; 7 | -------------------------------------------------------------------------------- /ytflow-bin/src/core.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | extern "C" { 3 | fn ytflow_bin_exec_core(); 4 | } 5 | unsafe { 6 | ytflow_bin_exec_core(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ytflow-bin/src/edit.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | extern "C" { 3 | fn ytflow_bin_exec_edit(); 4 | } 5 | unsafe { 6 | ytflow_bin_exec_edit(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/obfs/tls_obfs.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 4 | pub struct TlsObfsObfs { 5 | pub host: String, 6 | } 7 | -------------------------------------------------------------------------------- /ytflow/src/config/loader.rs: -------------------------------------------------------------------------------- 1 | mod profile; 2 | #[cfg(feature = "plugins")] 3 | pub(crate) mod proxy; 4 | 5 | #[cfg(feature = "plugins")] 6 | pub use profile::ProfileLoadResult; 7 | pub use profile::ProfileLoader; 8 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/obfs/http_obfs.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 4 | pub struct HttpObfsObfs { 5 | pub host: String, 6 | pub path: String, 7 | } 8 | -------------------------------------------------------------------------------- /ytflow-app-util/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "ffi", feature(ptr_metadata))] 2 | 3 | pub mod cbor; 4 | #[cfg(feature = "ffi")] 5 | pub mod ffi; 6 | pub mod profile; 7 | pub mod proxy; 8 | pub mod share_link; 9 | pub mod subscription; 10 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/protocol/trojan.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_bytes::ByteBuf; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 5 | pub struct TrojanProxy { 6 | pub password: ByteBuf, 7 | } 8 | -------------------------------------------------------------------------------- /ytflow-app-util/src/profile.rs: -------------------------------------------------------------------------------- 1 | mod export; 2 | mod import; 3 | 4 | pub use export::export_profile_toml; 5 | pub use import::{ 6 | parse_profile_toml, ParseTomlProfileError, ParseTomlProfileResult, ParsedTomlPlugin, 7 | ParsedTomlProfile, 8 | }; 9 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/data.rs: -------------------------------------------------------------------------------- 1 | mod analyze; 2 | mod compose_v1; 3 | mod v1; 4 | 5 | pub use analyze::{analyze_data_proxy, AnalyzeError, AnalyzeResult}; 6 | pub use compose_v1::{compose_data_proxy as compose_data_proxy_v1, ComposeError, ComposeResult}; 7 | -------------------------------------------------------------------------------- /ytflow/src/plugin/forward.rs: -------------------------------------------------------------------------------- 1 | mod datagram; 2 | mod responder; 3 | mod stats; 4 | mod stream; 5 | 6 | pub use datagram::DatagramForwardHandler; 7 | pub use responder::Responder; 8 | pub use stats::StatHandle; 9 | pub use stream::StreamForwardHandler; 10 | -------------------------------------------------------------------------------- /ytflow/src/data/migrations/V5__resource_triggers.sql: -------------------------------------------------------------------------------- 1 | CREATE TRIGGER [yt_resources_updated] 2 | AFTER UPDATE ON `yt_resources` 3 | FOR EACH ROW 4 | BEGIN 5 | UPDATE `yt_resources` SET `updated_at` = (strftime('%Y-%m-%d %H:%M:%f', 'now')) WHERE `id` = old.`id`; 6 | END 7 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/protocol/http.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_bytes::ByteBuf; 3 | 4 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 5 | pub struct HttpProxy { 6 | pub username: ByteBuf, 7 | pub password: ByteBuf, 8 | } 9 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/protocol/socks5.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_bytes::ByteBuf; 3 | 4 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 5 | pub struct Socks5Proxy { 6 | pub username: ByteBuf, 7 | pub password: ByteBuf, 8 | } 9 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/tls.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 4 | pub struct ProxyTlsLayer { 5 | pub alpn: Vec, 6 | pub sni: Option, 7 | pub skip_cert_check: Option, 8 | } 9 | -------------------------------------------------------------------------------- /ytflow-app-util/src/share_link.rs: -------------------------------------------------------------------------------- 1 | mod decode; 2 | mod encode; 3 | mod http; 4 | pub mod shadowsocks; 5 | mod socks5; 6 | mod trojan; 7 | mod vmess; 8 | 9 | pub use decode::{decode_share_link, DecodeError, DecodeResult}; 10 | pub use encode::{encode_share_link, EncodeError, EncodeResult}; 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["ytflow", "ytflow-app-util", "ytflow-bin", "ytflow-bin-shared"] 3 | resolver = "2" 4 | 5 | [patch.crates-io] 6 | rusqlite = { git = "https://github.com/YtFlow/rusqlite", branch = "winsqlite-0.31" } 7 | 8 | [profile.release] 9 | debug = true 10 | lto = true 11 | codegen-units = 1 12 | -------------------------------------------------------------------------------- /ytflow/src/data/migrations/V3__plugin_cache.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `yt_plugin_cache` ( 2 | `id` INTEGER PRIMARY KEY, 3 | `plugin_id` INTEGER NOT NULL REFERENCES `yt_plugins`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 4 | `key` VARCHAR(255) NOT NULL, 5 | `value` TEXT NOT NULL, 6 | UNIQUE (`plugin_id`, `key`) 7 | ); 8 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/protocol/vmess.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use ytflow::plugin::vmess::SupportedSecurity; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 5 | pub struct VMessProxy { 6 | pub user_id: uuid::Uuid, 7 | pub alter_id: u16, 8 | pub security: SupportedSecurity, 9 | } 10 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/edit/gen.rs: -------------------------------------------------------------------------------- 1 | use serde_bytes::ByteBuf; 2 | 3 | pub mod plugins; 4 | pub mod profiles; 5 | pub mod proxy_types; 6 | 7 | fn serialize_cbor(val: ciborium::value::Value) -> ByteBuf { 8 | let mut buf = Vec::new(); 9 | ciborium::ser::into_writer(&val, &mut buf).expect("Cannot serialize CBOR"); 10 | ByteBuf::from(buf) 11 | } 12 | -------------------------------------------------------------------------------- /ytflow/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | pub mod factory; 3 | mod human_repr; 4 | pub mod loader; 5 | mod param; 6 | pub mod plugin; 7 | #[cfg(feature = "plugins")] 8 | mod set; 9 | pub mod verify; 10 | 11 | pub use error::*; 12 | pub use human_repr::HumanRepr; 13 | pub use plugin::Plugin; 14 | #[cfg(feature = "plugins")] 15 | pub use set::PluginSet; 16 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/protocol/shadowsocks.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_bytes::ByteBuf; 3 | 4 | use ytflow::plugin::shadowsocks::SupportedCipher; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 7 | pub struct ShadowsocksProxy { 8 | pub cipher: SupportedCipher, 9 | pub password: ByteBuf, 10 | } 11 | -------------------------------------------------------------------------------- /ytflow-app-util/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | println!("cargo:rerun-if-changed=include/ytflow_core.h"); 5 | println!("cargo:rerun-if-changed=cbindgen.toml"); 6 | let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); 7 | 8 | cbindgen::generate(crate_dir) 9 | .expect("Unable to generate bindings") 10 | .write_to_file("include/ytflow_core.h"); 11 | } 12 | -------------------------------------------------------------------------------- /ytflow/src/plugin/dyn_outbound.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | #[cfg(feature = "plugins")] 3 | mod dyn_outbound; 4 | #[cfg(feature = "plugins")] 5 | mod responder; 6 | #[cfg(feature = "plugins")] 7 | mod select; 8 | 9 | #[cfg(feature = "plugins")] 10 | pub use dyn_outbound::DynOutbound; 11 | #[cfg(feature = "plugins")] 12 | pub use responder::Responder; 13 | 14 | pub const PLUGIN_CACHE_KEY_LAST_SELECT: &str = "last_select"; 15 | -------------------------------------------------------------------------------- /ytflow/src/flow/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum FlowError { 5 | #[error("IO Error")] 6 | Io(#[from] std::io::Error), 7 | #[error("End of stream")] 8 | Eof, 9 | #[error("Unexpected data received")] 10 | UnexpectedData, 11 | #[error("Cannot find a matching outbound")] 12 | NoOutbound, 13 | } 14 | 15 | pub type FlowResult = Result; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | # Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # Test SQLite database file 14 | /test.db 15 | -------------------------------------------------------------------------------- /ytflow/src/plugin/forward/stats.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU32, AtomicU64}; 2 | use std::sync::Arc; 3 | 4 | #[derive(Default)] 5 | pub struct StatInner { 6 | pub uplink_written: AtomicU64, 7 | pub downlink_written: AtomicU64, 8 | pub tcp_connection_count: AtomicU32, 9 | pub udp_session_count: AtomicU32, 10 | } 11 | 12 | #[derive(Clone, Default)] 13 | pub struct StatHandle { 14 | pub inner: Arc, 15 | } 16 | -------------------------------------------------------------------------------- /ytflow/src/flow.rs: -------------------------------------------------------------------------------- 1 | mod compat; 2 | mod context; 3 | mod datagram; 4 | mod error; 5 | mod manager; 6 | mod multiplexed_datagram; 7 | mod reader; 8 | mod resolver; 9 | mod stream; 10 | mod tun; 11 | 12 | pub use compat::*; 13 | pub use context::*; 14 | pub use datagram::*; 15 | pub use error::*; 16 | pub use manager::*; 17 | pub use multiplexed_datagram::*; 18 | pub use reader::StreamReader; 19 | pub use resolver::*; 20 | pub use stream::*; 21 | pub use tun::*; 22 | -------------------------------------------------------------------------------- /ytflow/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_unsafe)] 2 | #![feature(generic_const_exprs)] 3 | #![feature(stmt_expr_attributes)] 4 | #![feature(let_chains)] 5 | #![feature(ip)] 6 | #![feature(const_option)] 7 | #![feature(result_flattening)] 8 | #![feature(lazy_cell)] 9 | 10 | pub mod config; 11 | #[cfg(feature = "plugins")] 12 | pub mod control; 13 | pub mod data; 14 | pub mod flow; 15 | pub mod log; 16 | pub mod plugin; 17 | pub mod resource; 18 | 19 | pub use tokio; 20 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/obfs.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | mod http_obfs; 4 | mod tls_obfs; 5 | mod ws; 6 | 7 | pub use http_obfs::HttpObfsObfs; 8 | pub use tls_obfs::TlsObfsObfs; 9 | pub use ws::WebSocketObfs; 10 | 11 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 12 | pub enum ProxyObfsType { 13 | HttpObfs(http_obfs::HttpObfsObfs), 14 | TlsObfs(tls_obfs::TlsObfsObfs), 15 | WebSocket(ws::WebSocketObfs), 16 | } 17 | -------------------------------------------------------------------------------- /ytflow-bin/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | if std::env::var_os("CARGO_CFG_WINDOWS").is_some() { 3 | println!("cargo:rustc-link-lib=dylib=ytflow_bin_shared.dll"); 4 | } else { 5 | println!("cargo:rustc-link-lib=dylib=ytflow_bin_shared"); 6 | } 7 | println!( 8 | "cargo:rustc-link-search={}", 9 | std::env::var("CARGO_CDYLIB_DIR_YTFLOW_BIN_SHARED").unwrap() 10 | ); 11 | println!("cargo:rerun-if-changed=build.rs"); 12 | } 13 | -------------------------------------------------------------------------------- /ytflow/src/plugin/reject.rs: -------------------------------------------------------------------------------- 1 | use crate::flow::*; 2 | 3 | pub struct RejectHandler; 4 | 5 | impl StreamHandler for RejectHandler { 6 | fn on_stream(&self, lower: Box, _initial_data: Buffer, _context: Box) { 7 | drop(lower); 8 | } 9 | } 10 | 11 | impl DatagramSessionHandler for RejectHandler { 12 | fn on_session(&self, lower: Box, _context: Box) { 13 | drop(lower); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ytflow/src/config/param.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use super::{ConfigError, ConfigResult}; 4 | 5 | pub(super) fn parse_param<'de, T: Deserialize<'de>, D: AsRef<[u8]> + 'de>( 6 | plugin_name: &str, 7 | data: &'de D, 8 | ) -> ConfigResult { 9 | // TODO: Extract detailed error to identify the fields that contain error 10 | cbor4ii::serde::from_slice(data.as_ref()) 11 | .map_err(|e| ConfigError::ParseParam(plugin_name.to_string(), e)) 12 | } 13 | -------------------------------------------------------------------------------- /ytflow-bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ytflow-bin" 3 | version = "0.7.3" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [[bin]] 9 | name = "ytflow-core" 10 | path = "src/core.rs" 11 | 12 | [[bin]] 13 | name = "ytflow-edit" 14 | path = "src/edit.rs" 15 | 16 | [dependencies] 17 | 18 | [build-dependencies] 19 | ytflow-bin-shared = { path = "../ytflow-bin-shared", artifact = "cdylib" } 20 | -------------------------------------------------------------------------------- /ytflow/src/data/migrations/V6__proxy_subscription.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `yt_proxy_subscriptions` ( 2 | `id` INTEGER PRIMARY KEY, 3 | `proxy_group_id` INTEGER NOT NULL REFERENCES `yt_proxy_groups`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 4 | `format` VARCHAR(63) NOT NULL, 5 | `url` TEXT NOT NULL, 6 | `update_frequency` VARCHAR(63), 7 | `upload_bytes_used` INTEGER, 8 | `download_bytes_used` INTEGER, 9 | `bytes_total` INTEGER, 10 | `expires_at` TEXT, 11 | `additional_info` TEXT, 12 | `retrieved_at` TEXT 13 | ); 14 | -------------------------------------------------------------------------------- /ytflow/src/plugin/netif/sys/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | mod win; 3 | #[cfg(windows)] 4 | pub(super) use win::*; 5 | 6 | #[cfg(target_os = "linux")] 7 | mod linux; 8 | #[cfg(target_os = "linux")] 9 | pub(super) use linux::*; 10 | 11 | #[cfg(any(target_os = "macos", target_os = "ios"))] 12 | mod apple; 13 | #[cfg(any(target_os = "macos", target_os = "ios"))] 14 | pub(super) use apple::*; 15 | 16 | #[cfg(not(any(windows, target_os = "linux", target_os = "macos", target_os = "ios")))] 17 | compile_error!("Target OS does not support netif features"); 18 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/obfs/ws.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 6 | pub struct WebSocketObfs { 7 | pub host: Option, 8 | pub path: String, 9 | pub headers: HashMap, 10 | } 11 | 12 | impl Default for WebSocketObfs { 13 | fn default() -> Self { 14 | Self { 15 | host: None, 16 | path: "/".into(), 17 | headers: HashMap::new(), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ytflow/src/flow/manager.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Manager { 3 | pub rt: tokio::runtime::Runtime, 4 | } 5 | 6 | impl Manager { 7 | pub fn new() -> Self { 8 | let rt = tokio::runtime::Runtime::new().unwrap(); 9 | // let manager = Arc::new(Self { rt }); 10 | // let ip_stack = IpStack::new(manager.clone()); 11 | // (manager, ip_stack) 12 | Self { rt } 13 | // TODO: initialize other plugins 14 | } 15 | 16 | // pub(crate) fn query_node_factory(&self, node_handle) -> Box { 17 | 18 | // } 19 | } 20 | -------------------------------------------------------------------------------- /.cargo/publish.config.toml: -------------------------------------------------------------------------------- 1 | # Refer to https://github.com/shadowsocks/shadowsocks-rust/blob/93f0fb7fa9e3d5a421616bf1da3d649e3f1c4b17/.cargo/config.toml 2 | 3 | [target.'cfg(target_arch = "x86_64")'] 4 | rustflags = [ 5 | "-C", 6 | "target-feature=+sse,+sse2,+sse3,+ssse3,+sse4.1,+sse4.2,+avx,+avx2,+aes,+pclmulqdq", 7 | ] 8 | 9 | [target.'cfg(target_arch = "x86")'] 10 | rustflags = ["-C", "target-feature=+sse,+sse2,+sse3,+ssse3"] 11 | 12 | [target.'cfg(target_arch = "aarch64")'] 13 | rustflags = ["-C", "target-feature=+neon,+aes,+sha2,+crc"] 14 | 15 | [unstable] 16 | bindeps = true 17 | -------------------------------------------------------------------------------- /ytflow-app-util/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | namespace = "ytflow_core" 3 | pragma_once = true 4 | after_includes = ''' 5 | #ifdef __cplusplus 6 | namespace ytflow_core { 7 | #endif // __cplusplus 8 | 9 | typedef struct ytflow_connection ytflow_connection; 10 | typedef struct ytflow_database ytflow_database; 11 | 12 | #ifdef __cplusplus 13 | } // namespace ytflow_core 14 | #endif // __cplusplus 15 | ''' 16 | cpp_compat = true 17 | 18 | [parse] 19 | parse_deps = false 20 | 21 | [defines] 22 | "windows" = "_WIN32" 23 | "unix" = "__unix__" 24 | 25 | [export.mangle] 26 | rename_types = "SnakeCase" 27 | -------------------------------------------------------------------------------- /ytflow/src/flow/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, Ipv6Addr}; 2 | 3 | use async_trait::async_trait; 4 | use smallvec::SmallVec; 5 | 6 | pub type ResolvedV4 = SmallVec<[Ipv4Addr; 4]>; 7 | pub type ResolvedV6 = SmallVec<[Ipv6Addr; 2]>; 8 | pub type ResolveResultV4 = super::FlowResult>; 9 | pub type ResolveResultV6 = super::FlowResult>; 10 | 11 | #[async_trait] 12 | pub trait Resolver: Send + Sync { 13 | async fn resolve_ipv4(&self, domain: String) -> ResolveResultV4; 14 | async fn resolve_ipv6(&self, domain: String) -> ResolveResultV6; 15 | } 16 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(exitcode_exit_method)] 2 | #![feature(let_chains)] 3 | 4 | use std::process::ExitCode; 5 | 6 | pub mod core; 7 | pub mod edit; 8 | 9 | fn execute_main(main: impl FnOnce() -> Result<(), E>) { 10 | match main() { 11 | Ok(_) => ExitCode::SUCCESS, 12 | Err(_) => ExitCode::FAILURE, 13 | } 14 | .exit_process() 15 | } 16 | 17 | #[no_mangle] 18 | pub extern "C" fn ytflow_bin_exec_core() { 19 | execute_main(core::main) 20 | } 21 | 22 | #[no_mangle] 23 | pub extern "C" fn ytflow_bin_exec_edit() { 24 | execute_main(edit::main) 25 | } 26 | -------------------------------------------------------------------------------- /ytflow-app-util/src/share_link/vmess.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | mod v2rayn; 4 | 5 | use super::decode::{DecodeResult, QueryMap}; 6 | use super::encode::EncodeResult; 7 | use crate::proxy::protocol::VMessProxy; 8 | use crate::proxy::{Proxy, ProxyLeg}; 9 | 10 | impl VMessProxy { 11 | pub(super) fn decode_share_link(url: &Url, queries: &mut QueryMap) -> DecodeResult { 12 | v2rayn::decode_v2rayn(url, queries) 13 | } 14 | pub(super) fn encode_share_link(&self, leg: &ProxyLeg, proxy: &Proxy) -> EncodeResult { 15 | v2rayn::encode_v2rayn(self, leg, proxy) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ytflow/src/log/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | #[cfg(windows)] 3 | pub fn debug_log(log: impl AsRef) { 4 | use std::os::windows::ffi::OsStrExt; 5 | 6 | #[link(name = "Kernel32")] 7 | extern "system" { 8 | fn OutputDebugStringW(lp_output_string: *const u16); 9 | } 10 | 11 | let mut bytes: Vec = log.as_ref().encode_wide().collect(); 12 | bytes.extend_from_slice(&[13, 10, 0u16][..]); 13 | unsafe { OutputDebugStringW(bytes.as_ptr()) }; 14 | } 15 | 16 | #[cfg(not(windows))] 17 | pub fn debug_log(log: impl AsRef) { 18 | eprintln!("{}", log.as_ref()); 19 | } 20 | -------------------------------------------------------------------------------- /ytflow/src/plugin/simple_dispatcher.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use cidr::IpCidr; 4 | use serde::Deserialize; 5 | use smallvec::SmallVec; 6 | 7 | #[cfg(feature = "plugins")] 8 | pub mod datagram; 9 | #[cfg(feature = "plugins")] 10 | mod rule; 11 | #[cfg(feature = "plugins")] 12 | pub mod stream; 13 | 14 | use crate::config::HumanRepr; 15 | 16 | #[cfg(feature = "plugins")] 17 | pub use rule::Rule; 18 | 19 | #[derive(Clone, Deserialize)] 20 | pub struct Condition { 21 | pub ip_ranges: SmallVec<[HumanRepr; 2]>, 22 | pub port_ranges: SmallVec<[HumanRepr>; 4]>, 23 | } 24 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use ytflow::flow::DestinationAddr; 4 | 5 | pub mod data; 6 | pub mod obfs; 7 | pub mod protocol; 8 | pub mod tls; 9 | 10 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 11 | pub struct Proxy { 12 | pub name: String, 13 | pub legs: Vec, 14 | pub udp_supported: bool, 15 | } 16 | 17 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 18 | pub struct ProxyLeg { 19 | pub protocol: protocol::ProxyProtocolType, 20 | pub dest: DestinationAddr, 21 | pub obfs: Option, 22 | pub tls: Option, 23 | } 24 | -------------------------------------------------------------------------------- /ytflow/src/control/hub.rs: -------------------------------------------------------------------------------- 1 | use super::plugin; 2 | 3 | #[derive(Default)] 4 | pub struct ControlHub { 5 | pub(super) plugins: Vec, 6 | } 7 | 8 | impl ControlHub { 9 | pub fn create_plugin_control( 10 | &mut self, 11 | name: String, 12 | plugin: &'static str, 13 | responder: impl plugin::PluginResponder, 14 | ) -> plugin::PluginControlHandle { 15 | self.plugins.push(plugin::PluginController { 16 | id: self.plugins.len() as u32 + 1, 17 | name, 18 | plugin, 19 | responder: Box::new(responder), 20 | }); 21 | plugin::PluginControlHandle {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ytflow/src/plugin/simple_dispatcher/stream.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Weak; 2 | 3 | use super::Rule; 4 | use crate::flow::*; 5 | 6 | type StreamRule = Rule>; 7 | 8 | pub struct SimpleStreamDispatcher { 9 | pub rules: Vec, 10 | pub fallback: Weak, 11 | } 12 | 13 | impl StreamHandler for SimpleStreamDispatcher { 14 | fn on_stream(&self, lower: Box, initial_data: Buffer, context: Box) { 15 | let handler = self 16 | .rules 17 | .iter() 18 | .find_map(|r| r.matches(&context)) 19 | .unwrap_or_else(|| self.fallback.clone()); 20 | if let Some(handler) = handler.upgrade() { 21 | handler.on_stream(lower, initial_data, context) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ytflow-app-util/src/subscription.rs: -------------------------------------------------------------------------------- 1 | mod b64_links; 2 | mod decode; 3 | mod sip008; 4 | mod surge_proxy_list; 5 | mod userinfo; 6 | 7 | use std::ffi::CStr; 8 | 9 | pub use decode::{decode_subscription, decode_subscription_with_format, DecodeError, DecodeResult}; 10 | use serde::Serialize; 11 | pub use userinfo::SubscriptionUserInfo; 12 | 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 14 | pub struct SubscriptionFormat<'a>(pub(crate) &'a [u8]); 15 | 16 | impl From> for &'static CStr { 17 | fn from(s: SubscriptionFormat<'static>) -> &'static CStr { 18 | CStr::from_bytes_with_nul(s.0).expect("format is not null-terminated") 19 | } 20 | } 21 | 22 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 23 | pub struct Subscription { 24 | pub proxies: Vec, 25 | } 26 | -------------------------------------------------------------------------------- /ytflow/src/plugin/simple_dispatcher/datagram.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Weak; 2 | 3 | use super::Rule; 4 | use crate::flow::*; 5 | 6 | type DatagramRule = Rule>; 7 | 8 | pub struct SimpleDatagramDispatcher { 9 | pub rules: Vec, 10 | pub fallback: Weak, 11 | } 12 | 13 | impl DatagramSessionHandler for SimpleDatagramDispatcher { 14 | fn on_session(&self, session: Box, context: Box) { 15 | let handler = self 16 | .rules 17 | .iter() 18 | .find_map(|r| r.matches(&context)) 19 | .unwrap_or_else(|| self.fallback.clone()); 20 | if let Some(handler) = handler.upgrade() { 21 | handler.on_session(session, context) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ytflow/src/data/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum DataError { 5 | #[error("cannot migrate")] 6 | Migration(Box), 7 | #[error("error performing sqlite operations")] 8 | Database(Box), 9 | #[error("field \"{field:?}\" for \"{domain:?}\" is not valid")] 10 | InvalidData { 11 | domain: &'static str, 12 | field: &'static str, 13 | }, 14 | } 15 | 16 | impl From for DataError { 17 | fn from(e: refinery::Error) -> Self { 18 | DataError::Migration(Box::new(e)) 19 | } 20 | } 21 | 22 | impl From for DataError { 23 | fn from(e: rusqlite::Error) -> Self { 24 | DataError::Database(Box::new(e)) 25 | } 26 | } 27 | 28 | pub type DataResult = Result; 29 | -------------------------------------------------------------------------------- /ytflow/src/flow/datagram.rs: -------------------------------------------------------------------------------- 1 | use std::task::{Context, Poll}; 2 | 3 | use async_trait::async_trait; 4 | 5 | use super::*; 6 | 7 | pub trait DatagramSession: Send { 8 | fn poll_recv_from(&mut self, cx: &mut Context) -> Poll>; 9 | fn poll_send_ready(&mut self, cx: &mut Context<'_>) -> Poll<()>; 10 | fn send_to(&mut self, remote_peer: DestinationAddr, buf: Buffer); 11 | fn poll_shutdown(&mut self, cx: &mut Context<'_>) -> Poll>; 12 | } 13 | 14 | pub trait DatagramSessionHandler: Send + Sync { 15 | fn on_session(&self, session: Box, context: Box); 16 | } 17 | 18 | #[async_trait] 19 | pub trait DatagramSessionFactory: Send + Sync { 20 | async fn bind(&self, context: Box) -> FlowResult>; 21 | } 22 | -------------------------------------------------------------------------------- /ytflow/src/config/verify.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use super::factory::{DemandDescriptor, ParsedPlugin, ProvideDescriptor, RequiredResource}; 4 | use super::plugin::Plugin; 5 | use super::ConfigResult; 6 | 7 | #[derive(Debug, Clone, Serialize)] 8 | pub struct VerifyResult<'a> { 9 | #[serde(borrow)] 10 | requires: Vec>, 11 | provides: Vec, 12 | #[serde(borrow)] 13 | resources: Vec>, 14 | } 15 | pub fn verify_plugin(plugin: &'_ Plugin) -> ConfigResult> { 16 | let ParsedPlugin { 17 | provides, 18 | requires, 19 | resources, 20 | factory: _, 21 | } = super::factory::create_factory_from_plugin(plugin)?; 22 | Ok(VerifyResult { 23 | provides, 24 | requires, 25 | resources, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /ytflow/src/plugin/vmess/protocol/body/factory.rs: -------------------------------------------------------------------------------- 1 | use super::{RxCrypto, SizeCrypto, TxCrypto}; 2 | use crate::plugin::vmess::protocol::header::{DATA_IV_LEN, DATA_KEY_LEN}; 3 | 4 | pub trait BodyCryptoFactory { 5 | type Rx: RxCrypto 6 | where 7 | [(); S::LEN]:; 8 | type Tx: TxCrypto 9 | where 10 | [(); S::LEN]:; 11 | const HEADER_SEC_TYPE: u8; 12 | 13 | fn new_tx( 14 | &self, 15 | data_key: &[u8; DATA_KEY_LEN], 16 | data_iv: &[u8; DATA_IV_LEN], 17 | size_crypto: S, 18 | ) -> Self::Tx 19 | where 20 | [(); S::LEN]:; 21 | fn new_rx( 22 | &self, 23 | res_key: &[u8; DATA_KEY_LEN], 24 | res_iv: &[u8; DATA_IV_LEN], 25 | size_crypto: S, 26 | ) -> Self::Rx 27 | where 28 | [(); S::LEN]:; 29 | } 30 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi/cbor.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::os::raw::c_char; 3 | 4 | use super::error::ytflow_result; 5 | use super::interop::{serialize_byte_buffer, serialize_string_buffer}; 6 | 7 | #[no_mangle] 8 | pub unsafe extern "C" fn ytflow_app_cbor_to_json( 9 | cbor: *const u8, 10 | cbor_len: usize, 11 | ) -> ytflow_result { 12 | ytflow_result::catch_result_unwind(move || { 13 | let cbor = std::slice::from_raw_parts(cbor, cbor_len); 14 | crate::cbor::cbor_to_json(cbor).map(|j| serialize_string_buffer(j)) 15 | }) 16 | } 17 | 18 | #[no_mangle] 19 | pub unsafe extern "C" fn ytflow_app_cbor_from_json(json: *const c_char) -> ytflow_result { 20 | ytflow_result::catch_result_unwind(move || { 21 | let json = unsafe { CStr::from_ptr(json).to_string_lossy() }; 22 | crate::cbor::json_to_cbor(&json).map(|c| serialize_byte_buffer(c)) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /ytflow/src/plugin/http_proxy/util.rs: -------------------------------------------------------------------------------- 1 | pub fn format_u16(mut port: u16, buf: &mut [u8; 5]) -> usize { 2 | let mut cursor = 0; 3 | if port >= 10 { 4 | if port >= 100 { 5 | if port >= 1000 { 6 | if port >= 10000 { 7 | buf[cursor] = (b'0' as u16 + port / 10000) as u8; 8 | cursor += 1; 9 | port %= 10000; 10 | } 11 | buf[cursor] = (b'0' as u16 + port / 1000) as u8; 12 | cursor += 1; 13 | port %= 1000; 14 | } 15 | buf[cursor] = (b'0' as u16 + port / 100) as u8; 16 | cursor += 1; 17 | port %= 100; 18 | } 19 | buf[cursor] = (b'0' as u16 + port / 10) as u8; 20 | cursor += 1; 21 | port %= 10; 22 | } 23 | buf[cursor] = (b'0' as u16 + port) as u8; 24 | cursor + 1 25 | } 26 | -------------------------------------------------------------------------------- /ytflow/src/plugin/netif/mod.rs: -------------------------------------------------------------------------------- 1 | // Windows does not provide per-link hostname resolution. 2 | // On Linux, fallback to resolver when sytemd-resolved is not available. 3 | #[cfg(all(feature = "plugins", any(windows, target_os = "linux")))] 4 | mod resolver; 5 | #[cfg(feature = "plugins")] 6 | mod responder; 7 | #[cfg(feature = "plugins")] 8 | mod selector; 9 | #[cfg(feature = "plugins")] 10 | mod sys; 11 | 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[cfg(feature = "plugins")] 15 | pub use responder::Responder; 16 | #[cfg(feature = "plugins")] 17 | pub use selector::NetifSelector; 18 | 19 | #[derive(Clone, Serialize, Deserialize)] 20 | #[serde(tag = "type", content = "netif")] 21 | pub enum SelectionMode { 22 | Auto, 23 | Manual(String), 24 | } 25 | 26 | #[derive(Clone, Copy, Serialize, Deserialize)] 27 | pub enum FamilyPreference { 28 | Both, 29 | Ipv4Only, 30 | Ipv6Only, 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Build and Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, ci-scratch ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-bin: 14 | 15 | strategy: 16 | matrix: 17 | os: ["windows-latest", "ubuntu-latest", "macos-latest"] 18 | 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Prepare toolchain 24 | run: rustup show 25 | - uses: Swatinem/rust-cache@v1 26 | 27 | - name: Install OpenSSL (Windows) 28 | if: ${{ runner.os == 'Windows' }} 29 | run: | 30 | vcpkg install openssl:x64-windows 31 | echo "OPENSSL_DIR=C:\vcpkg\installed\x64-windows" >> $env:GITHUB_ENV 32 | 33 | 34 | - name: Build 35 | run: cargo build 36 | - name: Run tests 37 | run: cargo test 38 | -------------------------------------------------------------------------------- /ytflow/src/data/migrations/V2__proxy.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `yt_proxy_groups` ( 2 | `id` INTEGER PRIMARY KEY, 3 | `name` VARCHAR(255) NOT NULL, 4 | `type` VARCHAR(63) NOT NULL, 5 | `created_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')) 6 | ); 7 | 8 | CREATE TABLE `yt_proxies` ( 9 | `id` INTEGER PRIMARY KEY, 10 | `group_id` INTEGER NOT NULL REFERENCES `yt_proxy_groups`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 11 | `name` VARCHAR(255) NOT NULL, 12 | `order_num` INTEGER NOT NULL, 13 | `proxy` BLOB NOT NULL, 14 | `proxy_version` INT(4) NOT NULL DEFAULT '0', 15 | `updated_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')) 16 | ); 17 | 18 | CREATE TRIGGER [yt_proxies_updated] 19 | AFTER UPDATE ON `yt_proxies` 20 | FOR EACH ROW 21 | BEGIN 22 | UPDATE `yt_proxies` SET `updated_at` = (strftime('%Y-%m-%d %H:%M:%f', 'now')) WHERE `id` = old.`id`; 23 | END 24 | -------------------------------------------------------------------------------- /ytflow/src/plugin/null.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use crate::flow::*; 4 | 5 | pub struct Null; 6 | 7 | #[async_trait] 8 | impl StreamOutboundFactory for Null { 9 | async fn create_outbound( 10 | &self, 11 | _context: &mut FlowContext, 12 | _initial_data: &[u8], 13 | ) -> FlowResult<(Box, Buffer)> { 14 | Err(FlowError::NoOutbound) 15 | } 16 | } 17 | 18 | #[async_trait] 19 | impl DatagramSessionFactory for Null { 20 | async fn bind(&self, _context: Box) -> FlowResult> { 21 | Err(FlowError::NoOutbound) 22 | } 23 | } 24 | 25 | #[async_trait] 26 | impl Resolver for Null { 27 | async fn resolve_ipv4(&self, _domain: String) -> ResolveResultV4 { 28 | Err(FlowError::NoOutbound) 29 | } 30 | async fn resolve_ipv6(&self, _domain: String) -> ResolveResultV6 { 31 | Err(FlowError::NoOutbound) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi/config.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::os::raw::c_char; 3 | 4 | use ytflow::config::verify::verify_plugin; 5 | use ytflow::config::Plugin; 6 | 7 | use super::error::ytflow_result; 8 | use super::interop::serialize_buffer; 9 | 10 | #[no_mangle] 11 | pub unsafe extern "C" fn ytflow_plugin_verify( 12 | plugin: *const c_char, 13 | plugin_version: u16, 14 | param: *const u8, 15 | param_len: usize, 16 | ) -> ytflow_result { 17 | ytflow_result::catch_result_unwind(move || { 18 | let plugin = unsafe { CStr::from_ptr(plugin) }; 19 | let plugin = Plugin { 20 | id: None, 21 | name: String::from("test_plugin"), 22 | plugin: plugin.to_string_lossy().into_owned(), 23 | plugin_version, 24 | param: unsafe { std::slice::from_raw_parts(param, param_len).to_vec() }, 25 | }; 26 | verify_plugin(&plugin).map(|v| serialize_buffer(&v)) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher/rules/geoip.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | use std::sync::Arc; 3 | 4 | use maxminddb::geoip2; 5 | use smallvec::SmallVec; 6 | 7 | use crate::plugin::rule_dispatcher::RuleHandle; 8 | 9 | pub(crate) type GeoIpRuleMap = SmallVec<[(String, RuleHandle); 2]>; 10 | 11 | pub struct GeoIpSet { 12 | pub(crate) geoip_reader: maxminddb::Reader>, 13 | pub(crate) iso_code_rule: GeoIpRuleMap, 14 | } 15 | 16 | impl GeoIpSet { 17 | pub fn query(&self, ip: IpAddr) -> impl Iterator { 18 | let country: Option = self.geoip_reader.lookup(ip).ok(); 19 | country 20 | .and_then(|c| c.country) 21 | .and_then(|c| c.iso_code) 22 | .and_then(|c| { 23 | self.iso_code_rule 24 | .iter() 25 | .find(|(rc, _)| rc == c) 26 | .map(|(_, r)| *r) 27 | }) 28 | .into_iter() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher/builder/geoip.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | use super::*; 4 | 5 | impl RuleSet { 6 | pub fn build_dst_geoip_rule( 7 | code_action_mapping: impl Iterator, 8 | geoip_db: Arc<[u8]>, 9 | ) -> Option { 10 | let rule_id = 1; 11 | 12 | Some(Self { 13 | dst_geoip: Some(GeoIpSet { 14 | iso_code_rule: code_action_mapping 15 | .map(|(mut code, action)| { 16 | code.make_ascii_uppercase(); 17 | (code, RuleHandle::new(action, rule_id)) 18 | }) 19 | .dedup_by(|(code1, _), (code2, _)| code1 == code2) 20 | .collect(), 21 | geoip_reader: maxminddb::Reader::from_source(geoip_db).ok()?, 22 | }), 23 | first_resolving_rule_id: Some(rule_id), 24 | ..Default::default() 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ytflow-app-util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ytflow-app-util" 3 | version = "0.7.3" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [features] 9 | ffi = [] 10 | 11 | [build-dependencies] 12 | cbindgen = { version = "0.26", default-features = false } 13 | 14 | [dependencies] 15 | url = "2" 16 | percent-encoding = "2" 17 | base64 = "0.22" 18 | thiserror = "1" 19 | cbor4ii = { version = "0.3", features = ["use_std", "serde1"] } 20 | serde = { version = "1", features = ["derive"] } 21 | serde_json = { version = "1", features = ["alloc"] } 22 | serde_bytes = "0.11" 23 | ciborium = "0.2" 24 | chrono = { version = "*", features = ["serde"] } 25 | uuid = { version = "1", features = ["serde"] } 26 | rusqlite = { version = "=0.31", default-features = false } 27 | toml_edit = { version = "0.22", default-features = false, features = [ 28 | "parse", 29 | "display", 30 | ] } 31 | hex = "0.4" 32 | ytflow = { path = "../ytflow" } 33 | -------------------------------------------------------------------------------- /ytflow/src/plugin/dyn_outbound/config/v1.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_bytes::ByteBuf; 3 | 4 | pub const BUILTIN_PLUGIN_OUT: &str = "$out"; 5 | pub const BUILTIN_PLUGIN_NULL: &str = "$null"; 6 | pub const BUILTIN_PLUGIN_NAMES: [&str; 2] = [BUILTIN_PLUGIN_OUT, BUILTIN_PLUGIN_NULL]; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct Plugin { 10 | pub name: String, 11 | pub plugin: String, 12 | pub plugin_version: u16, 13 | pub param: ByteBuf, 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct Proxy { 18 | pub tcp_entry: String, 19 | pub udp_entry: Option, 20 | pub plugins: Vec, 21 | } 22 | 23 | impl From for crate::config::Plugin { 24 | fn from(plugin: Plugin) -> Self { 25 | Self { 26 | id: None, 27 | name: plugin.name, 28 | plugin: plugin.plugin, 29 | plugin_version: plugin.plugin_version, 30 | param: plugin.param.into_vec(), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build-uwp.yml: -------------------------------------------------------------------------------- 1 | name: Run Build for UWP 2 | 3 | on: 4 | push: 5 | branches: [main, ci-scratch] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-uwp: 14 | runs-on: windows-latest 15 | env: 16 | VCPKGRS_TRIPLET: x64-uwp 17 | UWP_CROSS_TARGET_TRIPLET: x86_64-uwp-windows-msvc 18 | UWP_NATIVE_TARGET_TRIPLET: x86_64-uwp-windows-msvc 19 | BUILD_STD_ARGS: build-std=std,panic_abort 20 | # THUMBV7A_UWP_WINDOWS_MSVC_OPENSSL_DIR: 'C:\vcpkg\installed\arm-uwp' 21 | X86_64_UWP_WINDOWS_MSVC_OPENSSL_DIR: 'C:\vcpkg\installed\x64-uwp' 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Prepare toolchain 26 | run: rustup show 27 | - uses: Swatinem/rust-cache@v1 28 | 29 | - name: Install OpenSSL 30 | run: vcpkg install openssl:x64-uwp 31 | 32 | - name: Build x64 33 | run: cargo build -p ytflow-app-util -Z $env:BUILD_STD_ARGS --target $env:UWP_CROSS_TARGET_TRIPLET --release 34 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher/rules/ip.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, Ipv6Addr}; 2 | 3 | use cidr::Cidr; 4 | 5 | use super::super::{RuleHandle, RuleSet}; 6 | 7 | fn match_ip_rules<'a, A: cidr::Address + 'a>( 8 | set: &'a [(A::Cidr, RuleHandle)], 9 | ip: A, 10 | ) -> impl Iterator + 'a { 11 | let idx = set.partition_point(|(cidr, _)| ip > cidr.last_address()); 12 | set[idx..] 13 | .iter() 14 | .take_while(move |(cidr, _)| ip >= cidr.first_address()) 15 | .filter(move |(cidr, _)| ip <= cidr.last_address()) 16 | .map(|(_, rule)| *rule) 17 | } 18 | 19 | impl RuleSet { 20 | pub(in super::super) fn match_ipv4_impl( 21 | &self, 22 | ip: Ipv4Addr, 23 | ) -> impl Iterator + '_ { 24 | match_ip_rules(&self.dst_ipv4_ordered_set, ip) 25 | } 26 | pub(in super::super) fn match_ipv6_impl( 27 | &self, 28 | ip: Ipv6Addr, 29 | ) -> impl Iterator + '_ { 30 | match_ip_rules(&self.dst_ipv6_ordered_set, ip) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ytflow/src/plugin/resolve_dest/mod.rs: -------------------------------------------------------------------------------- 1 | mod forward; 2 | 3 | pub use forward::{DatagramForwardResolver, StreamForwardResolver}; 4 | 5 | use std::sync::Arc; 6 | 7 | use crate::flow::*; 8 | 9 | async fn try_resolve_forward( 10 | is_ipv6: bool, 11 | resolver: Arc, 12 | domain: String, 13 | port: u16, 14 | ) -> DestinationAddr { 15 | match if is_ipv6 { 16 | resolver 17 | .resolve_ipv6(domain.clone()) 18 | .await 19 | .ok() 20 | .and_then(|ips| ips.first().cloned()) 21 | .map(Into::into) 22 | } else { 23 | resolver 24 | .resolve_ipv4(domain.clone()) 25 | .await 26 | .ok() 27 | .and_then(|ips| ips.first().cloned()) 28 | .map(Into::into) 29 | } { 30 | Some(ip) => DestinationAddr { 31 | host: HostName::Ip(ip), 32 | port, 33 | }, 34 | 35 | None => DestinationAddr { 36 | host: HostName::DomainName(domain), 37 | port, 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ytflow/src/flow/tun.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub type TunBufferSignature = [*mut usize; 2]; 4 | 5 | pub struct TunBufferToken { 6 | /// Opaque data 7 | signature: TunBufferSignature, 8 | pub data: &'static mut [u8], 9 | } 10 | 11 | unsafe impl Send for TunBufferToken {} 12 | unsafe impl Sync for TunBufferToken {} 13 | 14 | impl TunBufferToken { 15 | /// # Safety 16 | /// 17 | /// User must ensure `signature` can be sent to other threads safely. 18 | pub unsafe fn new(signature: TunBufferSignature, data: &'static mut [u8]) -> Self { 19 | Self { signature, data } 20 | } 21 | pub fn into_parts(self) -> (TunBufferSignature, &'static mut [u8]) { 22 | (self.signature, self.data) 23 | } 24 | } 25 | 26 | pub trait Tun: Send + Sync { 27 | // Read 28 | fn blocking_recv(&self) -> Option; 29 | fn return_recv_buffer(&self, buf: Buffer); 30 | 31 | // Write 32 | fn get_tx_buffer(&self) -> Option; 33 | fn send(&self, buf: TunBufferToken, len: usize); 34 | fn return_tx_buffer(&self, buf: TunBufferToken); 35 | } 36 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi/share_link.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::os::raw::c_char; 3 | use std::panic::AssertUnwindSafe; 4 | 5 | use super::interop::serialize_buffer; 6 | use super::{error::ytflow_result, interop::serialize_string_buffer}; 7 | use crate::share_link::{decode_share_link, encode_share_link}; 8 | 9 | #[no_mangle] 10 | pub unsafe extern "C" fn ytflow_app_share_link_decode(link: *const c_char) -> ytflow_result { 11 | ytflow_result::catch_result_unwind(AssertUnwindSafe(move || { 12 | let link = unsafe { CStr::from_ptr(link).to_string_lossy() }; 13 | decode_share_link(&link).map(|p| serialize_buffer(&p)) 14 | })) 15 | } 16 | 17 | #[no_mangle] 18 | pub unsafe extern "C" fn ytflow_app_share_link_encode( 19 | proxy: *const u8, 20 | proxy_len: usize, 21 | ) -> ytflow_result { 22 | ytflow_result::catch_result_unwind(AssertUnwindSafe(move || { 23 | let proxy = super::proxy::deserialize_proxy_cbor(proxy, proxy_len)?; 24 | encode_share_link(&proxy) 25 | .map(|l| serialize_string_buffer(l)) 26 | .map_err(ytflow_result::from) 27 | })) 28 | } 29 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/system_resolver.rs: -------------------------------------------------------------------------------- 1 | use crate::config::factory::*; 2 | use crate::config::*; 3 | 4 | #[derive(Clone)] 5 | pub struct SystemResolverFactory; 6 | 7 | impl SystemResolverFactory { 8 | pub(in super::super) fn parse(plugin: &Plugin) -> ConfigResult> { 9 | Ok(ParsedPlugin { 10 | factory: Self, 11 | requires: vec![], 12 | provides: vec![Descriptor { 13 | descriptor: plugin.name.to_string() + ".resolver", 14 | r#type: AccessPointType::RESOLVER, 15 | }], 16 | resources: vec![], 17 | }) 18 | } 19 | } 20 | 21 | impl Factory for SystemResolverFactory { 22 | #[cfg(feature = "plugins")] 23 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 24 | use crate::plugin::system_resolver::SystemResolver; 25 | 26 | let resolver = Arc::new(SystemResolver::new()); 27 | set.fully_constructed 28 | .resolver 29 | .insert(plugin_name + ".resolver", resolver); 30 | Ok(()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/core/fs_resource_loader.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use ytflow::resource::{FileResourceLoader, ResourceResult}; 6 | 7 | pub struct FsResourceLoader { 8 | root: PathBuf, 9 | } 10 | 11 | impl FsResourceLoader { 12 | pub fn new(root: PathBuf) -> io::Result { 13 | Ok(Self { 14 | root: root.canonicalize()?, 15 | }) 16 | } 17 | pub fn root(&self) -> &Path { 18 | self.root.as_path() 19 | } 20 | } 21 | 22 | impl FileResourceLoader for FsResourceLoader { 23 | fn load_file(&self, local_name: &str) -> ResourceResult { 24 | let file_path = Path::join(&self.root, PathBuf::from(local_name)).canonicalize()?; 25 | if !file_path.starts_with(self.root.as_path()) { 26 | return Err(std::io::Error::new( 27 | std::io::ErrorKind::Other, 28 | "File path is outside of resource root", 29 | ) 30 | .into()); 31 | } 32 | let file = File::options().read(true).open(file_path)?; 33 | Ok(file) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/factory/datagram.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | use async_trait::async_trait; 4 | 5 | use super::super::datagram::ShadowsocksDatagramSession; 6 | use super::ShadowCrypto; 7 | use crate::flow::*; 8 | 9 | pub struct ShadowsocksDatagramSessionFactory 10 | where 11 | [(); C::KEY_LEN]:, 12 | { 13 | pub(super) key: Arc<[u8; C::KEY_LEN]>, 14 | pub(super) next: Weak, 15 | pub(super) crypto_phantom: std::marker::PhantomData, 16 | } 17 | 18 | #[async_trait] 19 | impl DatagramSessionFactory for ShadowsocksDatagramSessionFactory 20 | where 21 | [(); C::KEY_LEN]:, 22 | [(); C::IV_LEN]:, 23 | [(); C::POST_CHUNK_OVERHEAD]:, 24 | { 25 | async fn bind(&self, context: Box) -> FlowResult> { 26 | let next = self.next.upgrade().ok_or(FlowError::NoOutbound)?; 27 | Ok(Box::new(ShadowsocksDatagramSession:: { 28 | key: self.key.clone(), 29 | lower: next.bind(context).await?, 30 | crypto_phantom: std::marker::PhantomData, 31 | })) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ytflow/src/plugin/switch/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod responder; 2 | 3 | use std::sync::{Arc, Weak}; 4 | 5 | use arc_swap::ArcSwap; 6 | 7 | pub use responder::Choice; 8 | pub use responder::Responder; 9 | 10 | use crate::flow::*; 11 | 12 | pub struct CurrentChoice { 13 | pub idx: u32, 14 | pub tcp_next: Weak, 15 | pub udp_next: Weak, 16 | } 17 | 18 | pub struct Switch { 19 | pub current_choice: ArcSwap, 20 | } 21 | 22 | impl StreamHandler for Switch { 23 | fn on_stream(&self, lower: Box, initial_data: Buffer, context: Box) { 24 | let Some(tcp_next) = self.current_choice.load().tcp_next.upgrade() else { 25 | return; 26 | }; 27 | tcp_next.on_stream(lower, initial_data, context); 28 | } 29 | } 30 | 31 | impl DatagramSessionHandler for Switch { 32 | fn on_session(&self, session: Box, context: Box) { 33 | let Some(udp_next) = self.current_choice.load().udp_next.upgrade() else { 34 | return; 35 | }; 36 | udp_next.on_session(session, context); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ytflow-app-util/src/cbor.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | mod json; 4 | mod raw_bytes; 5 | 6 | #[derive(Debug, Error, Clone, PartialEq, Eq)] 7 | pub enum CborUtilError { 8 | #[error("invalid CBOR or JSON encoding")] 9 | InvalidEncoding, 10 | #[error(r#"unexpected field "{0}" in raw byte representation"#)] 11 | UnexpectedByteReprKey(String), 12 | #[error("the bytes in {0} representation is invalid")] 13 | InvalidByteRepr(&'static str), 14 | #[error("missing data field for raw byte representation")] 15 | MissingData, 16 | #[error(r#"unknown byte representation "{0}""#)] 17 | UnknownByteRepr(String), 18 | } 19 | 20 | pub type CborUtilResult = Result; 21 | 22 | pub use json::{cbor_to_json, json_to_cbor}; 23 | pub use raw_bytes::{escape_cbor_buf, unescape_cbor_buf}; 24 | 25 | pub(crate) fn to_cbor( 26 | value: Result, 27 | ) -> serde_bytes::ByteBuf { 28 | let mut buf = Vec::with_capacity(128); 29 | ciborium::ser::into_writer(&value.expect("cannot encode cbor"), &mut buf) 30 | .expect("Cannot serialize proxy"); 31 | serde_bytes::ByteBuf::from(buf) 32 | } 33 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi/interop.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_void; 2 | use std::ptr::null_mut; 3 | 4 | use serde::Serialize; 5 | 6 | use super::error::ytflow_result; 7 | 8 | #[no_mangle] 9 | pub unsafe extern "C" fn ytflow_buffer_free(ptr: *mut c_void, metadata: usize) -> ytflow_result { 10 | ytflow_result::catch_ptr_unwind(|| { 11 | unsafe { 12 | drop(Box::from_raw(std::ptr::from_raw_parts_mut::<[u8]>( 13 | ptr as _, metadata, 14 | ))); 15 | } 16 | (null_mut(), 0) 17 | }) 18 | } 19 | 20 | pub(super) fn serialize_buffer(data: &T) -> (*mut c_void, usize) { 21 | let buf = cbor4ii::serde::to_vec(vec![], data).expect("Could not serialize buffer into CBOR"); 22 | serialize_byte_buffer(buf.into_boxed_slice()) 23 | } 24 | 25 | pub(super) fn serialize_byte_buffer(data: impl Into>) -> (*mut c_void, usize) { 26 | let ptr = Box::into_raw(data.into()); 27 | let (ptr, metadata) = ptr.to_raw_parts(); 28 | (ptr as _, metadata) 29 | } 30 | 31 | pub(super) fn serialize_string_buffer(data: impl Into) -> (*mut c_void, usize) { 32 | serialize_byte_buffer(data.into().into_bytes()) 33 | } 34 | -------------------------------------------------------------------------------- /ytflow/src/plugin/netif/sys/apple/bind.rs: -------------------------------------------------------------------------------- 1 | use std::os::fd::AsRawFd; 2 | 3 | use super::Netif; 4 | use super::*; 5 | 6 | pub fn bind_socket_v4(netif: &Netif, socket: &mut socket2::Socket) -> FlowResult<()> { 7 | let idx = netif.get_idx()?; 8 | let ret = unsafe { 9 | libc::setsockopt( 10 | socket.as_raw_fd(), 11 | libc::IPPROTO_IP, 12 | libc::IP_BOUND_IF, 13 | &idx as *const _ as _, 14 | std::mem::size_of::() as libc::socklen_t, 15 | ) 16 | }; 17 | if ret == -1 { 18 | Err(std::io::Error::last_os_error())?; 19 | } 20 | Ok(()) 21 | } 22 | 23 | pub fn bind_socket_v6(netif: &Netif, socket: &mut socket2::Socket) -> FlowResult<()> { 24 | let idx = netif.get_idx()?; 25 | let ret = unsafe { 26 | libc::setsockopt( 27 | socket.as_raw_fd(), 28 | libc::IPPROTO_IPV6, 29 | libc::IPV6_BOUND_IF, 30 | &idx as *const _ as _, 31 | std::mem::size_of::() as libc::socklen_t, 32 | ) 33 | }; 34 | if ret == -1 { 35 | Err(std::io::Error::last_os_error())?; 36 | } 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /ytflow/src/data/migrations/V4__resource.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `yt_resources` ( 2 | `id` INTEGER PRIMARY KEY, 3 | `key` VARCHAR(255) NOT NULL UNIQUE, 4 | `type` VARCHAR(255) NOT NULL, 5 | `local_file` TEXT NOT NULL, 6 | `remote_type` VARCHAR(255) NOT NULL, 7 | `created_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')), 8 | `updated_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')) 9 | ); 10 | 11 | CREATE TABLE `yt_resources_url` ( 12 | `id` INTEGER PRIMARY KEY, 13 | `resource_id` INTEGER NOT NULL UNIQUE REFERENCES `yt_resources`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 14 | `url` TEXT NOT NULL, 15 | `etag` VARCHAR(255), 16 | `last_modified` VARCHAR(255), 17 | `retrieved_at` TEXT 18 | ); 19 | 20 | CREATE TABLE `yt_resources_github_release` ( 21 | `id` INTEGER PRIMARY KEY, 22 | `resource_id` INTEGER NOT NULL UNIQUE REFERENCES `yt_resources`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 23 | `github_username` VARCHAR(255) NOT NULL, 24 | `github_repo` VARCHAR(255) NOT NULL, 25 | `asset_name` VARCHAR(255) NOT NULL, 26 | `git_tag` VARCHAR(255), 27 | `release_title` TEXT, 28 | `retrieved_at` TEXT 29 | ); 30 | -------------------------------------------------------------------------------- /ytflow/src/plugin/vmess/protocol/body.rs: -------------------------------------------------------------------------------- 1 | mod aead; 2 | mod aes_cfb; 3 | mod factory; 4 | mod none; 5 | mod shake; 6 | 7 | use crate::flow::FlowResult; 8 | pub use aead::{AesGcmCryptoFactory, ChachaPolyCryptoFactory}; 9 | pub use aes_cfb::AesCfbCryptoFactory; 10 | pub use factory::BodyCryptoFactory; 11 | pub use none::NoneCryptoFactory; 12 | pub use shake::ShakeSizeCrypto; 13 | 14 | pub trait SizeCrypto { 15 | const LEN: usize; 16 | fn encode_size(&mut self, size: usize) -> [u8; Self::LEN]; 17 | fn decode_size(&mut self, size_bytes: &mut [u8; Self::LEN]) -> FlowResult; 18 | } 19 | 20 | pub trait RxCrypto { 21 | fn peek_header_ciphertext(&mut self, _header_ciphertext: &mut [u8]) {} 22 | fn expected_next_size_len(&mut self) -> usize; 23 | fn on_size(&mut self, size_bytes: &mut [u8]) -> FlowResult; 24 | fn expected_next_chunk_len(&mut self) -> usize; 25 | fn on_chunk<'c>(&mut self, chunk: &'c mut [u8]) -> FlowResult<&'c mut [u8]>; 26 | } 27 | 28 | pub trait TxCrypto { 29 | fn calculate_overhead(&mut self, next_payload_len: usize) -> (usize, usize); 30 | fn seal(&mut self, pre_overhead: &mut [u8], payload: &mut [u8], post_overhead: &mut [u8]); 31 | } 32 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi/runtime.rs: -------------------------------------------------------------------------------- 1 | use std::{panic::AssertUnwindSafe, ptr::null_mut}; 2 | 3 | use ytflow::tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime as TokioRuntime}; 4 | 5 | use super::error::ytflow_result; 6 | 7 | #[allow(unused, non_camel_case_types)] 8 | pub struct ytflow_runtime { 9 | pub(crate) rt: TokioRuntime, 10 | } 11 | 12 | #[allow(unused, non_camel_case_types)] 13 | pub(crate) type FfiRuntime = ytflow_runtime; 14 | 15 | #[no_mangle] 16 | pub extern "C" fn ytflow_runtime_new() -> ytflow_result { 17 | ytflow_result::catch_ptr_unwind(|| { 18 | let rt = TokioRuntimeBuilder::new_multi_thread() 19 | .enable_all() 20 | .thread_name("ytflow-tokio-runtime-worker") 21 | .worker_threads(2) 22 | .build() 23 | .expect("Cannot build Tokio Runtime"); 24 | (Box::into_raw(Box::new(FfiRuntime { rt })) as _, 0) 25 | }) 26 | } 27 | 28 | #[no_mangle] 29 | pub unsafe extern "C" fn ytflow_runtime_free(runtime: *mut ytflow_runtime) -> ytflow_result { 30 | ytflow_result::catch_ptr_unwind(AssertUnwindSafe(|| { 31 | unsafe { drop(Box::from_raw(runtime)) }; 32 | (null_mut(), 0) 33 | })) 34 | } 35 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/crypto/plain.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub struct Plain {} 4 | 5 | impl ShadowCrypto for Plain { 6 | const KEY_LEN: usize = 0; 7 | const IV_LEN: usize = 0; 8 | const PRE_CHUNK_OVERHEAD: usize = 0; 9 | const POST_CHUNK_OVERHEAD: usize = 0; 10 | 11 | fn create_crypto(_key: &[u8; Self::KEY_LEN], _iv: &[u8; Self::IV_LEN]) -> Self { 12 | Plain {} 13 | } 14 | 15 | fn encrypt( 16 | &mut self, 17 | _pre_overhead: &mut [u8; Self::PRE_CHUNK_OVERHEAD], 18 | _data: &mut [u8], 19 | _post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD], 20 | ) { 21 | } 22 | fn encrypt_all( 23 | &mut self, 24 | _data: &mut [u8], 25 | _post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD], 26 | ) { 27 | } 28 | 29 | fn decrypt_size( 30 | &mut self, 31 | _pre_overhead: &mut [u8; Self::PRE_CHUNK_OVERHEAD], 32 | ) -> Option { 33 | None 34 | } 35 | 36 | #[must_use] 37 | fn decrypt( 38 | &mut self, 39 | _data: &mut [u8], 40 | _post_overhead: &[u8; Self::POST_CHUNK_OVERHEAD], 41 | ) -> bool { 42 | true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ytflow/src/plugin/vmess.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[cfg(feature = "plugins")] 6 | mod client; 7 | #[cfg(feature = "plugins")] 8 | mod protocol; 9 | #[cfg(feature = "plugins")] 10 | mod stream; 11 | 12 | #[cfg(feature = "plugins")] 13 | pub use client::VMessStreamOutboundFactory; 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 16 | pub enum SupportedSecurity { 17 | #[serde(rename = "none")] 18 | None, 19 | #[serde(rename = "auto")] 20 | Auto, 21 | #[serde(rename = "aes-128-cfb")] 22 | Aes128Cfb, 23 | #[serde(rename = "aes-128-gcm")] 24 | Aes128Gcm, 25 | #[serde(rename = "chacha20-poly1305")] 26 | Chacha20Poly1305, 27 | } 28 | 29 | impl Display for SupportedSecurity { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | f.write_str(match self { 32 | SupportedSecurity::None => "none", 33 | SupportedSecurity::Auto => "auto", 34 | SupportedSecurity::Aes128Cfb => "aes-128-cfb", 35 | SupportedSecurity::Aes128Gcm => "aes-128-gcm", 36 | SupportedSecurity::Chacha20Poly1305 => "chacha20-poly1305", 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ytflow/src/plugin.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "plugins")] 2 | pub mod dns_server; 3 | pub mod dyn_outbound; 4 | #[cfg(feature = "plugins")] 5 | pub mod fakeip; 6 | #[cfg(feature = "plugins")] 7 | pub mod fallback; 8 | #[cfg(feature = "plugins")] 9 | pub mod forward; 10 | #[cfg(feature = "plugins")] 11 | pub mod host_resolver; 12 | #[cfg(feature = "plugins")] 13 | pub mod http_proxy; 14 | #[cfg(feature = "plugins")] 15 | pub mod ip_stack; 16 | pub mod netif; 17 | #[cfg(feature = "plugins")] 18 | pub mod null; 19 | #[cfg(feature = "plugins")] 20 | pub mod obfs; 21 | #[cfg(feature = "plugins")] 22 | pub mod redirect; 23 | #[cfg(feature = "plugins")] 24 | pub mod reject; 25 | #[cfg(feature = "plugins")] 26 | pub mod resolve_dest; 27 | pub mod rule_dispatcher; 28 | pub mod shadowsocks; 29 | pub mod simple_dispatcher; 30 | #[cfg(feature = "plugins")] 31 | pub mod socket; 32 | #[cfg(feature = "plugins")] 33 | pub mod socks5; 34 | #[cfg(feature = "plugins")] 35 | pub mod switch; 36 | #[cfg(feature = "plugins")] 37 | pub mod system_resolver; 38 | #[cfg(feature = "plugins")] 39 | pub mod tls; 40 | #[cfg(feature = "plugins")] 41 | pub mod trojan; 42 | pub mod vmess; 43 | #[cfg(feature = "plugins")] 44 | pub mod ws; 45 | 46 | #[cfg(feature = "plugins")] 47 | mod h2; 48 | -------------------------------------------------------------------------------- /ytflow/src/plugin/vmess/protocol/body/shake.rs: -------------------------------------------------------------------------------- 1 | use sha3::digest::{core_api::XofReaderCoreWrapper, ExtendableOutput, Update, XofReader}; 2 | use sha3::{Shake128, Shake128ReaderCore}; 3 | 4 | use super::SizeCrypto; 5 | use crate::flow::FlowResult; 6 | 7 | type Shake128Reader = XofReaderCoreWrapper; 8 | 9 | pub struct ShakeSizeCrypto { 10 | reader: Shake128Reader, 11 | } 12 | 13 | impl ShakeSizeCrypto { 14 | pub fn new(iv: &[u8]) -> Self { 15 | let mut hasher = Shake128::default(); 16 | hasher.update(iv); 17 | let reader = hasher.finalize_xof(); 18 | Self { reader } 19 | } 20 | } 21 | 22 | impl SizeCrypto for ShakeSizeCrypto { 23 | const LEN: usize = 2; 24 | 25 | fn encode_size(&mut self, size: usize) -> [u8; Self::LEN] { 26 | // TODO: exceed u16? 27 | let mut buf = [0, 0]; 28 | self.reader.read(&mut buf); 29 | (u16::from_be_bytes(buf) ^ (size as u16)).to_be_bytes() 30 | } 31 | 32 | fn decode_size(&mut self, size_bytes: &mut [u8; Self::LEN]) -> FlowResult { 33 | let mut buf = [0, 0]; 34 | self.reader.read(&mut buf); 35 | Ok((u16::from_be_bytes(buf) ^ u16::from_be_bytes(*size_bytes)) as usize) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ytflow-bin-shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ytflow-bin-shared" 3 | version = "0.7.3" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | ytflow = { path = "../ytflow", features = ["plugins"] } 13 | ytflow-app-util = { path = "../ytflow-app-util", features = ["ffi"] } 14 | anyhow = "1" 15 | fern = { version = "0.6", features = ["colored"] } 16 | log = "0.4" 17 | chrono = "0.4" 18 | nanoid = "0.4" 19 | strum = "0.25" 20 | strum_macros = "0.25" 21 | cidr = { version = "0.2", features = ["serde"] } 22 | futures = { version = "0.3", default-features = false } 23 | 24 | # CLI 25 | clap = { version = "4", features = ["cargo"] } 26 | ctrlc = "3" 27 | edit = "0.1" 28 | 29 | # Data 30 | cbor4ii = { version = "0.3", features = ["use_std", "serde1"] } 31 | ciborium = "0.2" 32 | serde = { version = "1", features = ["derive"] } 33 | serde_bytes = "0.11" 34 | serde_json = { version = "1", features = ["alloc"] } 35 | base64 = "0.22" 36 | 37 | # TUI 38 | tui = { version = "0.19", default-features = false, features = ['crossterm'] } 39 | # tui 0.16.0 still uses crossterm 0.20, which crashes on Windows Terminal 40 | crossterm = "0.27" 41 | tui-input = "0.8" 42 | -------------------------------------------------------------------------------- /ytflow/src/plugin/dns_server/mod.rs: -------------------------------------------------------------------------------- 1 | mod datagram; 2 | mod map_back; 3 | 4 | use std::sync::Arc; 5 | 6 | pub use datagram::DnsServer; 7 | pub use map_back::{MapBackDatagramSessionHandler, MapBackStreamHandler}; 8 | 9 | pub async fn cache_writer(plugin: Arc) { 10 | let (plugin, notify) = { 11 | let notify = plugin.new_notify.clone(); 12 | let weak = Arc::downgrade(&plugin); 13 | drop(plugin); 14 | (weak, notify) 15 | }; 16 | if plugin.strong_count() == 0 { 17 | panic!("dns-server has no strong reference left for cache_writer"); 18 | } 19 | 20 | use tokio::select; 21 | use tokio::time::{sleep, Duration}; 22 | loop { 23 | let mut notified_fut = notify.notified(); 24 | let mut sleep_fut = sleep(Duration::from_secs(3600)); 25 | 'debounce: loop { 26 | select! { 27 | _ = notified_fut => { 28 | notified_fut = notify.notified(); 29 | sleep_fut = sleep(Duration::from_secs(3)); 30 | } 31 | _ = sleep_fut => { 32 | break 'debounce; 33 | } 34 | } 35 | } 36 | match plugin.upgrade() { 37 | Some(plugin) => plugin.save_cache(), 38 | None => break, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/crypto/ctor.rs: -------------------------------------------------------------------------------- 1 | use cipher::crypto_common::OutputSizeUser; 2 | 3 | use super::*; 4 | 5 | pub trait StreamCryptoCtor { 6 | type Output; 7 | fn create_crypto(key: &[u8], iv: &[u8]) -> Self::Output; 8 | } 9 | 10 | pub struct KeyOnlyCtor(PhantomData); 11 | 12 | impl StreamCryptoCtor for KeyOnlyCtor 13 | where 14 | T: KeyInit, 15 | { 16 | type Output = T; 17 | fn create_crypto(key: &[u8], _iv: &[u8]) -> Self::Output { 18 | T::new_from_slice(key).unwrap() 19 | } 20 | } 21 | 22 | pub struct KeyIvCtor(PhantomData); 23 | 24 | impl StreamCryptoCtor for KeyIvCtor 25 | where 26 | T: KeyIvInit, 27 | { 28 | type Output = T; 29 | fn create_crypto(key: &[u8], iv: &[u8]) -> Self::Output { 30 | T::new_from_slices(key, iv).unwrap() 31 | } 32 | } 33 | 34 | pub struct Rc4Md5Ctor(PhantomData); 35 | 36 | impl StreamCryptoCtor for Rc4Md5Ctor 37 | where 38 | T: KeyInit + KeySizeUser::OutputSize>, 39 | { 40 | type Output = T; 41 | fn create_crypto(key: &[u8], iv: &[u8]) -> Self::Output { 42 | use md5::digest::Digest; 43 | let mut hash = md5::Md5::new(); 44 | hash.update(key); 45 | hash.update(iv); 46 | let key = hash.finalize(); 47 | T::new(&key) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ytflow/src/plugin/vmess/protocol/header/crypto.rs: -------------------------------------------------------------------------------- 1 | use md5::{Digest, Md5}; 2 | 3 | use super::super::USER_ID_LEN; 4 | use super::{RequestHeader, ResponseHeader}; 5 | 6 | pub(crate) const HEADER_KEY_LEN: usize = 16; 7 | pub(crate) const HEADER_IV_LEN: usize = 16; 8 | pub(crate) const CMD_KEY_LEN: usize = 16; 9 | 10 | pub enum HeaderDecryptResult { 11 | Invalid, 12 | Incomplete { total_required: usize }, 13 | Complete { res: T, len: usize }, 14 | } 15 | 16 | pub trait RequestHeaderEnc { 17 | type Dec: ResponseHeaderDec; 18 | 19 | const REQUIRED_SIZE: usize; 20 | 21 | fn derive_res_iv(&self, header: &RequestHeader) -> [u8; HEADER_IV_LEN]; 22 | fn derive_res_key(&self, header: &RequestHeader) -> [u8; HEADER_KEY_LEN]; 23 | fn encrypt_req(self, header: &mut RequestHeader, buf: &mut [u8]) -> Option<(usize, Self::Dec)>; 24 | } 25 | 26 | pub trait ResponseHeaderDec { 27 | #[must_use] 28 | fn decrypt_res(&mut self, data: &mut [u8]) -> HeaderDecryptResult; 29 | } 30 | 31 | pub fn derive_cmd_key(user_id: &[u8; USER_ID_LEN]) -> [u8; CMD_KEY_LEN] { 32 | let mut cmd_key = *b"????????????????c48619fe-8f02-49e0-b9e9-edf763e17e21"; 33 | cmd_key[..USER_ID_LEN].copy_from_slice(user_id); 34 | let mut cmd_key_out = [0; CMD_KEY_LEN]; 35 | cmd_key_out[..].copy_from_slice(&Md5::digest(cmd_key)[..]); 36 | cmd_key_out 37 | } 38 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher/builder/surge_domainset.rs: -------------------------------------------------------------------------------- 1 | use aho_corasick::AhoCorasick; 2 | 3 | use crate::plugin::rule_dispatcher::set::RuleMappedAhoCorasick; 4 | 5 | use super::*; 6 | 7 | impl RuleSet { 8 | pub fn build_surge_domainset<'s>( 9 | lines: impl Iterator + Clone, 10 | action: ActionHandle, 11 | ) -> Option { 12 | let lines = lines 13 | .map(|l| l.trim()) 14 | .filter(|l| !l.is_empty() && !l.starts_with('#')); 15 | let full_ac = AhoCorasick::builder() 16 | .build(lines.clone().filter(|l| !l.starts_with('.'))) 17 | .ok()?; 18 | let sub_ac = AhoCorasick::builder() 19 | .build(lines.clone().filter_map(|l| l.strip_prefix('.'))) 20 | .ok()?; 21 | 22 | // TODO: observe order 23 | let rule_id = 1; 24 | Some(Self { 25 | dst_domain_full: Some(RuleMappedAhoCorasick { 26 | handle_map: vec![(0..full_ac.patterns_len(), RuleHandle::new(action, rule_id))], 27 | ac: full_ac, 28 | }), 29 | dst_domain_sub: Some(RuleMappedAhoCorasick { 30 | handle_map: vec![(0..sub_ac.patterns_len(), RuleHandle::new(action, rule_id))], 31 | ac: sub_ac, 32 | }), 33 | ..Default::default() 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/protocol.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | mod http; 4 | mod shadowsocks; 5 | mod socks5; 6 | mod trojan; 7 | mod vmess; 8 | 9 | pub use http::HttpProxy; 10 | pub use shadowsocks::ShadowsocksProxy; 11 | pub use socks5::Socks5Proxy; 12 | pub use trojan::TrojanProxy; 13 | pub use vmess::VMessProxy; 14 | 15 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 16 | pub enum ProxyProtocolType { 17 | Shadowsocks(shadowsocks::ShadowsocksProxy), 18 | Trojan(trojan::TrojanProxy), 19 | Http(http::HttpProxy), 20 | Socks5(socks5::Socks5Proxy), 21 | VMess(vmess::VMessProxy), 22 | } 23 | 24 | impl ProxyProtocolType { 25 | pub fn require_udp_next(&self) -> bool { 26 | match self { 27 | ProxyProtocolType::Shadowsocks(_) => true, 28 | ProxyProtocolType::Trojan(_) => false, 29 | ProxyProtocolType::Http(_) => false, 30 | ProxyProtocolType::Socks5(_) => true, 31 | ProxyProtocolType::VMess(_) => false, 32 | } 33 | } 34 | pub fn provide_udp(&self) -> bool { 35 | match self { 36 | ProxyProtocolType::Shadowsocks(_) => true, 37 | ProxyProtocolType::Trojan(_) => true, 38 | ProxyProtocolType::Http(_) => false, 39 | ProxyProtocolType::Socks5(_) => true, 40 | ProxyProtocolType::VMess(_) => true, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher/builder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | mod geoip; 4 | mod quanx_filter; 5 | mod surge_domainset; 6 | 7 | use crate::flow::Resolver; 8 | 9 | use super::dispatcher::ActionSet; 10 | use super::rules::GeoIpSet; 11 | use super::set::RuleSet; 12 | use super::{Action, ActionHandle, RuleDispatcher, RuleHandle, RuleId, ACTION_LIMIT}; 13 | 14 | #[derive(Default)] 15 | pub struct RuleDispatcherBuilder { 16 | resolver: Option>, 17 | actions: ActionSet, 18 | } 19 | 20 | impl RuleDispatcherBuilder { 21 | pub fn add_action(&mut self, action: Action) -> Option { 22 | if self.actions.len() >= ACTION_LIMIT { 23 | None 24 | } else { 25 | let handle = ActionHandle(self.actions.len() as u8); 26 | self.actions.push(action); 27 | Some(handle) 28 | } 29 | } 30 | 31 | pub fn set_resolver(&mut self, resolver: Option>) { 32 | self.resolver = resolver; 33 | } 34 | 35 | pub fn build( 36 | self, 37 | rule_set: RuleSet, 38 | fallback: Action, 39 | me: Weak, 40 | ) -> RuleDispatcher { 41 | let Self { resolver, actions } = self; 42 | RuleDispatcher { 43 | resolver, 44 | rule_set, 45 | actions, 46 | fallback, 47 | me, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ytflow-app-util/src/share_link/vmess/v2rayn/stupid_value.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::Display; 3 | use std::str::FromStr; 4 | 5 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 | 7 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 8 | pub(super) struct StupidValue(pub T); 9 | 10 | impl Serialize for StupidValue 11 | where 12 | T: ToString, 13 | { 14 | fn serialize(&self, serializer: S) -> Result 15 | where 16 | S: Serializer, 17 | { 18 | self.0.to_string().serialize(serializer) 19 | } 20 | } 21 | 22 | impl<'de, T> Deserialize<'de> for StupidValue 23 | where 24 | T: FromStr + Deserialize<'de>, 25 | T::Err: Display, 26 | { 27 | fn deserialize(deserializer: D) -> Result, D::Error> 28 | where 29 | D: Deserializer<'de>, 30 | { 31 | #[derive(Deserialize)] 32 | #[serde(untagged)] 33 | enum StrOrValue<'a, T> { 34 | Str(Cow<'a, str>), 35 | Value(T), 36 | } 37 | 38 | let str_or_val = StrOrValue::::deserialize(deserializer)?; 39 | Ok(StupidValue(match str_or_val { 40 | StrOrValue::Value(val) => val, 41 | StrOrValue::Str(s) => s.parse().map_err(serde::de::Error::custom)?, 42 | })) 43 | } 44 | } 45 | 46 | impl From for StupidValue { 47 | fn from(val: T) -> Self { 48 | StupidValue(val) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/reject.rs: -------------------------------------------------------------------------------- 1 | use crate::config::factory::*; 2 | use crate::config::*; 3 | 4 | pub struct RejectFactory {} 5 | 6 | impl RejectFactory { 7 | pub(in super::super) fn parse(plugin: &Plugin) -> ConfigResult> { 8 | let name = plugin.name.clone(); 9 | Ok(ParsedPlugin { 10 | factory: RejectFactory {}, 11 | requires: vec![], 12 | provides: vec![ 13 | Descriptor { 14 | descriptor: name.clone() + ".tcp", 15 | r#type: AccessPointType::STREAM_HANDLER, 16 | }, 17 | Descriptor { 18 | descriptor: name + ".udp", 19 | r#type: AccessPointType::DATAGRAM_SESSION_HANDLER, 20 | }, 21 | ], 22 | resources: vec![], 23 | }) 24 | } 25 | } 26 | 27 | impl Factory for RejectFactory { 28 | #[cfg(feature = "plugins")] 29 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 30 | use crate::plugin::reject; 31 | 32 | set.fully_constructed.stream_handlers.insert( 33 | plugin_name.clone() + ".tcp", 34 | Arc::new(reject::RejectHandler), 35 | ); 36 | set.fully_constructed 37 | .datagram_handlers 38 | .insert(plugin_name + ".udp", Arc::new(reject::RejectHandler)); 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ytflow-app-util/src/subscription/b64_links.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use base64::engine::general_purpose::STANDARD as base64; 4 | use base64::prelude::*; 5 | 6 | use super::decode::{DecodeError, DecodeResult}; 7 | use crate::share_link::decode_share_link; 8 | use crate::subscription::{Subscription, SubscriptionFormat}; 9 | 10 | impl SubscriptionFormat<'static> { 11 | pub const B64_LINKS: Self = SubscriptionFormat(b"b64_links\0"); 12 | } 13 | 14 | pub fn decode_b64_links(data: &[u8]) -> DecodeResult { 15 | let data = str::from_utf8(data).map_err(|_| DecodeError::InvalidEncoding)?; 16 | let proxies = data 17 | .lines() 18 | .filter_map(|l| base64.decode(l).ok()) 19 | .map(|l| String::from_utf8(l).unwrap_or_default()) 20 | .flat_map(|l| { 21 | l.lines() 22 | .filter_map(|l| decode_share_link(l).ok()) 23 | .collect::>() 24 | }) 25 | .collect(); 26 | Ok(Subscription { proxies }) 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | 33 | #[test] 34 | fn test_decode_b64_links_invalid_utf8() { 35 | let res = decode_b64_links(b"\xff"); 36 | assert_eq!(res, Err(DecodeError::InvalidEncoding)); 37 | } 38 | #[test] 39 | fn test_decode_b64_links_invalid_utf8_b64() { 40 | let res = decode_b64_links(b"/w=="); 41 | assert_eq!(res, Ok(Subscription { proxies: vec![] })); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/edit/views.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use tui::style::Color; 3 | 4 | mod input; 5 | mod main; 6 | mod new_profile; 7 | mod new_proxy_group; 8 | mod plugin_type; 9 | mod profile; 10 | mod proxy_group; 11 | mod proxy_type; 12 | mod utils; 13 | 14 | pub use input::run_input_view; 15 | pub use main::run_main_view; 16 | pub use new_profile::run_new_profile_view; 17 | pub use new_proxy_group::run_new_proxy_group_view; 18 | pub use plugin_type::run_plugin_type_view; 19 | pub use profile::run_profile_view; 20 | pub use proxy_group::run_proxy_group_view; 21 | pub use proxy_type::run_proxy_type_view; 22 | use ytflow::data::{Plugin, ProfileId, ProxyGroupId}; 23 | 24 | const BG: Color = Color::Black; 25 | const FG: Color = Color::White; 26 | const DIM_FG: Color = Color::Indexed(245); 27 | 28 | const fn bg_rev(focus: bool) -> Color { 29 | if focus { 30 | FG 31 | } else { 32 | DIM_FG 33 | } 34 | } 35 | 36 | pub struct InputRequest { 37 | item: String, 38 | desc: String, 39 | initial_value: String, 40 | max_len: usize, 41 | action: Box Result<()>>, 42 | } 43 | 44 | pub enum NavChoice { 45 | MainView, 46 | NewProfileView, 47 | ProfileView(ProfileId), 48 | PluginTypeView(ProfileId, Option), 49 | NewProxyGroupView, 50 | ProxyGroupView(ProxyGroupId), 51 | ProxyTypeView(ProxyGroupId), 52 | InputView(InputRequest), 53 | Back, 54 | } 55 | -------------------------------------------------------------------------------- /ytflow/src/data/migrations/V1__initial.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `yt_profiles` ( 2 | `id` INTEGER PRIMARY KEY, 3 | `permanent_id` BLOB(16) NOT NULL UNIQUE DEFAULT (randomblob(16)), 4 | `name` VARCHAR(255) NOT NULL UNIQUE, 5 | `locale` VARCHAR(64) NOT NULL DEFAULT 'en-US', 6 | `last_used_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')), 7 | `created_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')) 8 | ); 9 | 10 | CREATE TABLE `yt_plugins` ( 11 | `id` INTEGER PRIMARY KEY, 12 | `profile_id` INTEGER NOT NULL REFERENCES `yt_profiles`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 13 | `name` VARCHAR(255) NOT NULL, 14 | `desc` TEXT NOT NULL DEFAULT '', 15 | `plugin` VARCHAR(255) NOT NULL, 16 | `plugin_version` INT(4) NOT NULL DEFAULT '0', 17 | `param` TEXT NOT NULL, 18 | `updated_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')), 19 | UNIQUE (`profile_id`, `name`) 20 | ); 21 | 22 | CREATE TABLE `yt_profile_entry_plugin` ( 23 | `profile_id` INTEGER NOT NULL REFERENCES `yt_profiles`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 24 | `plugin_id` INTEGER NOT NULL REFERENCES `yt_plugins`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, 25 | PRIMARY KEY (`profile_id`,`plugin_id`) 26 | ); 27 | 28 | CREATE TRIGGER [yt_plugins_updated] 29 | AFTER UPDATE ON `yt_plugins` 30 | FOR EACH ROW 31 | BEGIN 32 | UPDATE `yt_plugins` SET `updated_at` = (strftime('%Y-%m-%d %H:%M:%f', 'now')) WHERE `id` = old.`id`; 33 | END 34 | -------------------------------------------------------------------------------- /ytflow/src/plugin/simple_dispatcher/rule.rs: -------------------------------------------------------------------------------- 1 | use super::Condition; 2 | use crate::flow::{FlowContext, HostName}; 3 | 4 | pub struct Rule { 5 | pub src_cond: Condition, 6 | pub dst_cond: Condition, 7 | pub next: N, 8 | } 9 | 10 | impl Rule { 11 | pub(super) fn matches(&self, context: &FlowContext) -> Option { 12 | // Match src 13 | { 14 | let Condition { 15 | ip_ranges, 16 | port_ranges, 17 | } = &self.src_cond; 18 | let ip = context.local_peer.ip(); 19 | let port = context.local_peer.port(); 20 | if !ip_ranges.iter().any(|r| r.inner.contains(&ip)) { 21 | return None; 22 | } 23 | if !port_ranges.iter().any(|r| r.inner.contains(&port)) { 24 | return None; 25 | } 26 | } 27 | // Match dst 28 | { 29 | let Condition { 30 | ip_ranges, 31 | port_ranges, 32 | } = &self.dst_cond; 33 | let port = context.remote_peer.port; 34 | if !port_ranges.iter().any(|r| r.inner.contains(&port)) { 35 | return None; 36 | } 37 | match &context.remote_peer.host { 38 | HostName::Ip(ip) if !ip_ranges.iter().any(|r| r.inner.contains(ip)) => None, 39 | HostName::DomainName(_) => None, 40 | _ => Some(self.next.clone()), 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ytflow-app-util/src/proxy/data/analyze.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::proxy::Proxy; 4 | 5 | #[derive(Debug, Clone, Error, PartialEq, Eq)] 6 | pub enum AnalyzeError { 7 | #[error("unknown proxy version")] 8 | UnknownVersion, 9 | #[error("invalid JSON, proxy or plugin format")] 10 | InvalidEncoding, 11 | #[error(r#"duplicated plugin name "{0}""#)] 12 | DuplicateName(String), 13 | #[error(r#"plugin "{0}" required by "{1}" not found"#)] 14 | PluginNotFound(String, String), 15 | #[error(r#"unknown access point: "{0}""#)] 16 | UnknownAccessPoint(String), 17 | #[error(r#"expect plugin "{0}" to have a UDP access point = {1}, but it does not"#)] 18 | UnexpectedUdpAccessPoint(String, bool), 19 | #[error("too complicated")] 20 | TooComplicated, 21 | #[error(r#"invalid plugin: "{0}""#)] 22 | InvalidPlugin(String), 23 | #[error(r#"unused plugin: "{0}""#)] 24 | UnusedPlugin(String), 25 | } 26 | 27 | pub type AnalyzeResult = Result; 28 | 29 | pub fn analyze_data_proxy(name: String, proxy: &[u8], version: u16) -> AnalyzeResult { 30 | if version != 0 { 31 | return Err(AnalyzeError::UnknownVersion); 32 | } 33 | super::v1::analyzer::analyze(name, proxy) 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | 40 | #[test] 41 | fn test_analyze_data_proxy_invalid_version() { 42 | let result = analyze_data_proxy("test".into(), &[], 1); 43 | assert_eq!(result, Err(AnalyzeError::UnknownVersion)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin.rs: -------------------------------------------------------------------------------- 1 | mod dns_server; 2 | mod dyn_outbound; 3 | mod fakeip; 4 | mod forward; 5 | mod host_resolver; 6 | mod http_obfs; 7 | mod http_proxy; 8 | mod ip_stack; 9 | mod list_dispatcher; 10 | mod netif; 11 | mod null; 12 | mod redirect; 13 | mod reject; 14 | mod resolve_dest; 15 | mod rule_dispatcher; 16 | mod shadowsocks; 17 | mod simple_dispatcher; 18 | mod socket; 19 | mod socket_listener; 20 | mod socks5; 21 | mod switch; 22 | mod system_resolver; 23 | mod tls; 24 | mod tls_obfs; 25 | mod trojan; 26 | mod vmess; 27 | mod vpntun; 28 | mod ws; 29 | 30 | pub use dns_server::*; 31 | pub use dyn_outbound::*; 32 | pub use fakeip::*; 33 | pub use forward::*; 34 | pub use host_resolver::*; 35 | pub use http_obfs::*; 36 | pub use http_proxy::*; 37 | pub use ip_stack::*; 38 | pub use list_dispatcher::ListDispatcherFactory; 39 | pub use netif::*; 40 | pub use null::*; 41 | pub use redirect::*; 42 | pub use reject::*; 43 | pub use resolve_dest::*; 44 | pub use rule_dispatcher::RuleDispatcherFactory; 45 | pub use shadowsocks::*; 46 | pub use simple_dispatcher::*; 47 | pub use socket::*; 48 | pub use socket_listener::*; 49 | pub use socks5::*; 50 | pub use switch::*; 51 | pub use system_resolver::*; 52 | pub use tls::*; 53 | pub use tls_obfs::*; 54 | pub use trojan::*; 55 | pub use vmess::*; 56 | pub use vpntun::*; 57 | pub use ws::*; 58 | 59 | use crate::data::PluginId; 60 | 61 | #[derive(Debug, Clone)] 62 | pub struct Plugin { 63 | pub id: Option, 64 | pub name: String, 65 | pub plugin: String, 66 | pub plugin_version: u16, 67 | pub param: Vec, 68 | } 69 | -------------------------------------------------------------------------------- /ytflow/src/plugin/system_resolver.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, ErrorKind}; 2 | use std::net::IpAddr; 3 | 4 | use async_trait::async_trait; 5 | use smallvec::SmallVec; 6 | use tokio::net::lookup_host; 7 | 8 | use crate::flow::*; 9 | 10 | #[derive(Default)] 11 | pub struct SystemResolver {} 12 | 13 | impl SystemResolver { 14 | pub fn new() -> Self { 15 | Default::default() 16 | } 17 | } 18 | 19 | #[async_trait] 20 | impl Resolver for SystemResolver { 21 | async fn resolve_ipv4(&self, domain: String) -> ResolveResultV4 { 22 | let ips = lookup_host(domain.clone() + ":0").await?; 23 | let records = ips 24 | .filter_map(|saddr| match saddr.ip() { 25 | IpAddr::V4(ip) => Some(ip), 26 | _ => None, 27 | }) 28 | .collect::>(); 29 | if records.is_empty() { 30 | Err(io::Error::new(ErrorKind::NotFound, "IPv4 record not found").into()) 31 | } else { 32 | Ok(records) 33 | } 34 | } 35 | async fn resolve_ipv6(&self, domain: String) -> ResolveResultV6 { 36 | let ips = lookup_host(domain.clone() + ":0").await?; 37 | let records = ips 38 | .filter_map(|saddr| match saddr.ip() { 39 | IpAddr::V6(ip) => Some(ip), 40 | _ => None, 41 | }) 42 | .collect::>(); 43 | if records.is_empty() { 44 | Err(io::Error::new(ErrorKind::NotFound, "IPv6 record not found").into()) 45 | } else { 46 | Ok(records) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ytflow/src/data/plugin_cache.rs: -------------------------------------------------------------------------------- 1 | use cbor4ii::serde::{from_slice, to_vec}; 2 | use rusqlite::{params, OptionalExtension}; 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | 5 | use super::{DataResult, Database, PluginId}; 6 | 7 | #[derive(Clone)] 8 | pub struct PluginCache { 9 | plugin_id: PluginId, 10 | db: Option, 11 | } 12 | 13 | impl PluginCache { 14 | pub fn new(plugin_id: PluginId, db: Option) -> Self { 15 | Self { plugin_id, db } 16 | } 17 | 18 | pub fn set(&self, key: &str, value: &T) -> DataResult<()> { 19 | let Some(db) = &self.db else { return Ok(()) }; 20 | let conn = db.connect()?; 21 | conn.execute( 22 | "INSERT OR REPLACE INTO `yt_plugin_cache` (`plugin_id`, `key`, `value`) VALUES (?1, ?2, ?3)", 23 | params![self.plugin_id.0, key, to_vec(vec![], value).unwrap()], 24 | )?; 25 | Ok(()) 26 | } 27 | pub fn get(&self, key: &str) -> DataResult> { 28 | let Some(db) = &self.db else { return Ok(None) }; 29 | let conn = db.connect()?; 30 | let ret = conn 31 | .query_row( 32 | "SELECT `value` FROM `yt_plugin_cache` WHERE `plugin_id` = ?1 AND `key` = ?2", 33 | params![self.plugin_id.0, key], 34 | |row| { 35 | let value: Vec = row.get(0)?; 36 | Ok(from_slice::(&value).ok()) 37 | }, 38 | ) 39 | .optional()? 40 | .flatten(); 41 | Ok(ret) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi/proxy.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::os::raw::c_char; 3 | use std::panic::AssertUnwindSafe; 4 | 5 | use super::error::{ytflow_result, InvalidCborError}; 6 | use super::interop::{serialize_buffer, serialize_byte_buffer}; 7 | use crate::proxy; 8 | 9 | #[no_mangle] 10 | pub unsafe extern "C" fn ytflow_app_proxy_data_proxy_analyze( 11 | name: *const c_char, 12 | data_proxy: *const u8, 13 | data_proxy_len: usize, 14 | version: u16, 15 | ) -> ytflow_result { 16 | ytflow_result::catch_result_unwind(AssertUnwindSafe(move || { 17 | let name = unsafe { CStr::from_ptr(name).to_string_lossy().into_owned() }; 18 | proxy::data::analyze_data_proxy( 19 | name, 20 | std::slice::from_raw_parts(data_proxy, data_proxy_len), 21 | version, 22 | ) 23 | .map(|p| serialize_buffer(&p)) 24 | })) 25 | } 26 | 27 | pub(super) unsafe fn deserialize_proxy_cbor( 28 | data: *const u8, 29 | data_len: usize, 30 | ) -> Result { 31 | let data = std::slice::from_raw_parts(data, data_len); 32 | cbor4ii::serde::from_slice(data).map_err(|_| super::error::InvalidCborError) 33 | } 34 | 35 | #[no_mangle] 36 | pub unsafe extern "C" fn ytflow_app_proxy_data_proxy_compose_v1( 37 | proxy: *const u8, 38 | proxy_len: usize, 39 | ) -> ytflow_result { 40 | ytflow_result::catch_result_unwind(AssertUnwindSafe(move || { 41 | let proxy = deserialize_proxy_cbor(proxy, proxy_len)?; 42 | proxy::data::compose_data_proxy_v1(&proxy) 43 | .map(|p| serialize_byte_buffer(p)) 44 | .map_err(ytflow_result::from) 45 | })) 46 | } 47 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/crypto/stream.rs: -------------------------------------------------------------------------------- 1 | use super::ctor::StreamCryptoCtor; 2 | use super::*; 3 | 4 | pub struct RustCryptoStream { 5 | inner: Ctor::Output, 6 | ctor: PhantomData, 7 | } 8 | 9 | impl ShadowCrypto for RustCryptoStream 10 | where 11 | Ctor: Send + Sync + Unpin + 'static + StreamCryptoCtor, 12 | Ctor::Output: KeySizeUser + StreamCipher + Send + Sync + Unpin + 'static, 13 | { 14 | const KEY_LEN: usize = ::KeySize::USIZE; 15 | const IV_LEN: usize = IV_LEN; 16 | const PRE_CHUNK_OVERHEAD: usize = 0; 17 | const POST_CHUNK_OVERHEAD: usize = 0; 18 | 19 | fn create_crypto(key: &[u8; Self::KEY_LEN], iv: &[u8; Self::IV_LEN]) -> Self { 20 | let inner = Ctor::create_crypto(key, iv); 21 | Self { 22 | inner, 23 | ctor: PhantomData, 24 | } 25 | } 26 | 27 | fn encrypt( 28 | &mut self, 29 | _pre_overhead: &mut [u8; 0], 30 | data: &mut [u8], 31 | _post_overhead: &mut [u8; 0], 32 | ) { 33 | self.inner.apply_keystream(data); 34 | } 35 | fn encrypt_all( 36 | &mut self, 37 | data: &mut [u8], 38 | _post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD], 39 | ) { 40 | self.inner.apply_keystream(data); 41 | } 42 | 43 | fn decrypt_size(&mut self, _pre_overhead: &mut [u8; 0]) -> Option { 44 | None 45 | } 46 | 47 | fn decrypt(&mut self, data: &mut [u8], _post_overhead: &[u8; 0]) -> bool { 48 | self.inner.apply_keystream(data); 49 | true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/null.rs: -------------------------------------------------------------------------------- 1 | use crate::config::factory::*; 2 | use crate::config::*; 3 | 4 | pub struct NullFactory {} 5 | 6 | impl NullFactory { 7 | pub(in super::super) fn parse(plugin: &Plugin) -> ConfigResult> { 8 | let name = plugin.name.clone(); 9 | Ok(ParsedPlugin { 10 | factory: NullFactory {}, 11 | requires: vec![], 12 | provides: vec![ 13 | Descriptor { 14 | descriptor: name.clone() + ".tcp", 15 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 16 | }, 17 | Descriptor { 18 | descriptor: name.clone() + ".udp", 19 | r#type: AccessPointType::DATAGRAM_SESSION_FACTORY, 20 | }, 21 | Descriptor { 22 | descriptor: name + ".resolver", 23 | r#type: AccessPointType::RESOLVER, 24 | }, 25 | ], 26 | resources: vec![], 27 | }) 28 | } 29 | } 30 | 31 | impl Factory for NullFactory { 32 | #[cfg(feature = "plugins")] 33 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 34 | use crate::plugin::null; 35 | 36 | set.fully_constructed 37 | .stream_outbounds 38 | .insert(plugin_name.clone() + ".tcp", Arc::new(null::Null)); 39 | set.fully_constructed 40 | .datagram_outbounds 41 | .insert(plugin_name.clone() + ".udp", Arc::new(null::Null)); 42 | set.fully_constructed 43 | .resolver 44 | .insert(plugin_name + ".resolver", Arc::new(null::Null)); 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Weak; 2 | 3 | #[cfg(feature = "plugins")] 4 | mod builder; 5 | #[cfg(feature = "plugins")] 6 | mod dispatcher; 7 | #[cfg(feature = "plugins")] 8 | mod rules; 9 | #[cfg(feature = "plugins")] 10 | mod set; 11 | 12 | use crate::flow::*; 13 | #[cfg(feature = "plugins")] 14 | pub use builder::RuleDispatcherBuilder; 15 | #[cfg(feature = "plugins")] 16 | pub use dispatcher::RuleDispatcher; 17 | #[cfg(feature = "plugins")] 18 | pub use set::RuleSet; 19 | 20 | pub const ACTION_LIMIT: usize = 15; 21 | 22 | // High 8 bits: ActionHandle (maximum 255 actions, but in doc we say 15) 23 | // Low 24 bits: RuleId (maximum 16M rules, equivalent to 105 copies of SukkaW reject domain set) 24 | #[derive(Clone, Copy, Debug)] 25 | pub struct RuleHandle(u32); 26 | pub type RuleId = u32; 27 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] 28 | pub struct ActionHandle(u8); 29 | 30 | impl RuleHandle { 31 | pub fn new(action: ActionHandle, rule_id: RuleId) -> Self { 32 | Self((action.0 as u32) << 24 | (rule_id & 0x00ffffff)) 33 | } 34 | pub fn action(&self) -> ActionHandle { 35 | ActionHandle((self.0 >> 24) as u8) 36 | } 37 | pub fn set_action(&mut self, action: ActionHandle) { 38 | self.0 = (self.0 & 0x00ffffff) | ((action.0 as u32) << 24); 39 | } 40 | pub fn rule_id(&self) -> RuleId { 41 | self.0 & 0x00ffffff 42 | } 43 | pub fn set_rule_id(&mut self, rule_id: RuleId) { 44 | self.0 = (self.0 & 0xff000000) | (rule_id & 0x00ffffff); 45 | } 46 | } 47 | 48 | pub struct Action { 49 | pub tcp_next: Weak, 50 | pub udp_next: Weak, 51 | pub resolver: Weak, 52 | } 53 | -------------------------------------------------------------------------------- /ytflow/src/plugin/tls/initial_data_extract_stream.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | use std::task::{Context, Poll}; 3 | 4 | use crate::flow::*; 5 | 6 | pub(super) struct InitialDataExtractStream { 7 | pub(super) data: Arc>>, 8 | } 9 | 10 | impl Stream for InitialDataExtractStream { 11 | fn poll_request_size(&mut self, _cx: &mut Context<'_>) -> Poll> { 12 | Poll::Pending 13 | } 14 | 15 | fn commit_rx_buffer(&mut self, _buffer: Buffer) -> Result<(), (Buffer, FlowError)> { 16 | panic!("InitialDataExtractStream: should not commit rx buffer without requesting size"); 17 | } 18 | 19 | fn poll_rx_buffer( 20 | &mut self, 21 | _cx: &mut Context<'_>, 22 | ) -> Poll> { 23 | Poll::Pending 24 | } 25 | 26 | fn poll_tx_buffer( 27 | &mut self, 28 | _cx: &mut Context<'_>, 29 | size: std::num::NonZeroUsize, 30 | ) -> Poll> { 31 | let mut buf = self.data.lock().unwrap(); 32 | let mut buf = buf 33 | .take() 34 | .expect("InitialDataExtractStream: should not poll tx buffer without committing first"); 35 | buf.reserve(size.get()); 36 | Poll::Ready(Ok(buf)) 37 | } 38 | 39 | fn commit_tx_buffer(&mut self, buffer: Buffer) -> FlowResult<()> { 40 | let mut buf = self.data.lock().unwrap(); 41 | *buf = Some(buffer); 42 | Ok(()) 43 | } 44 | 45 | fn poll_flush_tx(&mut self, _cx: &mut Context<'_>) -> Poll> { 46 | Poll::Ready(Ok(())) 47 | } 48 | 49 | fn poll_close_tx(&mut self, _cx: &mut Context<'_>) -> Poll> { 50 | Poll::Ready(Ok(())) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ytflow-app-util/src/cbor/json.rs: -------------------------------------------------------------------------------- 1 | use super::{escape_cbor_buf, unescape_cbor_buf, CborUtilError, CborUtilResult}; 2 | 3 | pub fn cbor_to_json(cbor: &[u8]) -> CborUtilResult { 4 | let mut val = cbor4ii::serde::from_slice(cbor).map_err(|_| CborUtilError::InvalidEncoding)?; 5 | escape_cbor_buf(&mut val); 6 | serde_json::to_string_pretty(&val).map_err(|_| CborUtilError::InvalidEncoding) 7 | } 8 | 9 | pub fn json_to_cbor(json: &str) -> CborUtilResult> { 10 | let mut val = serde_json::from_str(json).map_err(|_| CborUtilError::InvalidEncoding)?; 11 | unescape_cbor_buf(&mut val)?; 12 | cbor4ii::serde::to_vec(vec![], &val).map_err(|_| CborUtilError::InvalidEncoding) 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::*; 18 | 19 | #[test] 20 | fn test_cbor_to_json() { 21 | let cbor = b"\x42\x68\x68"; 22 | let json = cbor_to_json(cbor).unwrap(); 23 | assert_eq!( 24 | json, 25 | r#"{ 26 | "__byte_repr": "utf8", 27 | "data": "hh" 28 | }"# 29 | ); 30 | } 31 | #[test] 32 | fn test_cbor_to_json_invalid_cbor() { 33 | let cbor = b"\x42\x68"; 34 | let res = cbor_to_json(cbor); 35 | assert_eq!(res, Err(CborUtilError::InvalidEncoding)); 36 | } 37 | 38 | #[test] 39 | fn test_json_to_cbor() { 40 | let json = r#"{ "__byte_repr": "utf8", "data": "hh" }"#; 41 | let cbor = json_to_cbor(json).unwrap(); 42 | let expected_cbor = b"\x42\x68\x68"; 43 | assert_eq!(cbor, expected_cbor); 44 | } 45 | #[test] 46 | fn test_json_to_cbor_invalid_json() { 47 | let json = "{ "; 48 | let res = json_to_cbor(json); 49 | assert_eq!(res, Err(CborUtilError::InvalidEncoding)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ytflow/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | mod db; 2 | mod error; 3 | mod plugin; 4 | mod plugin_cache; 5 | mod profile; 6 | mod proxy; 7 | pub mod proxy_group; 8 | mod resource; 9 | 10 | use std::fmt::{self, Debug, Display, Formatter}; 11 | use std::marker::PhantomData; 12 | 13 | use serde::Serialize; 14 | 15 | #[derive(Serialize)] 16 | #[serde(transparent)] 17 | pub struct Id(pub u32, PhantomData); 18 | 19 | impl Debug for Id { 20 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 21 | write!(f, "Id({})", self.0) 22 | } 23 | } 24 | 25 | impl Display for Id { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 27 | write!(f, "{}", self.0) 28 | } 29 | } 30 | 31 | impl Clone for Id { 32 | fn clone(&self) -> Id { 33 | *self 34 | } 35 | } 36 | impl Copy for Id {} 37 | impl PartialEq for Id { 38 | fn eq(&self, rhs: &Self) -> bool { 39 | self.0.eq(&rhs.0) 40 | } 41 | } 42 | impl Eq for Id {} 43 | impl From for Id { 44 | fn from(id: u32) -> Self { 45 | Self(id, PhantomData) 46 | } 47 | } 48 | impl Default for Id { 49 | fn default() -> Self { 50 | Self(u32::default(), PhantomData) 51 | } 52 | } 53 | impl Id { 54 | pub const fn new(id: u32) -> Self { 55 | Self(id, PhantomData) 56 | } 57 | } 58 | 59 | pub use db::Connection; 60 | pub use db::Database; 61 | pub use error::*; 62 | pub use plugin::{Plugin, PluginId}; 63 | pub use plugin_cache::PluginCache; 64 | pub use profile::{Profile, ProfileId}; 65 | pub use proxy::{Proxy, ProxyId, ProxyInput}; 66 | pub use proxy_group::{ProxyGroup, ProxyGroupId, ProxySubscription}; 67 | pub use resource::{ 68 | Resource, ResourceGitHubRelease, ResourceGitHubReleaseId, ResourceId, ResourceUrl, 69 | ResourceUrlId, 70 | }; 71 | -------------------------------------------------------------------------------- /ytflow/src/plugin/trojan.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Weak; 2 | 3 | use async_trait::async_trait; 4 | use sha2::Digest; 5 | 6 | use crate::flow::*; 7 | 8 | pub struct TrojanStreamOutboundFactory { 9 | password_hex: [u8; 56], 10 | next: Weak, 11 | } 12 | 13 | impl TrojanStreamOutboundFactory { 14 | pub fn new(password: &[u8], next: Weak) -> Self { 15 | fn nibble_to_hex(n: u8) -> u8 { 16 | match n { 17 | 0..=9 => n + 48, 18 | _ => n + 87, 19 | } 20 | } 21 | let hash = sha2::Sha224::digest(password); 22 | let mut hex = Vec::with_capacity(56); 23 | for x in hash { 24 | hex.push(nibble_to_hex(x >> 4)); 25 | hex.push(nibble_to_hex(x & 0x0F)); 26 | } 27 | Self { 28 | password_hex: (&*hex).try_into().unwrap(), 29 | next, 30 | } 31 | } 32 | } 33 | 34 | #[async_trait] 35 | impl StreamOutboundFactory for TrojanStreamOutboundFactory { 36 | async fn create_outbound( 37 | &self, 38 | context: &mut FlowContext, 39 | initial_data: &'_ [u8], 40 | ) -> FlowResult<(Box, Buffer)> { 41 | let outbound_factory = self.next.upgrade().ok_or(FlowError::NoOutbound)?; 42 | 43 | let mut tx_handshake = Vec::with_capacity(320 + initial_data.len()); 44 | tx_handshake.extend_from_slice(&self.password_hex); 45 | tx_handshake.extend_from_slice(b"\r\n\x01"); 46 | super::shadowsocks::util::write_dest(&mut tx_handshake, &context.remote_peer); 47 | tx_handshake.extend_from_slice(b"\r\n"); 48 | tx_handshake.extend_from_slice(initial_data); 49 | 50 | outbound_factory 51 | .create_outbound(context, &tx_handshake) 52 | .await 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/crypto/cfb128.rs: -------------------------------------------------------------------------------- 1 | use cfb_mode::{BufDecryptor, BufEncryptor}; 2 | use cipher::{generic_array::ArrayLength, BlockCipher, BlockEncrypt, BlockSizeUser}; 3 | 4 | use super::*; 5 | 6 | pub struct RustCryptoCfb128 { 7 | enc: BufEncryptor, 8 | dec: BufDecryptor, 9 | } 10 | 11 | impl ShadowCrypto 12 | for RustCryptoCfb128 13 | where 14 | C: KeyInit + Unpin + Send + Sync + 'static, 15 | <::BlockSize as ArrayLength>::ArrayType: Unpin, 16 | { 17 | const KEY_LEN: usize = C::KeySize::USIZE; 18 | const IV_LEN: usize = IV_LEN; 19 | const PRE_CHUNK_OVERHEAD: usize = 0; 20 | const POST_CHUNK_OVERHEAD: usize = 0; 21 | 22 | fn create_crypto(key: &[u8; Self::KEY_LEN], iv: &[u8; Self::IV_LEN]) -> Self { 23 | Self { 24 | enc: BufEncryptor::new_from_slices(key, iv).unwrap(), 25 | dec: BufDecryptor::new_from_slices(key, iv).unwrap(), 26 | } 27 | } 28 | 29 | fn encrypt( 30 | &mut self, 31 | _pre_overhead: &mut [u8; 0], 32 | data: &mut [u8], 33 | _post_overhead: &mut [u8; 0], 34 | ) { 35 | self.enc.encrypt(data); 36 | } 37 | fn encrypt_all( 38 | &mut self, 39 | data: &mut [u8], 40 | _post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD], 41 | ) { 42 | self.enc.encrypt(data); 43 | } 44 | 45 | fn decrypt_size( 46 | &mut self, 47 | _pre_overhead: &mut [u8; Self::PRE_CHUNK_OVERHEAD], 48 | ) -> Option { 49 | None 50 | } 51 | 52 | fn decrypt( 53 | &mut self, 54 | data: &mut [u8], 55 | _post_overhead: &[u8; Self::POST_CHUNK_OVERHEAD], 56 | ) -> bool { 57 | self.dec.decrypt(data); 58 | true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/vpntun.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 3 | 4 | use cidr::{Ipv4Cidr, Ipv6Cidr}; 5 | 6 | use crate::config::factory::*; 7 | use crate::config::HumanRepr; 8 | use crate::config::*; 9 | 10 | pub type TunFactory = Box Arc>; 11 | 12 | thread_local! { 13 | /// To be instantiated by a VPN entrypoint after parsing. 14 | pub static ON_VPNTUN: RefCell> = RefCell::new(None); 15 | } 16 | 17 | #[derive(Clone, Deserialize)] 18 | pub struct VpnTunFactory { 19 | pub ipv4: Option>, 20 | pub ipv6: Option>, 21 | pub ipv4_route: Vec>, 22 | pub ipv6_route: Vec>, 23 | pub dns: Vec>, 24 | // Use String so that the struct can be 'static. 25 | pub web_proxy: Option, 26 | } 27 | 28 | impl VpnTunFactory { 29 | pub(in super::super) fn parse(plugin: &Plugin) -> ConfigResult> { 30 | let Plugin { name, param, .. } = plugin; 31 | let config: Self = parse_param(name, param)?; 32 | Ok(ParsedPlugin { 33 | factory: config, 34 | requires: vec![], 35 | provides: vec![Descriptor { 36 | descriptor: name.to_string() + ".tun", 37 | r#type: AccessPointType::TUN, 38 | }], 39 | resources: vec![], 40 | }) 41 | } 42 | } 43 | impl Factory for VpnTunFactory { 44 | #[cfg(feature = "plugins")] 45 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 46 | let tun = (ON_VPNTUN.with(|cb| cb.borrow_mut().take())).ok_or_else(|| { 47 | ConfigError::TooManyPlugin { 48 | plugin: plugin_name.clone() + ".tun", 49 | r#type: "vpn-tun", 50 | } 51 | })?(self); 52 | set.fully_constructed.tun.insert(plugin_name + ".tun", tun); 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi/subscription.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::os::raw::c_char; 3 | use std::panic::AssertUnwindSafe; 4 | 5 | use crate::subscription::{ 6 | decode_subscription, decode_subscription_with_format, DecodeError, SubscriptionFormat, 7 | SubscriptionUserInfo, 8 | }; 9 | 10 | use super::error::{ytflow_result, InvalidCborError}; 11 | use super::interop::serialize_buffer; 12 | 13 | #[no_mangle] 14 | pub unsafe extern "C" fn ytflow_app_subscription_userinfo_header_decode( 15 | header: *const c_char, 16 | ) -> ytflow_result { 17 | ytflow_result::catch_result_unwind(AssertUnwindSafe(move || { 18 | let header = unsafe { CStr::from_ptr(header).to_string_lossy() }; 19 | let info = SubscriptionUserInfo::decode_header(&header); 20 | Ok::<_, InvalidCborError>(serialize_buffer(&info)) 21 | })) 22 | } 23 | 24 | #[no_mangle] 25 | pub unsafe extern "C" fn ytflow_app_subscription_decode( 26 | subscription: *const u8, 27 | subscription_len: usize, 28 | format: *mut *const c_char, 29 | ) -> ytflow_result { 30 | ytflow_result::catch_result_unwind(AssertUnwindSafe(move || { 31 | let subscription = std::slice::from_raw_parts(subscription, subscription_len); 32 | *format = std::ptr::null_mut(); 33 | let (subscription, decoded_format) = decode_subscription(subscription)?; 34 | *format = <&'static CStr>::from(decoded_format).as_ptr(); 35 | Ok::<_, DecodeError>(serialize_buffer(&subscription)) 36 | })) 37 | } 38 | 39 | #[no_mangle] 40 | pub unsafe extern "C" fn ytflow_app_subscription_decode_with_format( 41 | subscription: *const u8, 42 | subscription_len: usize, 43 | format: *const c_char, 44 | ) -> ytflow_result { 45 | ytflow_result::catch_result_unwind(AssertUnwindSafe(move || { 46 | let subscription = std::slice::from_raw_parts(subscription, subscription_len); 47 | let format = SubscriptionFormat(CStr::from_ptr(format).to_bytes_with_nul()); 48 | decode_subscription_with_format(subscription, format).map(|s| serialize_buffer(&s)) 49 | })) 50 | } 51 | -------------------------------------------------------------------------------- /ytflow-app-util/src/share_link/shadowsocks.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | mod decode_legacy; 4 | mod decode_sip002; 5 | mod encode; 6 | 7 | use super::decode::{extract_name_from_frag, DecodeResult, QueryMap}; 8 | use crate::proxy::protocol::ShadowsocksProxy; 9 | use crate::proxy::Proxy; 10 | pub(crate) use decode_sip002::decode_shadowsocks_plugin_opts; 11 | 12 | impl ShadowsocksProxy { 13 | pub(super) fn decode_share_link(url: &Url, queries: &mut QueryMap) -> DecodeResult { 14 | let leg = if url.username().is_empty() { 15 | decode_legacy::decode_legacy(url, queries) 16 | } else { 17 | decode_sip002::decode_sip002(url, queries) 18 | }?; 19 | 20 | Ok(Proxy { 21 | name: extract_name_from_frag(url, &leg.dest)?, 22 | legs: vec![leg], 23 | udp_supported: true, 24 | }) 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use url::Url; 31 | 32 | use crate::proxy::protocol::ProxyProtocolType; 33 | 34 | use super::*; 35 | 36 | #[test] 37 | fn test_decode_legacy() { 38 | let url = Url::parse("ss://YWVzLTI1Ni1jZmI6VVlMMUV2a2ZJMGNUNk5PWUAzLjE4Ny4yMjUuNzozNDE4Nw") 39 | .unwrap(); 40 | let mut queries = QueryMap::new(); 41 | let proxy = ShadowsocksProxy::decode_share_link(&url, &mut queries).unwrap(); 42 | assert_eq!(proxy.legs[0].dest.port, 34187); 43 | assert!(queries.is_empty()); 44 | } 45 | 46 | #[test] 47 | fn test_decode_sip002() { 48 | let url = 49 | Url::parse("ss://YWVzLTI1Ni1jZmI6VVlMMUV2a2ZJMGNUNk5PWQ@3.187.225.7:34187").unwrap(); 50 | let mut queries = QueryMap::new(); 51 | let mut proxy = ShadowsocksProxy::decode_share_link(&url, &mut queries).unwrap(); 52 | let ss = match proxy.legs.pop().unwrap().protocol { 53 | ProxyProtocolType::Shadowsocks(ss) => ss, 54 | p => panic!("unexpected protocol type {:?}", p), 55 | }; 56 | assert_eq!(&ss.password, b"UYL1EvkfI0cT6NOY"); 57 | assert!(queries.is_empty()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ytflow/src/control/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::convert::Infallible; 3 | 4 | use cbor4ii::serde::DecodeError; 5 | use serde::{Deserialize, Serialize, Serializer}; 6 | use serde_bytes::ByteBuf; 7 | use thiserror::Error; 8 | 9 | #[derive(Debug, Error, Serialize)] 10 | pub enum PluginRequestError { 11 | #[error("No such plugin")] 12 | NoSuchPlugin, 13 | #[error("No such func")] 14 | NoSuchFunc, 15 | #[error("Bad param")] 16 | #[serde(serialize_with = "serialize_bad_param")] 17 | #[serde(skip_deserializing)] 18 | BadParam(#[from] DecodeError), 19 | } 20 | 21 | fn serialize_bad_param(t: &DecodeError, s: S) -> Result 22 | where 23 | S: Serializer, 24 | { 25 | s.serialize_str(&format!("Bad Param: {}", t)) 26 | } 27 | 28 | pub type PluginRequestResult = Result; 29 | 30 | pub trait PluginResponder: Send + Sync + 'static { 31 | fn collect_info(&self, hash: &mut u32) -> Option>; 32 | fn on_request(&self, func: &str, params: &[u8]) -> PluginRequestResult>; 33 | } 34 | 35 | pub struct PluginControlHandle { 36 | // TODO: send notification 37 | } 38 | 39 | pub(super) struct PluginController { 40 | pub(super) id: u32, 41 | pub(super) name: String, 42 | pub(super) plugin: &'static str, 43 | pub(super) responder: Box, 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize)] 47 | pub struct PluginInfo<'a> { 48 | pub id: u32, 49 | pub name: Cow<'a, str>, 50 | pub plugin: Cow<'a, str>, 51 | pub info: ByteBuf, 52 | pub hashcode: u32, 53 | } 54 | 55 | impl PluginController { 56 | pub fn collect_info(&self, mut hashcode: u32) -> Option { 57 | self.responder 58 | .collect_info(&mut hashcode) 59 | .map(|info| PluginInfo { 60 | id: self.id, 61 | name: Cow::Borrowed(&self.name), 62 | plugin: Cow::Borrowed(self.plugin), 63 | info: ByteBuf::from(info), 64 | hashcode, 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ytflow/src/plugin/tls/load_certs_windows.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::{c_int, c_void}; 2 | use std::sync::OnceLock; 3 | 4 | use foreign_types_shared::ForeignType; 5 | use openssl::ssl::SslConnectorBuilder; 6 | use openssl::x509::store::{X509Store, X509StoreBuilder}; 7 | use openssl::x509::X509; 8 | use windows::core::ComInterface; 9 | use windows::Security::Cryptography::Certificates; 10 | use windows::Storage::Streams::IBuffer; 11 | use windows::Win32::System::WinRT::IBufferByteAccess; 12 | 13 | static CERT_STORE: OnceLock = OnceLock::new(); 14 | 15 | pub(crate) fn query_slice_from_ibuffer_mut(buf: &mut IBuffer) -> &mut [u8] { 16 | let len = buf.Length().unwrap() as _; 17 | let byte_access: IBufferByteAccess = buf.cast().unwrap(); 18 | #[allow(unused_unsafe)] 19 | unsafe { 20 | let ptr = byte_access.Buffer().unwrap(); 21 | std::slice::from_raw_parts_mut(ptr, len) 22 | } 23 | } 24 | 25 | fn load_store() -> X509Store { 26 | let cert_query = Certificates::CertificateQuery::new().unwrap(); 27 | cert_query.SetStoreName(&"ROOT".into()).unwrap(); 28 | let all_certs = Certificates::CertificateStores::FindAllWithQueryAsync(&cert_query) 29 | .unwrap() 30 | .get() 31 | .unwrap(); 32 | let mut builder = X509StoreBuilder::new().unwrap(); 33 | for cert in all_certs { 34 | let mut buf = cert.GetCertificateBlob().unwrap(); 35 | let buf = query_slice_from_ibuffer_mut(&mut buf); 36 | let cert = X509::from_der(buf).unwrap(); 37 | builder.add_cert(cert).unwrap(); 38 | } 39 | builder.build() 40 | } 41 | 42 | extern "C" { 43 | fn X509_STORE_up_ref(v: *mut c_void) -> c_int; 44 | } 45 | 46 | pub(super) fn load(builder: &mut SslConnectorBuilder) { 47 | let store_ref = CERT_STORE.get_or_init(load_store); 48 | let store_ptr = store_ref.as_ptr(); 49 | let store = unsafe { 50 | if unsafe { X509_STORE_up_ref(store_ptr as _) } != 1 { 51 | panic!("Failed to clone x509 store ref"); 52 | } 53 | X509Store::from_ptr(store_ptr) 54 | }; 55 | builder.set_cert_store(store); 56 | } 57 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/tls_obfs.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::factory::*; 4 | use crate::config::*; 5 | 6 | #[cfg_attr(not(feature = "plugins"), allow(dead_code))] 7 | #[derive(Deserialize)] 8 | pub struct TlsObfsClientFactory<'a> { 9 | host: &'a str, 10 | next: &'a str, 11 | } 12 | 13 | impl<'de> TlsObfsClientFactory<'de> { 14 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 15 | let Plugin { name, param, .. } = plugin; 16 | let config: Self = parse_param(name, param)?; 17 | let next = config.next; 18 | Ok(ParsedPlugin { 19 | factory: config, 20 | requires: vec![Descriptor { 21 | descriptor: next, 22 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 23 | }], 24 | provides: vec![Descriptor { 25 | descriptor: name.to_string() + ".tcp", 26 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 27 | }], 28 | resources: vec![], 29 | }) 30 | } 31 | } 32 | 33 | impl<'de> Factory for TlsObfsClientFactory<'de> { 34 | #[cfg(feature = "plugins")] 35 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 36 | use crate::plugin::null::Null; 37 | use crate::plugin::obfs::simple_tls; 38 | 39 | let factory = Arc::new_cyclic(|weak| { 40 | set.stream_outbounds 41 | .insert(plugin_name.clone() + ".tcp", weak.clone() as _); 42 | let next = match set.get_or_create_stream_outbound(plugin_name.clone(), self.next) { 43 | Ok(next) => next, 44 | Err(e) => { 45 | set.errors.push(e); 46 | Arc::downgrade(&(Arc::new(Null))) 47 | } 48 | }; 49 | 50 | simple_tls::SimpleTlsOutbound::new(self.host.as_bytes(), next) 51 | }); 52 | set.fully_constructed 53 | .stream_outbounds 54 | .insert(plugin_name + ".tcp", factory); 55 | Ok(()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ytflow/src/plugin/netif/responder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use cbor4ii::serde::{from_slice, to_vec}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::control::{PluginRequestError, PluginRequestResult, PluginResponder}; 7 | 8 | pub struct Responder { 9 | selector: Arc, 10 | } 11 | 12 | #[derive(Serialize)] 13 | struct Info<'a> { 14 | selection: &'a super::SelectionMode, 15 | preference: super::FamilyPreference, 16 | netif: &'a super::sys::Netif, 17 | } 18 | 19 | #[derive(Deserialize)] 20 | struct SelectRequest { 21 | selection: super::SelectionMode, 22 | preference: super::FamilyPreference, 23 | } 24 | 25 | impl Responder { 26 | pub fn new(selector: Arc) -> Self { 27 | Self { selector } 28 | } 29 | } 30 | 31 | impl PluginResponder for Responder { 32 | fn collect_info(&self, hashcode: &mut u32) -> Option> { 33 | let super::NetifSelector { 34 | selection, 35 | cached_netif, 36 | .. 37 | } = &*self.selector; 38 | let selection = selection.load(); 39 | let netif = cached_netif.load(); 40 | let new_hashcode = (Arc::as_ptr(&selection) as u32) << 16 | Arc::as_ptr(&netif) as u32; 41 | if std::mem::replace(hashcode, new_hashcode) == new_hashcode { 42 | return None; 43 | } 44 | let info = Info { 45 | selection: &selection.0, 46 | preference: selection.1, 47 | netif: &netif, 48 | }; 49 | Some(to_vec(vec![], &info).unwrap()) 50 | } 51 | 52 | fn on_request(&self, func: &str, params: &[u8]) -> PluginRequestResult> { 53 | Ok(match func { 54 | "select" => { 55 | let info: SelectRequest = from_slice(params)?; 56 | self.selector 57 | .selection 58 | .store(Arc::new((info.selection, info.preference))); 59 | self.selector.update(); 60 | vec![] 61 | } 62 | _ => return Err(PluginRequestError::NoSuchFunc), 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ytflow/src/plugin/forward/responder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::Ordering, Mutex}; 2 | 3 | use cbor4ii::serde::to_vec; 4 | use serde::Serialize; 5 | 6 | use super::StatHandle; 7 | use crate::control::{PluginRequestError, PluginRequestResult, PluginResponder}; 8 | 9 | #[derive(Clone, Default, Serialize, PartialEq, Eq)] 10 | struct StatInfo { 11 | uplink_written: u64, 12 | downlink_written: u64, 13 | tcp_connection_count: u32, 14 | udp_session_count: u32, 15 | } 16 | 17 | pub struct Responder { 18 | stat: StatHandle, 19 | last_stat: Mutex<(StatInfo, u32)>, 20 | } 21 | 22 | impl Responder { 23 | pub fn new(stat: StatHandle) -> Self { 24 | Self { 25 | stat, 26 | last_stat: Mutex::new((StatInfo::default(), 1)), 27 | } 28 | } 29 | } 30 | 31 | fn stat_snapshot(stat: &StatHandle) -> StatInfo { 32 | let inner = &stat.inner; 33 | StatInfo { 34 | uplink_written: inner.uplink_written.load(Ordering::Relaxed), 35 | downlink_written: inner.downlink_written.load(Ordering::Relaxed), 36 | tcp_connection_count: inner.tcp_connection_count.load(Ordering::Relaxed), 37 | udp_session_count: inner.udp_session_count.load(Ordering::Relaxed), 38 | } 39 | } 40 | 41 | impl PluginResponder for Responder { 42 | fn collect_info(&self, hashcode: &mut u32) -> Option> { 43 | let stat = { 44 | let mut last_stat_guard = self.last_stat.lock().unwrap(); 45 | let (last_stat, last_hashcode) = &mut *last_stat_guard; 46 | let new_stat = stat_snapshot(&self.stat); 47 | if new_stat == *last_stat { 48 | if *last_hashcode == *hashcode { 49 | return None; 50 | } 51 | } else { 52 | *last_stat = new_stat.clone(); 53 | *last_hashcode = (*last_hashcode).wrapping_add(1); 54 | } 55 | *hashcode = *last_hashcode; 56 | new_stat 57 | }; 58 | Some(to_vec(vec![], &stat).unwrap()) 59 | } 60 | 61 | fn on_request(&self, _func: &str, _params: &[u8]) -> PluginRequestResult> { 62 | Err(PluginRequestError::NoSuchFunc) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ytflow/src/plugin/rule_dispatcher/rules/domain.rs: -------------------------------------------------------------------------------- 1 | use aho_corasick::Match; 2 | 3 | use super::super::{set::RuleMappedAhoCorasick, RuleHandle, RuleSet}; 4 | 5 | fn match_ac<'a>( 6 | domain: &'a str, 7 | ac: &'a RuleMappedAhoCorasick, 8 | filter: impl FnMut(&Match) -> bool + 'a, 9 | ) -> impl Iterator + 'a { 10 | let matches = ac.ac.find_overlapping_iter(domain); 11 | let handle_it = matches.into_iter().filter(filter).map(|m| { 12 | ac.handle_map 13 | .iter() 14 | .find(|(range, _)| range.contains(&m.pattern().as_usize())) 15 | .expect("Cannot find a matching rule for domain") 16 | .1 17 | }); 18 | handle_it 19 | } 20 | 21 | impl RuleSet { 22 | pub(in super::super) fn match_domain_impl<'a>( 23 | &'a self, 24 | mut domain: &'a str, 25 | ) -> impl Iterator + 'a { 26 | domain = domain.strip_suffix('.').unwrap_or(domain); 27 | let full_it = self 28 | .dst_domain_full 29 | .iter() 30 | .flat_map(|ac_set| match_ac(domain, ac_set, |m| m.range() == (0..domain.len()))); 31 | let sub_it = self.dst_domain_sub.iter().flat_map(|ac_set| { 32 | match_ac(domain, ac_set, |m| { 33 | m.end() == domain.len() 34 | && (m.start() == 0 || domain.as_bytes()[m.start() - 1] == b'.') 35 | }) 36 | }); 37 | let keyword_it = self 38 | .dst_domain_keyword 39 | .iter() 40 | .flat_map(|ac_set| match_ac(domain, ac_set, |_| true)); 41 | let regex_it = self.dst_domain_regex.iter().flat_map(|regex_set| { 42 | let matches = regex_set.regex_set.matches(domain.as_bytes()); 43 | let handle_it = matches.into_iter().map(|m| { 44 | regex_set 45 | .handle_map 46 | .iter() 47 | .find(|(range, _)| range.contains(&m)) 48 | .expect("Cannot find a matching rule for domain regex") 49 | .1 50 | }); 51 | handle_it 52 | }); 53 | full_it.chain(sub_it).chain(keyword_it).chain(regex_it) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/tls.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::factory::*; 4 | use crate::config::*; 5 | 6 | #[cfg_attr(not(feature = "plugins"), allow(dead_code))] 7 | #[derive(Deserialize)] 8 | pub struct TlsFactory<'a> { 9 | sni: Option<&'a str>, 10 | #[serde(borrow, default)] 11 | alpn: Vec<&'a str>, 12 | #[serde(default)] 13 | skip_cert_check: bool, 14 | next: &'a str, 15 | } 16 | 17 | impl<'de> TlsFactory<'de> { 18 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 19 | let Plugin { name, param, .. } = plugin; 20 | let config: Self = parse_param(name, param)?; 21 | let next = config.next; 22 | Ok(ParsedPlugin { 23 | factory: config, 24 | requires: vec![Descriptor { 25 | descriptor: next, 26 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 27 | }], 28 | provides: vec![Descriptor { 29 | descriptor: name.to_string() + ".tcp", 30 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 31 | }], 32 | resources: vec![], 33 | }) 34 | } 35 | } 36 | 37 | impl<'de> Factory for TlsFactory<'de> { 38 | #[cfg(feature = "plugins")] 39 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 40 | use crate::plugin::null::Null; 41 | use crate::plugin::tls; 42 | 43 | let factory = Arc::new_cyclic(|weak| { 44 | set.stream_outbounds 45 | .insert(plugin_name.clone() + ".tcp", weak.clone() as _); 46 | let next = match set.get_or_create_stream_outbound(plugin_name.clone(), self.next) { 47 | Ok(next) => next, 48 | Err(e) => { 49 | set.errors.push(e); 50 | Arc::downgrade(&(Arc::new(Null))) 51 | } 52 | }; 53 | 54 | tls::SslStreamFactory::new( 55 | next, 56 | std::mem::take(&mut self.alpn), 57 | self.skip_cert_check, 58 | self.sni.map(|s| s.to_string()), 59 | ) 60 | }); 61 | set.fully_constructed 62 | .stream_outbounds 63 | .insert(plugin_name + ".tcp", factory); 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/fakeip.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::factory::*; 4 | use crate::config::*; 5 | use crate::data::PluginId; 6 | 7 | #[cfg_attr(not(feature = "plugins"), allow(dead_code))] 8 | #[derive(Clone, Deserialize)] 9 | pub struct FakeIpFactory<'a> { 10 | prefix_v4: [u8; 2], 11 | prefix_v6: [u8; 14], 12 | // Reserved for CNAME, TXT and SRV support 13 | fallback: &'a str, 14 | #[serde(skip)] 15 | plugin_id: Option, 16 | } 17 | 18 | impl<'de> FakeIpFactory<'de> { 19 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 20 | let Plugin { 21 | name, param, id, .. 22 | } = plugin; 23 | let mut config: Self = parse_param(name, param)?; 24 | config.plugin_id = *id; 25 | Ok(ParsedPlugin { 26 | factory: config.clone(), 27 | requires: vec![Descriptor { 28 | descriptor: config.fallback, 29 | r#type: AccessPointType::RESOLVER, 30 | }], 31 | provides: vec![Descriptor { 32 | descriptor: name.to_string() + ".resolver", 33 | r#type: AccessPointType::RESOLVER, 34 | }], 35 | resources: vec![], 36 | }) 37 | } 38 | } 39 | 40 | impl<'de> Factory for FakeIpFactory<'de> { 41 | #[cfg(feature = "plugins")] 42 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 43 | use crate::{data::PluginCache, plugin::fakeip}; 44 | 45 | let db = set 46 | .db 47 | .ok_or_else(|| LoadError::DatabaseRequired { 48 | plugin: plugin_name.clone(), 49 | })? 50 | .clone(); 51 | let cache = PluginCache::new( 52 | self.plugin_id.ok_or_else(|| LoadError::DatabaseRequired { 53 | plugin: plugin_name.clone(), 54 | })?, 55 | Some(db.clone()), 56 | ); 57 | let plugin = Arc::new(fakeip::FakeIp::new(self.prefix_v4, self.prefix_v6, cache)); 58 | set.fully_constructed 59 | .long_running_tasks 60 | .push(tokio::spawn(fakeip::cache_writer(plugin.clone()))); 61 | set.fully_constructed 62 | .resolver 63 | .insert(plugin_name + ".resolver", plugin); 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ytflow/src/plugin/ip_stack/tcp_socket_entry.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicI64, Ordering}; 2 | use std::sync::{Arc, Mutex, MutexGuard}; 3 | use std::time::Instant; 4 | 5 | use smoltcp::iface::SocketHandle; 6 | use smoltcp::socket::tcp::Socket as TcpSocket; 7 | 8 | use super::*; 9 | 10 | pub(super) struct TcpSocketEntry { 11 | pub(super) socket_handle: SocketHandle, 12 | pub(super) local_endpoint: SocketAddr, 13 | pub(super) stack: Arc>, 14 | pub(super) most_recent_scheduled_poll: Arc, 15 | } 16 | 17 | impl TcpSocketEntry { 18 | pub fn lock(&self) -> SocketEntryGuard<'_> { 19 | SocketEntryGuard { 20 | entry: self, 21 | guard: self.stack.lock().unwrap(), 22 | } 23 | } 24 | } 25 | 26 | pub(super) struct SocketEntryGuard<'s> { 27 | pub(super) entry: &'s TcpSocketEntry, 28 | pub(super) guard: MutexGuard<'s, IpStackInner>, 29 | } 30 | 31 | impl<'s> SocketEntryGuard<'s> { 32 | pub fn with_socket(&mut self, f: impl FnOnce(&mut TcpSocket) -> R) -> R { 33 | let handle = self.entry.socket_handle; 34 | let socket = self.guard.socket_set.get_mut::>(handle); 35 | f(socket) 36 | } 37 | 38 | pub fn poll(&mut self) { 39 | let now = Instant::now(); 40 | let Self { entry, guard } = self; 41 | let TcpSocketEntry { 42 | stack, 43 | most_recent_scheduled_poll, 44 | .. 45 | } = entry; 46 | let IpStackInner { 47 | netif, 48 | dev, 49 | socket_set, 50 | .. 51 | } = &mut **guard; 52 | let _ = netif.poll(now.into(), dev, socket_set); 53 | if let Some(delay) = netif.poll_delay(now.into(), socket_set) { 54 | let scheduled_poll_milli = (smoltcp::time::Instant::from(now) + delay).total_millis(); 55 | if scheduled_poll_milli >= most_recent_scheduled_poll.load(Ordering::Relaxed) { 56 | return; 57 | } 58 | // TODO: CAS spin loop 59 | most_recent_scheduled_poll.store(scheduled_poll_milli, Ordering::Relaxed); 60 | 61 | tokio::spawn(schedule_repoll( 62 | stack.clone(), 63 | now + Duration::from(delay), 64 | Arc::clone(most_recent_scheduled_poll), 65 | )); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/http_proxy.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde_bytes::Bytes; 3 | 4 | use crate::config::factory::*; 5 | use crate::config::*; 6 | 7 | #[cfg_attr(not(feature = "plugins"), allow(dead_code))] 8 | #[derive(Deserialize)] 9 | pub struct HttpProxyFactory<'a> { 10 | user: &'a Bytes, 11 | pass: &'a Bytes, 12 | tcp_next: &'a str, 13 | } 14 | 15 | impl<'de> HttpProxyFactory<'de> { 16 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 17 | let Plugin { name, param, .. } = plugin; 18 | let config: Self = parse_param(name, param)?; 19 | let tcp_next = config.tcp_next; 20 | Ok(ParsedPlugin { 21 | factory: config, 22 | requires: vec![Descriptor { 23 | descriptor: tcp_next, 24 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 25 | }], 26 | provides: vec![Descriptor { 27 | descriptor: name.to_string() + ".tcp", 28 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 29 | }], 30 | resources: vec![], 31 | }) 32 | } 33 | } 34 | 35 | impl<'de> Factory for HttpProxyFactory<'de> { 36 | #[cfg(feature = "plugins")] 37 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 38 | use crate::plugin::http_proxy; 39 | use crate::plugin::null::Null; 40 | 41 | let factory = Arc::new_cyclic(|weak| { 42 | set.stream_outbounds 43 | .insert(plugin_name.clone() + ".tcp", weak.clone() as _); 44 | let tcp_next = 45 | match set.get_or_create_stream_outbound(plugin_name.clone(), self.tcp_next) { 46 | Ok(t) => t, 47 | Err(e) => { 48 | set.errors.push(e); 49 | Arc::downgrade(&(Arc::new(Null) as _)) 50 | } 51 | }; 52 | http_proxy::HttpProxyOutboundFactory::new( 53 | Some((self.user, self.pass)) 54 | .filter(|(u, p)| !u.is_empty() && !p.is_empty()) 55 | .map(|(u, p)| (&**u, &**p)), 56 | tcp_next, 57 | ) 58 | }); 59 | set.fully_constructed 60 | .stream_outbounds 61 | .insert(plugin_name + ".tcp", factory); 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ytflow/src/data/db.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | pub use rusqlite::Connection; 4 | 5 | mod embedded_migrations { 6 | use refinery::embed_migrations; 7 | embed_migrations!("src/data/migrations"); 8 | } 9 | 10 | use super::*; 11 | 12 | #[cfg(target_vendor = "uwp")] 13 | fn setup_temp() { 14 | use windows::Storage::ApplicationData; 15 | 16 | use std::sync::Once; 17 | static SETUP_TEMP_ONCE: Once = Once::new(); 18 | 19 | // Mark the library with LLVM dllimport storage class 20 | // See https://rust-lang.github.io/rfcs/1717-dllimport.html . 21 | #[link(name = "winsqlite3", kind = "dylib")] 22 | extern "C" { 23 | static mut sqlite3_temp_directory: *mut ::std::os::raw::c_char; 24 | } 25 | 26 | fn setup_temp_core() -> windows::core::Result<()> { 27 | use std::ffi::CString; 28 | let temp_path = ApplicationData::Current()? 29 | .TemporaryFolder()? 30 | .Path()? 31 | .to_string_lossy(); 32 | unsafe { 33 | let c_path = CString::new(temp_path).unwrap(); 34 | let sqlite_dir = 35 | rusqlite::ffi::sqlite3_mprintf(b"%s\0".as_ptr() as *const _, c_path.as_ptr()); 36 | sqlite3_temp_directory = sqlite_dir; 37 | } 38 | Ok(()) 39 | } 40 | 41 | SETUP_TEMP_ONCE.call_once(|| setup_temp_core().unwrap()); 42 | } 43 | 44 | #[cfg(not(target_vendor = "uwp"))] 45 | fn setup_temp() {} 46 | 47 | #[derive(Clone)] 48 | pub struct Database { 49 | path: PathBuf, 50 | } 51 | 52 | fn connect(path: impl AsRef) -> DataResult { 53 | setup_temp(); 54 | let db = Connection::open(&path)?; 55 | db.pragma_update(None, "foreign_keys", "ON")?; 56 | Ok(db) 57 | } 58 | 59 | impl Database { 60 | pub fn open(path: impl AsRef) -> DataResult { 61 | let mut db = connect(&path)?; 62 | embedded_migrations::migrations::runner().run(&mut db)?; 63 | Ok(Database { 64 | path: path.as_ref().to_path_buf(), 65 | }) 66 | } 67 | 68 | pub fn connect(&self) -> DataResult { 69 | connect(self.path.as_path()) 70 | } 71 | 72 | pub fn connect_temp() -> DataResult { 73 | setup_temp(); 74 | let mut db = Connection::open_in_memory()?; 75 | db.pragma_update(None, "foreign_keys", "ON")?; 76 | embedded_migrations::migrations::runner().run(&mut db)?; 77 | Ok(db) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ytflow/src/plugin/switch/responder.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use super::*; 4 | use crate::control::{PluginRequestError, PluginRequestResult, PluginResponder}; 5 | use crate::data::PluginCache; 6 | 7 | pub const PLUGIN_CACHE_KEY_LAST_SELECT: &str = "last_select"; 8 | 9 | #[derive(Clone, Serialize)] 10 | pub struct Choice { 11 | pub name: String, 12 | pub description: String, 13 | #[serde(skip)] 14 | pub tcp_next: Weak, 15 | #[serde(skip)] 16 | pub udp_next: Weak, 17 | } 18 | 19 | pub struct Responder { 20 | pub choices: Vec, 21 | pub switch: Arc, 22 | pub cache: PluginCache, 23 | } 24 | 25 | #[derive(Serialize)] 26 | struct Info<'a> { 27 | choices: &'a [Choice], 28 | current: u32, 29 | } 30 | 31 | impl Responder { 32 | pub fn switch(&self, idx: u32) -> Option { 33 | let new_choice = self.choices.get(idx as usize)?; 34 | let new_choice = CurrentChoice { 35 | idx, 36 | tcp_next: new_choice.tcp_next.clone(), 37 | udp_next: new_choice.udp_next.clone(), 38 | }; 39 | let old_choice = self.switch.current_choice.swap(Arc::new(new_choice)); 40 | let _ = self.cache.set(PLUGIN_CACHE_KEY_LAST_SELECT, &idx).ok(); 41 | Some(old_choice.idx) 42 | } 43 | } 44 | 45 | impl PluginResponder for Responder { 46 | fn collect_info(&self, hash: &mut u32) -> Option> { 47 | let guard = self.switch.current_choice.load(); 48 | 49 | let ptr_hash = Arc::as_ptr(&guard) as u32; 50 | if std::mem::replace(hash, ptr_hash) == ptr_hash { 51 | return None; 52 | } 53 | 54 | let current = guard.idx; 55 | drop(guard); 56 | 57 | Some( 58 | cbor4ii::serde::to_vec( 59 | vec![], 60 | &Info { 61 | choices: &self.choices, 62 | current, 63 | }, 64 | ) 65 | .unwrap(), 66 | ) 67 | } 68 | 69 | fn on_request(&self, func: &str, params: &[u8]) -> PluginRequestResult> { 70 | match func { 71 | "s" => { 72 | let choice_idx: u32 = cbor4ii::serde::from_slice(params)?; 73 | let ret = self.switch(choice_idx); 74 | Ok(cbor4ii::serde::to_vec(Vec::with_capacity(4), &ret).unwrap()) 75 | } 76 | _ => Err(PluginRequestError::NoSuchFunc), 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/edit/views/utils/cbor_editor.rs: -------------------------------------------------------------------------------- 1 | use ::edit::{edit_bytes_with_builder, Builder as EditorBuilder}; 2 | use anyhow::{Context, Result}; 3 | use cbor4ii::core::Value as CborValue; 4 | 5 | use ytflow_app_util::cbor::{cbor_to_json, unescape_cbor_buf}; 6 | 7 | use crate::edit; 8 | 9 | pub fn open_editor_for_cbor( 10 | ctx: &mut edit::AppContext, 11 | val: &[u8], 12 | mut verify_fn: impl FnMut(CborValue) -> Result, 13 | ) -> Result> { 14 | // Note: some editors will change line endings 15 | const CANCEL_SAFEWORD: &[u8] = b"// === Remove this line to cancel editing ===\n"; 16 | const BAD_JSON_MSG: &[u8] = 17 | b"// === Remove this line and everything below after correcting the errors ===\n"; 18 | 19 | let json_buf = cbor_to_json(val).context("Failed to convert into JSON")?; 20 | let mut edit_buf = CANCEL_SAFEWORD.to_vec(); 21 | edit_buf.extend_from_slice(json_buf.as_bytes()); 22 | let ret = loop { 23 | let input_buf = edit_bytes_with_builder( 24 | &edit_buf, 25 | EditorBuilder::new() 26 | .prefix("ytflow-editor-param-") 27 | .suffix(".json"), 28 | ) 29 | .context("Failed to edit")?; 30 | // Editor process output will mess up the terminal 31 | // Force a redraw 32 | ctx.term.clear().unwrap(); 33 | 34 | if !input_buf.starts_with(CANCEL_SAFEWORD) 35 | || (input_buf.len() == edit_buf.len() && input_buf.as_slice() == edit_buf.as_slice()) 36 | { 37 | return Ok(None); 38 | } 39 | 40 | // Leave a newline in the buffer for correct error messages 41 | match serde_json::from_slice(&input_buf[(CANCEL_SAFEWORD.len() - 1)..]) 42 | .map_err(|e| e.to_string()) 43 | .and_then(|mut v| { 44 | unescape_cbor_buf(&mut v) 45 | .map(|()| v) 46 | .map_err(|e| e.to_string()) 47 | }) 48 | .and_then(|v| verify_fn(v).map_err(|e| e.to_string())) 49 | { 50 | Ok(v) => break v, 51 | Err(err_str) => { 52 | edit_buf.clear(); 53 | edit_buf.reserve(input_buf.len() + BAD_JSON_MSG.len() + err_str.len()); 54 | edit_buf.extend_from_slice(&input_buf); 55 | edit_buf.extend_from_slice(BAD_JSON_MSG); 56 | edit_buf.extend_from_slice(err_str.as_bytes()); 57 | continue; 58 | } 59 | }; 60 | }; 61 | Ok(Some(ret)) 62 | } 63 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/trojan.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde_bytes::Bytes; 3 | 4 | use crate::config::factory::*; 5 | use crate::config::*; 6 | 7 | #[cfg_attr(not(feature = "plugins"), allow(dead_code))] 8 | #[derive(Clone, Deserialize)] 9 | pub struct TrojanFactory<'a> { 10 | password: &'a Bytes, 11 | tls_next: &'a str, 12 | } 13 | 14 | impl<'de> TrojanFactory<'de> { 15 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 16 | let Plugin { name, param, .. } = plugin; 17 | let config: Self = parse_param(name, param)?; 18 | Ok(ParsedPlugin { 19 | factory: config.clone(), 20 | requires: vec![Descriptor { 21 | descriptor: config.tls_next, 22 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 23 | }], 24 | provides: vec![ 25 | Descriptor { 26 | descriptor: name.to_string() + ".tcp", 27 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY, 28 | }, 29 | Descriptor { 30 | descriptor: name.to_string() + ".udp", 31 | r#type: AccessPointType::DATAGRAM_SESSION_FACTORY, 32 | }, 33 | ], 34 | resources: vec![], 35 | }) 36 | } 37 | } 38 | 39 | impl<'de> Factory for TrojanFactory<'de> { 40 | #[cfg(feature = "plugins")] 41 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 42 | use crate::plugin::null::Null; 43 | use crate::plugin::trojan; 44 | 45 | let factory = Arc::new_cyclic(|weak| { 46 | set.stream_outbounds 47 | .insert(plugin_name.clone() + ".tcp", weak.clone() as _); 48 | set.datagram_outbounds.insert( 49 | plugin_name.clone() + ".udp", 50 | // TODO: trojan udp 51 | Arc::downgrade(&Arc::new(Null)) as _, 52 | ); 53 | let tls_next = 54 | match set.get_or_create_stream_outbound(plugin_name.clone(), self.tls_next) { 55 | Ok(t) => t, 56 | Err(e) => { 57 | set.errors.push(e); 58 | Arc::downgrade(&(Arc::new(Null) as _)) 59 | } 60 | }; 61 | trojan::TrojanStreamOutboundFactory::new(self.password, tls_next) 62 | }); 63 | set.fully_constructed 64 | .stream_outbounds 65 | .insert(plugin_name + ".tcp", factory); 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/ip_stack.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::factory::*; 4 | use crate::config::*; 5 | 6 | #[derive(Clone, Deserialize)] 7 | pub struct IpStackFactory<'a> { 8 | tun: &'a str, 9 | tcp_next: &'a str, 10 | udp_next: &'a str, 11 | } 12 | 13 | impl<'de> IpStackFactory<'de> { 14 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 15 | let Plugin { name, param, .. } = plugin; 16 | let config: Self = parse_param(name, param)?; 17 | Ok(ParsedPlugin { 18 | factory: config.clone(), 19 | requires: vec![ 20 | Descriptor { 21 | descriptor: config.tun, 22 | r#type: AccessPointType::TUN, 23 | }, 24 | Descriptor { 25 | descriptor: config.tcp_next, 26 | r#type: AccessPointType::STREAM_HANDLER, 27 | }, 28 | Descriptor { 29 | descriptor: config.udp_next, 30 | r#type: AccessPointType::DATAGRAM_SESSION_HANDLER, 31 | }, 32 | ], 33 | provides: vec![], 34 | resources: vec![], 35 | }) 36 | } 37 | } 38 | 39 | impl<'de> Factory for IpStackFactory<'de> { 40 | #[cfg(feature = "plugins")] 41 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 42 | use crate::plugin::ip_stack; 43 | use crate::plugin::reject::RejectHandler; 44 | 45 | let tun = set.get_or_create_tun(plugin_name.clone(), self.tun)?; 46 | let tcp_next = set 47 | .get_or_create_stream_handler(plugin_name.clone(), self.tcp_next) 48 | .unwrap_or_else(|e| { 49 | set.errors.push(e); 50 | Arc::downgrade(&(Arc::new(RejectHandler) as _)) 51 | }); 52 | let udp_next = set 53 | .get_or_create_datagram_handler(plugin_name.clone(), self.udp_next) 54 | .unwrap_or_else(|e| { 55 | set.errors.push(e); 56 | Arc::downgrade(&(Arc::new(RejectHandler) as _)) 57 | }); 58 | let tun = match tun.upgrade() { 59 | Some(tun) => tun, 60 | None => { 61 | return Err(LoadError::UnsatisfiedStrongReference { 62 | initiator: plugin_name, 63 | descriptor: self.tun.to_string(), 64 | }) 65 | } 66 | }; 67 | set.fully_constructed 68 | .long_running_tasks 69 | .push(ip_stack::run(tun, tcp_next, udp_next)); 70 | Ok(()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ytflow-app-util/src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_safety_doc)] 2 | pub mod cbor; 3 | pub mod config; 4 | pub mod data; 5 | pub mod error; 6 | pub mod interop; 7 | pub mod proxy; 8 | pub mod runtime; 9 | pub mod share_link; 10 | pub mod subscription; 11 | 12 | pub mod exports { 13 | pub use super::ytflow_get_version; 14 | use super::*; 15 | pub use cbor::{ytflow_app_cbor_from_json, ytflow_app_cbor_to_json}; 16 | pub use config::ytflow_plugin_verify; 17 | #[cfg(unix)] 18 | pub use data::ytflow_db_new_unix; 19 | #[cfg(windows)] 20 | pub use data::ytflow_db_new_win32; 21 | pub use data::{ 22 | ytflow_db_conn_free, ytflow_db_conn_new, ytflow_db_free, ytflow_plugin_create, 23 | ytflow_plugin_delete, ytflow_plugin_update, ytflow_plugins_get_by_profile, 24 | ytflow_plugins_get_entry, ytflow_profile_create, ytflow_profile_delete, 25 | ytflow_profile_update, ytflow_profiles_get_all, ytflow_proxy_create, ytflow_proxy_delete, 26 | ytflow_proxy_get_by_proxy_group, ytflow_proxy_group_create, ytflow_proxy_group_delete, 27 | ytflow_proxy_group_get_all, ytflow_proxy_group_get_by_id, ytflow_proxy_group_rename, 28 | ytflow_proxy_reorder, ytflow_proxy_update, ytflow_resource_create_with_github_release, 29 | ytflow_resource_create_with_url, ytflow_resource_delete, ytflow_resource_get_all, 30 | ytflow_resource_github_release_query_by_resource_id, 31 | ytflow_resource_github_release_update_retrieved_by_resource_id, 32 | ytflow_resource_url_query_by_resource_id, 33 | ytflow_resource_url_update_retrieved_by_resource_id, 34 | }; 35 | pub use error::ytflow_result_free; 36 | pub use interop::ytflow_buffer_free; 37 | pub use proxy::{ytflow_app_proxy_data_proxy_analyze, ytflow_app_proxy_data_proxy_compose_v1}; 38 | pub use runtime::{ytflow_runtime_free, ytflow_runtime_new}; 39 | pub use share_link::{ytflow_app_share_link_decode, ytflow_app_share_link_encode}; 40 | pub use subscription::{ 41 | ytflow_app_subscription_decode, ytflow_app_subscription_decode_with_format, 42 | ytflow_app_subscription_userinfo_header_decode, 43 | }; 44 | } 45 | 46 | #[no_mangle] 47 | pub extern "C" fn ytflow_get_version() -> *const std::os::raw::c_char { 48 | use std::ffi::CString; 49 | use std::os::raw::c_char; 50 | use std::ptr::null_mut; 51 | use std::sync::atomic::{AtomicPtr, Ordering}; 52 | static VERSION_CPTR: AtomicPtr = AtomicPtr::new(null_mut()); 53 | let cptr = VERSION_CPTR.load(Ordering::Acquire); 54 | if !cptr.is_null() { 55 | return cptr as _; 56 | } 57 | // Let it leak 58 | VERSION_CPTR.store( 59 | CString::new(env!("CARGO_PKG_VERSION")).unwrap().into_raw(), 60 | Ordering::Release, 61 | ); 62 | ytflow_get_version() 63 | } 64 | -------------------------------------------------------------------------------- /ytflow/src/config/error.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum ConfigError { 7 | #[error(r#"error parsing param for plugin "{0}": {1}"#)] 8 | ParseParam(String, cbor4ii::serde::DecodeError), 9 | #[error(r#"config field "{field:}" for plugin "{plugin:}" is not valid"#)] 10 | InvalidParam { plugin: String, field: &'static str }, 11 | #[error(r#"cannot find descriptor "{descriptor:}" from plugin "{initiator:}", or "{descriptor:}" failed to load previously"#)] 12 | NoAccessPoint { 13 | initiator: String, 14 | descriptor: String, 15 | }, 16 | #[error(r#"descriptor "{descriptor:}" cannot be used as a {r#type:} required by plugin "{initiator:?}""#)] 17 | BadAccessPointType { 18 | initiator: String, 19 | r#type: String, 20 | descriptor: String, 21 | }, 22 | #[error(r#"cannot find plugin "{plugin:}" from plugin "{initiator:}""#)] 23 | NoPlugin { initiator: String, plugin: String }, 24 | #[error(r#"don't know how to create plugin "{initiator:}" of type "{r#type:}" v{version:}"#)] 25 | NoPluginType { 26 | initiator: String, 27 | r#type: String, 28 | version: u16, 29 | }, 30 | #[error("loading plugin {0} exceeds the depth limit")] 31 | RecursionLimitExceeded(String), 32 | #[error(r#"there are too many plugins of type "{r#type:}" when loading plugin "{plugin:}""#)] 33 | TooManyPlugin { 34 | plugin: String, 35 | r#type: &'static str, 36 | }, 37 | } 38 | 39 | #[derive(Debug, Error)] 40 | pub enum LoadError { 41 | #[error("error in config: {0}")] 42 | Config(#[from] ConfigError), 43 | #[error(r#"plugin "{initiator:}" requires "{descriptor:}" to be fully loaded"#)] 44 | UnsatisfiedStrongReference { 45 | initiator: String, 46 | descriptor: String, 47 | }, 48 | #[error(r#"a system error occurs while loading plugin {plugin:}: {error:}"#)] 49 | Io { 50 | plugin: String, 51 | error: std::io::Error, 52 | }, 53 | #[error(r#"an error occurs while loading the resources for plugin {plugin:}: {error:}"#)] 54 | Resource { 55 | plugin: String, 56 | error: crate::resource::ResourceError, 57 | }, 58 | #[error(r#"plugin "{plugin:}" requires a resource "{resource_key:}" to have type "{expected:?}", but it is "{actual:}""#)] 59 | ResourceTypeMismatch { 60 | plugin: String, 61 | resource_key: String, 62 | expected: &'static [&'static str], 63 | actual: String, 64 | }, 65 | #[error(r#"plugin "{plugin:}" required a database to work"#)] 66 | DatabaseRequired { plugin: String }, 67 | } 68 | 69 | pub type ConfigResult = Result; 70 | pub type LoadResult = Result; 71 | -------------------------------------------------------------------------------- /ytflow/src/flow/stream.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use std::task::Context; 4 | use std::task::Poll; 5 | 6 | use async_trait::async_trait; 7 | 8 | use crate::flow::context::FlowContext; 9 | use crate::flow::error::{FlowError, FlowResult}; 10 | 11 | // TODO: custom buffer with offset 12 | pub type Buffer = Vec; 13 | 14 | #[derive(Debug, Clone, Copy)] 15 | pub enum SizeHint { 16 | AtLeast(usize), 17 | Unknown { overhead: usize }, 18 | } 19 | 20 | impl SizeHint { 21 | #[inline] 22 | pub fn with_min_content(self, content_len: usize) -> usize { 23 | match self { 24 | Self::AtLeast(min) => min, 25 | Self::Unknown { overhead } => overhead + content_len, 26 | } 27 | } 28 | } 29 | 30 | impl Default for SizeHint { 31 | fn default() -> SizeHint { 32 | Self::Unknown { overhead: 0 } 33 | } 34 | } 35 | 36 | pub trait Stream: Send { 37 | // Read 38 | fn poll_request_size(&mut self, cx: &mut Context<'_>) -> Poll>; 39 | fn commit_rx_buffer(&mut self, buffer: Buffer) -> Result<(), (Buffer, FlowError)>; 40 | fn poll_rx_buffer(&mut self, cx: &mut Context<'_>) 41 | -> Poll>; 42 | 43 | // Write 44 | fn poll_tx_buffer( 45 | &mut self, 46 | cx: &mut Context<'_>, 47 | size: NonZeroUsize, 48 | ) -> Poll>; 49 | fn commit_tx_buffer(&mut self, buffer: Buffer) -> FlowResult<()>; 50 | fn poll_flush_tx(&mut self, cx: &mut Context<'_>) -> Poll>; 51 | 52 | fn poll_close_tx(&mut self, cx: &mut Context<'_>) -> Poll>; 53 | } 54 | 55 | #[macro_export] 56 | macro_rules! get_request_size_boxed { 57 | ($s: expr) => { 58 | ::futures::future::poll_fn(|cx| $s.poll_request_size(cx)).await 59 | }; 60 | } 61 | 62 | #[macro_export] 63 | macro_rules! get_rx_buffer_boxed { 64 | ($s: expr) => { 65 | ::futures::future::poll_fn(|cx| $s.poll_rx_buffer(cx)).await 66 | }; 67 | } 68 | 69 | #[macro_export] 70 | macro_rules! get_tx_buffer_boxed { 71 | ($s: expr, $size: expr) => { 72 | ::futures::future::poll_fn(|cx| $s.poll_tx_buffer(cx, $size)).await 73 | }; 74 | } 75 | 76 | #[macro_export] 77 | macro_rules! close_tx_boxed { 78 | ($s: expr) => { 79 | ::futures::future::poll_fn(|cx| $s.poll_close_tx(cx)).await 80 | }; 81 | } 82 | 83 | pub trait StreamHandler: Send + Sync { 84 | fn on_stream(&self, lower: Box, initial_data: Buffer, context: Box); 85 | } 86 | 87 | #[async_trait] 88 | pub trait StreamOutboundFactory: Send + Sync { 89 | async fn create_outbound<'s, 'a, 'b>( 90 | &'s self, 91 | context: &'a mut FlowContext, 92 | initial_data: &'b [u8], 93 | ) -> FlowResult<(Box, Buffer)>; 94 | } 95 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/crypto.rs: -------------------------------------------------------------------------------- 1 | mod aead; 2 | mod cfb128; 3 | mod ctor; 4 | mod plain; 5 | mod stream; 6 | 7 | use std::marker::PhantomData; 8 | use std::num::NonZeroUsize; 9 | 10 | use aes_gcm::aes::{Aes128, Aes192, Aes256}; 11 | use aes_gcm::{AeadCore, AeadInPlace}; 12 | use camellia::{Camellia128, Camellia192, Camellia256}; 13 | use cipher::generic_array::{ 14 | typenum::{Unsigned, U12, U16}, 15 | GenericArray, 16 | }; 17 | use cipher::{KeyInit, KeyIvInit, KeySizeUser, StreamCipher}; 18 | use ctr::Ctr64BE; 19 | use hkdf::Hkdf; 20 | use sha1::Sha1; 21 | 22 | use super::util::increase_num_buf; 23 | use aead::RustCryptoAead; 24 | use cfb128::RustCryptoCfb128; 25 | use ctor::{KeyIvCtor, KeyOnlyCtor, Rc4Md5Ctor}; 26 | pub use plain::Plain; 27 | use stream::RustCryptoStream; 28 | 29 | pub trait ShadowCrypto: Send + Sync + Unpin + 'static { 30 | const KEY_LEN: usize; 31 | const IV_LEN: usize; 32 | const PRE_CHUNK_OVERHEAD: usize; 33 | const POST_CHUNK_OVERHEAD: usize; 34 | 35 | fn create_crypto(key: &[u8; Self::KEY_LEN], iv: &[u8; Self::IV_LEN]) -> Self; 36 | fn encrypt( 37 | &mut self, 38 | pre_overhead: &mut [u8; Self::PRE_CHUNK_OVERHEAD], 39 | data: &mut [u8], 40 | post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD], 41 | ); 42 | fn encrypt_all(&mut self, data: &mut [u8], post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD]); 43 | fn decrypt_size( 44 | &mut self, 45 | pre_overhead: &mut [u8; Self::PRE_CHUNK_OVERHEAD], 46 | ) -> Option; 47 | #[must_use] 48 | fn decrypt(&mut self, data: &mut [u8], post_overhead: &[u8; Self::POST_CHUNK_OVERHEAD]) 49 | -> bool; 50 | } 51 | 52 | pub type Aes128Gcm = RustCryptoAead, 16>; 53 | pub type Aes256Gcm = RustCryptoAead, 32>; 54 | pub type Chacha20IetfPoly1305 = RustCryptoAead; 55 | pub type XChacha20IetfPoly1305 = RustCryptoAead; 56 | 57 | pub type Rc4 = RustCryptoStream>, 0>; 58 | pub type Rc4Md5 = RustCryptoStream>, 16>; 59 | pub type Chacha20Ietf = RustCryptoStream, 12>; 60 | pub type Aes128Ctr = RustCryptoStream>, 16>; 61 | pub type Aes192Ctr = RustCryptoStream>, 16>; 62 | pub type Aes256Ctr = RustCryptoStream>, 16>; 63 | 64 | pub type Aes128Cfb = RustCryptoCfb128; 65 | pub type Aes192Cfb = RustCryptoCfb128; 66 | pub type Aes256Cfb = RustCryptoCfb128; 67 | pub type Camellia128Cfb = RustCryptoCfb128; 68 | pub type Camellia192Cfb = RustCryptoCfb128; 69 | pub type Camellia256Cfb = RustCryptoCfb128; 70 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[cfg(feature = "plugins")] 6 | mod crypto; 7 | #[cfg(feature = "plugins")] 8 | mod datagram; 9 | #[cfg(feature = "plugins")] 10 | pub mod factory; 11 | #[cfg(feature = "plugins")] 12 | mod stream; 13 | #[cfg(feature = "plugins")] 14 | pub(crate) mod util; 15 | 16 | #[rustfmt::skip] 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 18 | pub enum SupportedCipher { 19 | #[serde(rename = "none")] 20 | None, 21 | #[serde(rename = "rc4")] 22 | Rc4, 23 | #[serde(rename = "rc4-md5")] 24 | Rc4Md5, 25 | #[serde(rename = "aes-128-cfb")] 26 | Aes128Cfb, 27 | #[serde(rename = "aes-192-cfb")] 28 | Aes192Cfb, 29 | #[serde(rename = "aes-256-cfb")] 30 | Aes256Cfb, 31 | #[serde(rename = "aes-128-ctr")] 32 | Aes128Ctr, 33 | #[serde(rename = "aes-192-ctr")] 34 | Aes192Ctr, 35 | #[serde(rename = "aes-256-ctr")] 36 | Aes256Ctr, 37 | #[serde(rename = "camellia-128-cfb")] 38 | Camellia128Cfb, 39 | #[serde(rename = "camellia-192-cfb")] 40 | Camellia192Cfb, 41 | #[serde(rename = "camellia-256-cfb")] 42 | Camellia256Cfb, 43 | #[serde(rename = "aes-128-gcm")] 44 | Aes128Gcm, 45 | #[serde(rename = "aes-256-gcm")] 46 | Aes256Gcm, 47 | #[serde(rename = "chacha20-ietf")] 48 | Chacha20Ietf, 49 | #[serde(rename = "chacha20-ietf-poly1305")] 50 | Chacha20IetfPoly1305, 51 | #[serde(rename = "xchacha20-ietf-poly1305")] 52 | XChacha20IetfPoly1305, 53 | } 54 | 55 | impl Display for SupportedCipher { 56 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 | f.write_str(match self { 58 | SupportedCipher::None => "none", 59 | SupportedCipher::Rc4 => "rc4", 60 | SupportedCipher::Rc4Md5 => "rc4-md5", 61 | SupportedCipher::Aes128Cfb => "aes-128-cfb", 62 | SupportedCipher::Aes192Cfb => "aes-192-cfb", 63 | SupportedCipher::Aes256Cfb => "aes-256-cfb", 64 | SupportedCipher::Aes128Ctr => "aes-128-ctr", 65 | SupportedCipher::Aes192Ctr => "aes-192-ctr", 66 | SupportedCipher::Aes256Ctr => "aes-256-ctr", 67 | SupportedCipher::Camellia128Cfb => "camellia-128-cfb", 68 | SupportedCipher::Camellia192Cfb => "camellia-192-cfb", 69 | SupportedCipher::Camellia256Cfb => "camellia-256-cfb", 70 | SupportedCipher::Aes128Gcm => "aes-128-gcm", 71 | SupportedCipher::Aes256Gcm => "aes-256-gcm", 72 | SupportedCipher::Chacha20Ietf => "chacha20-ietf", 73 | SupportedCipher::Chacha20IetfPoly1305 => "chacha20-ietf-poly1305", 74 | SupportedCipher::XChacha20IetfPoly1305 => "xchacha20-ietf-poly1305", 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ytflow/src/plugin/obfs/simple_tls/template.rs: -------------------------------------------------------------------------------- 1 | use super::packet; 2 | 3 | pub const CLIENT_HELLO: packet::tls_client_hello = 4 | packet::tls_client_hello(packet::tls_client_hello_Inner { 5 | content_type: 0x16, 6 | version: 0x0301u16.to_be(), 7 | len: 0, 8 | 9 | handshake_type: 1, 10 | handshake_len_1: 0, 11 | handshake_len_2: 0, 12 | handshake_version: 0x0303u16.to_be(), 13 | 14 | random_unix_time: 0, 15 | random_bytes: [0; 28], 16 | 17 | session_id_len: 32, 18 | session_id: [0; 32], 19 | 20 | cipher_suites_len: 56u16.to_be(), 21 | cipher_suites: [ 22 | 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 23 | 0xc0, 0x2f, 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 24 | 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 25 | 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, 26 | ], 27 | 28 | comp_methods_len: 1, 29 | comp_methods: [0], 30 | 31 | ext_len: 0, 32 | }); 33 | 34 | pub const EXT_SERVER_NAME: packet::tls_ext_server_name = 35 | packet::tls_ext_server_name(packet::tls_ext_server_name_Inner { 36 | ext_type: 0, 37 | ext_len: 0, 38 | server_name_list_len: 0, 39 | server_name_type: 0, 40 | server_name_len: 0, 41 | }); 42 | 43 | pub const EXT_SESSION_TICKET: packet::tls_ext_session_ticket = 44 | packet::tls_ext_session_ticket(packet::tls_ext_session_ticket_Inner { 45 | session_ticket_type: 0x0023u16.to_be(), 46 | session_ticket_ext_len: 0, 47 | }); 48 | 49 | pub const EXT_OTHERS: packet::tls_ext_others = 50 | packet::tls_ext_others(packet::tls_ext_others_Inner { 51 | ec_point_formats_ext_type: 0x000Bu16.to_be(), 52 | ec_point_formats_ext_len: 4u16.to_be(), 53 | ec_point_formats_len: 3, 54 | ec_point_formats: [0x01, 0x00, 0x02], 55 | 56 | elliptic_curves_type: 0x000au16.to_be(), 57 | elliptic_curves_ext_len: 10u16.to_be(), 58 | elliptic_curves_len: 8u16.to_be(), 59 | elliptic_curves: [0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18], 60 | 61 | sig_algos_type: 0x000du16.to_be(), 62 | sig_algos_ext_len: 32u16.to_be(), 63 | sig_algos_len: 30u16.to_be(), 64 | sig_algos: [ 65 | 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 66 | 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 67 | 0x02, 0x03, 68 | ], 69 | 70 | encrypt_then_mac_type: 0x0016u16.to_be(), 71 | encrypt_then_mac_ext_len: 0, 72 | 73 | extended_master_secret_type: 0x0017u16.to_be(), 74 | extended_master_secret_ext_len: 0, 75 | }); 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YtFlowCore 2 | 3 | ![build tests ci](https://github.com/YtFlow/YtFlowCore/actions/workflows/build-tests.yml/badge.svg) 4 | 5 | A modern proxy framework, core of YtFlow. 6 | 7 | If you are looking for the UWP app powered by YtFlowCore, please head over to [YtFlowApp](https://github.com/YtFlow/YtFlowApp). 8 | 9 | ## Features 10 | 11 | - Fully customizable. Design your own network flow! 12 | - Multiple inbound types: VPN, SOCKS5. 13 | - Supports common proxy protocols: Shadowsocks, Trojan, VMess, SOCKS5 and HTTP. 14 | - Flexible outbound transports: simple-obfs, WebSocket, TLS etc. 15 | - DNS resolvers at your option: libc resolver, classic DNS over UDP, DNS over HTTPS. 16 | - Rule-based split routing modules: GeoIP, Surge domain set, Quantumult X filter. 17 | - Share link encoders/decoders: Shadowsocks, SIP002, Trojan, SOCKS5, HTTP and V2rayN 18 | - Subscription parsers: Base64 encoded share links, SIP008, Surge proxy list 19 | - Runs on Linux, macOS and Universal Windows Platform. 20 | 21 | ## Usage 22 | 23 | Use `ytflow-edit` to generate a new database file `conf.db` and a profile `my_profile`, and edit plugins accordingly. For newcomers, you may be interested in the `default-ss` and `default-redir` plugins. Read the [YtFlowCore Book](https://ytflow.github.io/ytflow-book/) to learn more about configuration. 24 | 25 | When the profile is ready, execute `ytflow-core --db-file conf.db my_profile` to launch YtFlowCore. 26 | 27 | ## Project Layout 28 | 29 | | Package | Description | Dependency | 30 | |---------|-------------|------------| 31 | | ytflow | Includes all components and plugins to run a YtFlowCore instance. | - | 32 | | ytflow-bin | Shell executables for the core `ytflow-core` and a TUI editor `ytflow-edit` that actually call into entrypoints exposed by `ytflow-bin-shared`. | ytflow-bin-shared | 33 | | ytflow-bin-shared | Contains the actual code for the binaries. Produces a single cdylib that reuses common dependencies to reduce final artifact size. | ytflow, ytflow-app-util | 34 | | ytflow-app-util | Provides utilities for app frontends to handle share links, subscriptions etc. Also exports FFI functions and generates a C header file. | ytflow | 35 | 36 | ## Build 37 | 38 | Steps to build `ytflow-core` and `ytflow-edit`: 39 | 1. Setup [rustup](https://rustup.rs/) and Visual C++ Build Tools on Windows or GCC toolchain on Linux. 40 | 2. Clone this repository. 41 | 3. Rename `.cargo/publish.config.toml` to `.cargo/config.toml`. 42 | 4. Run `cargo build -p ytflow-bin --release`. 43 | 5. If no error occurrs, you can find the binaries in `target/release/`. 44 | 45 | To build for YtFlowApp, please refer to the build steps on https://github.com/YtFlow/YtFlowApp/blob/main/README.md. 46 | 47 | ## Credits 48 | 49 | This project is inspired from: 50 | 51 | - [shadowsocks-rust](https://github.com/shadowsocks/shadowsocks-rust) 52 | - [Leaf](https://github.com/eycorsican/leaf) 53 | - [Project V](https://github.com/v2fly/v2ray-core) 54 | - ... and many more others! 55 | -------------------------------------------------------------------------------- /ytflow/src/data/profile.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDateTime; 2 | use rusqlite::{params, Error as SqError, OptionalExtension, Row}; 3 | use serde::Serialize; 4 | 5 | use super::*; 6 | 7 | pub type ProfileId = super::Id; 8 | 9 | #[derive(Debug, Clone, Serialize)] 10 | pub struct Profile { 11 | pub id: ProfileId, 12 | // TODO: uuid 13 | pub permanent_id: [u8; 16], 14 | pub name: String, 15 | pub locale: String, 16 | pub last_used_at: NaiveDateTime, 17 | pub created_at: NaiveDateTime, 18 | } 19 | 20 | fn map_from_row(row: &Row) -> Result { 21 | Ok(Profile { 22 | id: super::Id(row.get(0)?, Default::default()), 23 | permanent_id: { 24 | let row_ref = row.get_ref(1)?; 25 | *row_ref 26 | .as_blob() 27 | .ok() 28 | .and_then(|b| <&[u8; 16]>::try_from(b).ok()) 29 | .ok_or_else(|| { 30 | SqError::InvalidColumnType(1, String::from("permanent_id"), row_ref.data_type()) 31 | })? 32 | }, 33 | name: row.get(2)?, 34 | locale: row.get(3)?, 35 | last_used_at: row.get(4)?, 36 | created_at: row.get(5)?, 37 | }) 38 | } 39 | 40 | impl Profile { 41 | pub fn query_by_id(id: usize, conn: &super::Connection) -> DataResult> { 42 | Ok(conn 43 | .query_row_and_then( 44 | r"SELECT `id`, `permanent_id`, `name`, `locale`, `last_used_at`, `created_at` 45 | FROM `yt_profiles` WHERE `id` = ?", 46 | [&id], 47 | map_from_row, 48 | ) 49 | .optional()?) 50 | } 51 | pub fn query_all(conn: &super::Connection) -> DataResult> { 52 | let mut stmt = conn.prepare_cached("SELECT `id`, `permanent_id`, `name`, `locale`, `last_used_at`, `created_at` FROM `yt_profiles` ORDER BY `id` ASC")?; 53 | let ret = stmt 54 | .query_and_then([], map_from_row)? 55 | .filter_map(|r: Result| r.ok()) 56 | .collect(); 57 | Ok(ret) 58 | } 59 | pub fn create(name: String, locale: String, conn: &super::Connection) -> DataResult { 60 | conn.execute( 61 | "INSERT INTO `yt_profiles` (`name`, `locale`) VALUES (?, ?)", 62 | [name, locale], 63 | )?; 64 | Ok(conn.last_insert_rowid() as u32) 65 | } 66 | pub fn update( 67 | id: u32, 68 | name: String, 69 | locale: String, 70 | conn: &super::Connection, 71 | ) -> DataResult<()> { 72 | conn.execute( 73 | "UPDATE `yt_profiles` SET `name` = ?, `locale` = ? WHERE `id` = ?", 74 | params![name, locale, id], 75 | )?; 76 | Ok(()) 77 | } 78 | pub fn delete(id: u32, conn: &super::Connection) -> DataResult<()> { 79 | conn.execute("DELETE FROM `yt_profiles` WHERE `id` = ?", [id])?; 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/datagram.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::task::{Context, Poll}; 3 | 4 | use futures::ready; 5 | 6 | use super::crypto::*; 7 | use super::util::{parse_dest, write_dest}; 8 | use crate::flow::*; 9 | 10 | pub struct ShadowsocksDatagramSession 11 | where 12 | [(); C::KEY_LEN]:, 13 | { 14 | pub(super) key: Arc<[u8; C::KEY_LEN]>, 15 | pub(super) lower: Box, 16 | pub(super) crypto_phantom: std::marker::PhantomData, 17 | } 18 | 19 | impl DatagramSession for ShadowsocksDatagramSession 20 | where 21 | [(); C::KEY_LEN]:, 22 | [(); C::IV_LEN]:, 23 | [(); C::POST_CHUNK_OVERHEAD]:, 24 | { 25 | fn poll_recv_from(&mut self, cx: &mut Context) -> Poll> { 26 | let Some((_, mut buf)) = ready!(self.lower.poll_recv_from(cx)) else { 27 | return Poll::Ready(None); 28 | }; 29 | if buf.len() <= C::IV_LEN + C::POST_CHUNK_OVERHEAD { 30 | return Poll::Ready(None); 31 | } 32 | let (iv, rem) = buf.split_at_mut(C::IV_LEN); 33 | let (payload, post_overhead) = rem.split_at_mut(rem.len() - C::POST_CHUNK_OVERHEAD); 34 | let mut crypto = C::create_crypto(&self.key, (&*iv).try_into().unwrap()); 35 | if !crypto.decrypt(payload, (&*post_overhead).try_into().unwrap()) { 36 | return Poll::Ready(None); 37 | } 38 | let Some((dst, header_offset)) = parse_dest(payload) else { 39 | return Poll::Ready(None); 40 | }; 41 | buf.drain(..C::IV_LEN + header_offset); 42 | buf.truncate(buf.len() - C::POST_CHUNK_OVERHEAD); 43 | Poll::Ready(Some((dst, buf))) 44 | } 45 | 46 | fn poll_send_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { 47 | self.lower.poll_send_ready(cx) 48 | } 49 | 50 | fn send_to(&mut self, remote_peer: DestinationAddr, buf: Buffer) { 51 | let mut tx_handshake = Vec::with_capacity(259 + buf.len()); 52 | write_dest(&mut tx_handshake, &remote_peer); 53 | tx_handshake.extend_from_slice(&buf); 54 | 55 | // TODO: Skip zero-fill tx_handshake part 56 | let mut req_buf = vec![0; C::IV_LEN + tx_handshake.len() + C::POST_CHUNK_OVERHEAD]; 57 | let (iv, remaining) = req_buf.split_at_mut(C::IV_LEN); 58 | let (chunk, post_overhead) = remaining.split_at_mut(tx_handshake.len()); 59 | 60 | getrandom::getrandom(iv).unwrap(); 61 | chunk.copy_from_slice(&tx_handshake); 62 | 63 | let iv: &mut [u8; C::IV_LEN] = iv.try_into().unwrap(); 64 | let post_overhead: &mut [u8; C::POST_CHUNK_OVERHEAD] = post_overhead.try_into().unwrap(); 65 | let mut tx_crypto = C::create_crypto(&self.key, iv); 66 | tx_crypto.encrypt_all(chunk, post_overhead); 67 | self.lower.send_to(remote_peer, req_buf.into()); 68 | } 69 | 70 | fn poll_shutdown(&mut self, cx: &mut Context<'_>) -> Poll> { 71 | self.lower.poll_shutdown(cx) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ytflow/src/config/loader/profile.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "plugins")] 2 | use crate::resource::ResourceRegistry; 3 | #[cfg(feature = "plugins")] 4 | use std::collections::BTreeMap; 5 | 6 | use crate::config::factory::RequiredResource; 7 | use crate::config::*; 8 | 9 | #[cfg(feature = "plugins")] 10 | pub struct ProfileLoader<'f>(BTreeMap>); 11 | #[cfg(not(feature = "plugins"))] 12 | pub struct ProfileLoader<'f>(std::marker::PhantomData<&'f ()>); 13 | 14 | #[cfg(feature = "plugins")] 15 | pub struct ProfileLoadResult { 16 | pub plugin_set: set::PluginSet, 17 | pub errors: Vec, 18 | pub control_hub: crate::control::ControlHub, 19 | } 20 | 21 | impl<'f> ProfileLoader<'f> { 22 | pub fn parse_profile( 23 | entry_plugins: impl Iterator, 24 | all_plugins: &'f [Plugin], 25 | ) -> (Self, Vec, Vec) { 26 | let res = factory::parse_plugins_recursively( 27 | |resolver, _| { 28 | for entry_plugin in entry_plugins { 29 | resolver 30 | .plugin_to_visit 31 | .insert(&entry_plugin.name, Some(entry_plugin)); 32 | } 33 | }, 34 | all_plugins, 35 | ); 36 | #[cfg(feature = "plugins")] 37 | let res = (Self(res.factories), res.resources, res.errors); 38 | #[cfg(not(feature = "plugins"))] 39 | let res = (Self(Default::default()), res.resources, res.errors); 40 | res 41 | } 42 | #[cfg(feature = "plugins")] 43 | pub fn load_all( 44 | self, 45 | rt_handle: &tokio::runtime::Handle, 46 | resource_registry: Box, 47 | db: Option<&crate::data::Database>, 48 | ) -> ProfileLoadResult { 49 | use std::collections::HashMap; 50 | use std::mem::ManuallyDrop; 51 | 52 | let rt_handle_cloned = rt_handle.clone(); 53 | let _enter_guard = rt_handle.enter(); 54 | let mut partial_set = set::PartialPluginSet::new( 55 | self.0.into_iter().map(|(k, v)| (k, Some(v))).collect(), 56 | resource_registry, 57 | db, 58 | set::PluginSet { 59 | rt_handle: rt_handle_cloned, 60 | long_running_tasks: vec![], 61 | stream_handlers: ManuallyDrop::new(HashMap::new()), 62 | stream_outbounds: ManuallyDrop::new(HashMap::new()), 63 | datagram_handlers: ManuallyDrop::new(HashMap::new()), 64 | datagram_outbounds: ManuallyDrop::new(HashMap::new()), 65 | resolver: ManuallyDrop::new(HashMap::new()), 66 | tun: ManuallyDrop::new(HashMap::new()), 67 | }, 68 | ); 69 | partial_set.load_all(); 70 | ProfileLoadResult { 71 | plugin_set: partial_set.fully_constructed, 72 | errors: partial_set.errors, 73 | control_hub: partial_set.control_hub, 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ytflow/src/plugin/vmess/protocol/body/none.rs: -------------------------------------------------------------------------------- 1 | use super::super::header::{DATA_IV_LEN, DATA_KEY_LEN, VMESS_HEADER_ENC_NONE}; 2 | use super::{BodyCryptoFactory, RxCrypto, SizeCrypto, TxCrypto}; 3 | use crate::flow::{FlowError, FlowResult}; 4 | 5 | pub struct NoneClientCryptoTx { 6 | size_crypto: S, 7 | } 8 | 9 | pub struct NoneClientCryptoRx { 10 | size_crypto: S, 11 | expected_chunk_len: usize, 12 | } 13 | 14 | impl NoneClientCryptoTx { 15 | pub fn new(size_crypto: S) -> Self { 16 | Self { size_crypto } 17 | } 18 | } 19 | 20 | impl NoneClientCryptoRx { 21 | pub fn new(size_crypto: S) -> Self { 22 | Self { 23 | size_crypto, 24 | expected_chunk_len: 0, 25 | } 26 | } 27 | } 28 | 29 | impl TxCrypto for NoneClientCryptoTx 30 | where 31 | [(); S::LEN]:, 32 | { 33 | fn calculate_overhead(&mut self, _next_payload_len: usize) -> (usize, usize) { 34 | (S::LEN, 0) 35 | } 36 | 37 | fn seal(&mut self, pre_overhead: &mut [u8], payload: &mut [u8], _post_overhead: &mut [u8]) { 38 | pre_overhead.copy_from_slice(&self.size_crypto.encode_size(payload.len())); 39 | } 40 | } 41 | 42 | impl RxCrypto for NoneClientCryptoRx 43 | where 44 | [(); S::LEN]:, 45 | { 46 | fn expected_next_size_len(&mut self) -> usize { 47 | S::LEN 48 | } 49 | 50 | fn on_size(&mut self, size_bytes: &mut [u8]) -> FlowResult { 51 | let len = self 52 | .size_crypto 53 | .decode_size(&mut size_bytes[..].try_into().unwrap())?; 54 | if len == 0 { 55 | return Err(FlowError::Eof); 56 | } 57 | self.expected_chunk_len = len; 58 | Ok(len) 59 | } 60 | 61 | fn expected_next_chunk_len(&mut self) -> usize { 62 | self.expected_chunk_len 63 | } 64 | 65 | fn on_chunk<'c>(&mut self, chunk: &'c mut [u8]) -> FlowResult<&'c mut [u8]> { 66 | Ok(chunk) 67 | } 68 | } 69 | 70 | pub struct NoneCryptoFactory {} 71 | 72 | impl BodyCryptoFactory for NoneCryptoFactory { 73 | type Rx = NoneClientCryptoRx 74 | where 75 | [(); S::LEN]:,; 76 | type Tx = NoneClientCryptoTx 77 | where 78 | [(); S::LEN]:,; 79 | const HEADER_SEC_TYPE: u8 = VMESS_HEADER_ENC_NONE; 80 | 81 | fn new_tx( 82 | &self, 83 | _data_key: &[u8; DATA_KEY_LEN], 84 | _data_iv: &[u8; DATA_IV_LEN], 85 | size_crypto: S, 86 | ) -> Self::Tx 87 | where 88 | [(); S::LEN]:, 89 | { 90 | NoneClientCryptoTx::new(size_crypto) 91 | } 92 | fn new_rx( 93 | &self, 94 | _res_key: &[u8; DATA_KEY_LEN], 95 | _res_iv: &[u8; DATA_IV_LEN], 96 | size_crypto: S, 97 | ) -> Self::Rx 98 | where 99 | [(); S::LEN]:, 100 | { 101 | NoneClientCryptoRx::new(size_crypto) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ytflow/src/plugin/obfs/simple_tls/packet.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone)] 2 | #[repr(C, align(1))] 3 | pub struct tls_client_hello(pub tls_client_hello_Inner); 4 | #[derive(Copy, Clone)] 5 | #[repr(C, packed)] 6 | pub struct tls_client_hello_Inner { 7 | pub content_type: u8, 8 | pub version: u16, 9 | pub len: u16, 10 | pub handshake_type: u8, 11 | pub handshake_len_1: u8, 12 | pub handshake_len_2: u16, 13 | pub handshake_version: u16, 14 | pub random_unix_time: u32, 15 | pub random_bytes: [u8; 28], 16 | pub session_id_len: u8, 17 | pub session_id: [u8; 32], 18 | pub cipher_suites_len: u16, 19 | pub cipher_suites: [u8; 56], 20 | pub comp_methods_len: u8, 21 | pub comp_methods: [u8; 1], 22 | pub ext_len: u16, 23 | } 24 | #[allow(dead_code, non_upper_case_globals)] 25 | const tls_client_hello_PADDING: usize = 26 | ::std::mem::size_of::() - ::std::mem::size_of::(); 27 | 28 | #[derive(Copy, Clone)] 29 | #[repr(C, align(1))] 30 | pub struct tls_ext_server_name(pub tls_ext_server_name_Inner); 31 | #[derive(Copy, Clone)] 32 | #[repr(C, packed)] 33 | pub struct tls_ext_server_name_Inner { 34 | pub ext_type: u16, 35 | pub ext_len: u16, 36 | pub server_name_list_len: u16, 37 | pub server_name_type: u8, 38 | pub server_name_len: u16, 39 | } 40 | #[allow(dead_code, non_upper_case_globals)] 41 | const tls_ext_server_name_PADDING: usize = ::std::mem::size_of::() 42 | - ::std::mem::size_of::(); 43 | 44 | #[derive(Copy, Clone)] 45 | #[repr(C, align(1))] 46 | pub struct tls_ext_session_ticket(pub tls_ext_session_ticket_Inner); 47 | #[derive(Copy, Clone)] 48 | #[repr(C, packed)] 49 | pub struct tls_ext_session_ticket_Inner { 50 | pub session_ticket_type: u16, 51 | pub session_ticket_ext_len: u16, 52 | } 53 | #[allow(dead_code, non_upper_case_globals)] 54 | const tls_ext_session_ticket_PADDING: usize = ::std::mem::size_of::() 55 | - ::std::mem::size_of::(); 56 | 57 | #[derive(Copy, Clone)] 58 | #[repr(C, align(1))] 59 | pub struct tls_ext_others(pub tls_ext_others_Inner); 60 | #[derive(Copy, Clone)] 61 | #[repr(C, packed)] 62 | pub struct tls_ext_others_Inner { 63 | pub ec_point_formats_ext_type: u16, 64 | pub ec_point_formats_ext_len: u16, 65 | pub ec_point_formats_len: u8, 66 | pub ec_point_formats: [u8; 3], 67 | pub elliptic_curves_type: u16, 68 | pub elliptic_curves_ext_len: u16, 69 | pub elliptic_curves_len: u16, 70 | pub elliptic_curves: [u8; 8], 71 | pub sig_algos_type: u16, 72 | pub sig_algos_ext_len: u16, 73 | pub sig_algos_len: u16, 74 | pub sig_algos: [u8; 30], 75 | pub encrypt_then_mac_type: u16, 76 | pub encrypt_then_mac_ext_len: u16, 77 | pub extended_master_secret_type: u16, 78 | pub extended_master_secret_ext_len: u16, 79 | } 80 | #[allow(dead_code, non_upper_case_globals)] 81 | const tls_ext_others_PADDING: usize = 82 | ::std::mem::size_of::() - ::std::mem::size_of::(); 83 | -------------------------------------------------------------------------------- /ytflow/src/plugin/dyn_outbound/dyn_outbound.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | use arc_swap::ArcSwap; 4 | use async_trait::async_trait; 5 | use itertools::Itertools; 6 | 7 | use crate::data::{self, DataResult, Database, PluginCache}; 8 | use crate::flow::*; 9 | 10 | pub struct FixedOutbound { 11 | pub name: String, 12 | pub tcp_next: Weak, 13 | pub udp_next: Weak, 14 | } 15 | pub struct DynOutbound { 16 | pub(super) db: Database, 17 | pub(super) plugin_cache: PluginCache, 18 | pub(super) fixed_outbounds: Vec, 19 | pub(super) proxy_list: ArcSwap<( 20 | Vec<(data::Proxy, data::ProxyGroupId)>, 21 | Vec, 22 | )>, 23 | pub(super) current: ArcSwap>, 24 | pub(super) tcp_next: Weak, 25 | pub(super) udp_next: Weak, 26 | } 27 | 28 | impl DynOutbound { 29 | pub fn new( 30 | db: Database, 31 | plugin_cache: PluginCache, 32 | fixed_outbounds: Vec, 33 | tcp_next: Weak, 34 | udp_next: Weak, 35 | ) -> Self { 36 | Self { 37 | db, 38 | plugin_cache, 39 | fixed_outbounds, 40 | proxy_list: ArcSwap::new(Default::default()), 41 | current: ArcSwap::new(Arc::new(None)), 42 | tcp_next, 43 | udp_next, 44 | } 45 | } 46 | 47 | pub fn load_proxies(&self) -> DataResult<()> { 48 | let conn = self.db.connect()?; 49 | let groups = data::ProxyGroup::query_all(&conn)?; 50 | let all_proxies = groups 51 | .iter() 52 | .map(|g| { 53 | data::Proxy::query_all_by_group(g.id, &conn) 54 | .map(|ps| ps.into_iter().map(|p| (p, g.id)).collect_vec()) 55 | }) 56 | .collect::>>()? 57 | .into_iter() 58 | .flatten() 59 | .collect_vec(); 60 | self.proxy_list.store(Arc::new((all_proxies, groups))); 61 | Ok(()) 62 | } 63 | } 64 | 65 | #[async_trait] 66 | impl StreamOutboundFactory for DynOutbound { 67 | async fn create_outbound( 68 | &self, 69 | context: &mut FlowContext, 70 | initial_data: &'_ [u8], 71 | ) -> FlowResult<(Box, Buffer)> { 72 | let next = (**self.current.load()) 73 | .as_ref() 74 | .ok_or(FlowError::NoOutbound)? 75 | .tcp 76 | .clone(); 77 | next.create_outbound(context, initial_data).await 78 | } 79 | } 80 | 81 | #[async_trait] 82 | impl DatagramSessionFactory for DynOutbound { 83 | async fn bind(&self, context: Box) -> FlowResult> { 84 | let next = (**self.current.load()) 85 | .as_ref() 86 | .ok_or(FlowError::NoOutbound)? 87 | .udp 88 | .clone(); 89 | next.bind(context).await 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ytflow-app-util/src/subscription/userinfo.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, NaiveDateTime}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] 5 | pub struct SubscriptionUserInfo { 6 | pub upload_bytes_used: Option, 7 | pub download_bytes_used: Option, 8 | pub bytes_total: Option, 9 | pub expires_at: Option, 10 | } 11 | 12 | impl SubscriptionUserInfo { 13 | pub fn decode_header(header: &str) -> Self { 14 | let mut ret = Self::default(); 15 | for kv in header.split(';') { 16 | let mut kv = kv.split('='); 17 | let key = kv.next().expect("first split must exist").trim(); 18 | let Some(mut value) = kv.next() else { 19 | continue; 20 | }; 21 | value = value.trim(); 22 | 23 | match key { 24 | "upload" => { 25 | ret.upload_bytes_used = value.parse().ok(); 26 | } 27 | "download" => { 28 | ret.download_bytes_used = value.parse().ok(); 29 | } 30 | "total" => { 31 | ret.bytes_total = value.parse().ok(); 32 | } 33 | "expire" => { 34 | ret.expires_at = value 35 | .parse() 36 | .ok() 37 | .and_then(|s| DateTime::from_timestamp(s, 0)) 38 | .map(|dt| dt.naive_utc()); 39 | } 40 | _ => { 41 | continue; 42 | } 43 | } 44 | } 45 | ret 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | 53 | #[test] 54 | fn test_decode_header() { 55 | let header = 56 | "upload=455727941; download=6174315083; total=1073741824000; expire=1671815872;"; 57 | let info = SubscriptionUserInfo::decode_header(header); 58 | assert_eq!( 59 | info, 60 | SubscriptionUserInfo { 61 | upload_bytes_used: Some(455727941), 62 | download_bytes_used: Some(6174315083), 63 | bytes_total: Some(1073741824000), 64 | expires_at: DateTime::from_timestamp(1671815872, 0).map(|t| t.naive_local()), 65 | } 66 | ); 67 | } 68 | #[test] 69 | fn test_decode_header_empty() { 70 | let info = SubscriptionUserInfo::decode_header(""); 71 | assert_eq!(info, SubscriptionUserInfo::default()); 72 | } 73 | #[test] 74 | fn test_decode_header_no_expire() { 75 | let header = "upload=455727941; download=6174315083; total=1073741824000;"; 76 | let info = SubscriptionUserInfo::decode_header(header); 77 | assert_eq!( 78 | info, 79 | SubscriptionUserInfo { 80 | upload_bytes_used: Some(455727941), 81 | download_bytes_used: Some(6174315083), 82 | bytes_total: Some(1073741824000), 83 | expires_at: None, 84 | } 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/edit/views/new_profile.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; 3 | use tui::{ 4 | layout::{Constraint, Direction, Layout}, 5 | style::Style, 6 | widgets::{Block, Borders, List, ListItem, ListState}, 7 | }; 8 | 9 | use super::{bg_rev, NavChoice, BG}; 10 | use crate::edit; 11 | use edit::gen::profiles as gen_profiles; 12 | 13 | pub fn run_new_profile_view(ctx: &mut edit::AppContext) -> Result { 14 | let mut template_state = ListState::default(); 15 | template_state.select(Some(0)); 16 | loop { 17 | let size = ctx.term.size()?; 18 | let main_chunk = Layout::default() 19 | .direction(Direction::Vertical) 20 | .constraints([Constraint::Min(0)].as_ref()) 21 | .split(size)[0]; 22 | let template_list = List::new([ 23 | ListItem::new("SOCKS5 (9080) inbound + Shadowsocks outbound"), 24 | // ListItem::new("SOCKS5 (9080) inbound + Trojan (via TLS) outbound"), 25 | // ListItem::new("SOCKS5 (9080) inbound + HTTP (CONNECT) outbound"), 26 | ]) 27 | .block( 28 | Block::default() 29 | .title("Choose a Template") 30 | .borders(Borders::ALL), 31 | ) 32 | .highlight_style(Style::default().bg(bg_rev(true)).fg(BG)); 33 | ctx.term.draw(|f| { 34 | f.render_stateful_widget(template_list, main_chunk, &mut template_state); 35 | })?; 36 | if let Event::Key(KeyEvent { 37 | code, 38 | kind: KeyEventKind::Press, 39 | .. 40 | }) = crossterm::event::read().unwrap() 41 | { 42 | match code { 43 | KeyCode::Char('q') | KeyCode::Esc => return Ok(NavChoice::Back), 44 | KeyCode::Down => { 45 | template_state.select(template_state.selected().map(|i| (i + 1) % 1)); 46 | } 47 | KeyCode::Up => { 48 | template_state.select(template_state.selected().map(|i| { 49 | if i == 0 { 50 | 0 51 | } else { 52 | i - 1 53 | } 54 | })); 55 | } 56 | KeyCode::Enter => { 57 | let profile_id = gen_profiles::create_profile(&ctx.conn) 58 | .context("Could not create profile")?; 59 | let selected = template_state.selected().unwrap_or_default(); 60 | match selected { 61 | 0 => { 62 | let plugins = gen_profiles::generate_shadowsocks_plugins(); 63 | gen_profiles::save_plugins(plugins, profile_id, &ctx.conn) 64 | .context("Failed to save plugins")?; 65 | } 66 | _ => {} 67 | } 68 | return Ok(NavChoice::Back); 69 | } 70 | _ => {} 71 | } 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/socket.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::config::factory::*; 6 | use crate::config::*; 7 | 8 | fn default_bind_addr_v4() -> Option> { 9 | Some(HumanRepr { 10 | inner: SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0), 11 | }) 12 | } 13 | 14 | fn default_bind_addr_v6() -> Option> { 15 | Some(HumanRepr { 16 | inner: SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0), 17 | }) 18 | } 19 | 20 | #[cfg_attr(not(feature = "plugins"), allow(dead_code))] 21 | #[derive(Clone, Deserialize)] 22 | pub struct SocketFactory<'a> { 23 | resolver: &'a str, 24 | #[serde(default = "default_bind_addr_v4")] 25 | bind_addr_v4: Option>, 26 | #[serde(default = "default_bind_addr_v6")] 27 | bind_addr_v6: Option>, 28 | } 29 | 30 | impl<'de> SocketFactory<'de> { 31 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 32 | let Plugin { name, param, .. } = plugin; 33 | let config: Self = parse_param(name, param)?; 34 | Ok(ParsedPlugin { 35 | factory: config.clone(), 36 | requires: vec![Descriptor { 37 | descriptor: config.resolver, 38 | r#type: AccessPointType::RESOLVER, 39 | }], 40 | provides: vec![Descriptor { 41 | descriptor: name.clone(), 42 | r#type: AccessPointType::STREAM_OUTBOUND_FACTORY 43 | | AccessPointType::DATAGRAM_SESSION_FACTORY, 44 | }], 45 | resources: vec![], 46 | }) 47 | } 48 | } 49 | 50 | impl<'de> Factory for SocketFactory<'de> { 51 | #[cfg(feature = "plugins")] 52 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 53 | use crate::plugin::null::Null; 54 | use crate::plugin::socket; 55 | 56 | let factory = Arc::new_cyclic(|weak| { 57 | set.stream_outbounds 58 | .insert(plugin_name.clone(), weak.clone() as _); 59 | set.datagram_outbounds 60 | .insert(plugin_name.clone(), weak.clone() as _); 61 | let resolver = match set.get_or_create_resolver(plugin_name.clone(), self.resolver) { 62 | Ok(resolver) => resolver, 63 | Err(e) => { 64 | set.errors.push(e); 65 | Arc::downgrade(&(Arc::new(Null) as _)) 66 | } 67 | }; 68 | socket::SocketOutboundFactory { 69 | resolver, 70 | bind_addr_v4: self.bind_addr_v4.clone().map(|h| h.inner), 71 | bind_addr_v6: self.bind_addr_v6.clone().map(|h| h.inner), 72 | } 73 | }); 74 | set.fully_constructed 75 | .stream_outbounds 76 | .insert(plugin_name.clone() + ".tcp", factory.clone()); 77 | set.fully_constructed 78 | .datagram_outbounds 79 | .insert(plugin_name + ".udp", factory); 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ytflow/src/plugin/socket/udp_listener.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::io; 3 | use std::net::{SocketAddr, ToSocketAddrs}; 4 | use std::sync::{Arc, Weak}; 5 | use std::task::{ready, Context, Poll}; 6 | 7 | use flume::{bounded, SendError}; 8 | 9 | use crate::flow::*; 10 | 11 | pub fn listen_udp( 12 | next: Weak, 13 | addr: impl ToSocketAddrs + Send + 'static, 14 | ) -> io::Result> { 15 | let mut session_map = BTreeMap::new(); 16 | let listener = std::net::UdpSocket::bind(addr)?; 17 | listener.set_nonblocking(true)?; 18 | Ok(tokio::spawn(async move { 19 | let listener = Arc::new( 20 | tokio::net::UdpSocket::from_std(listener) 21 | .expect("Calling listen_udp when runtime is not set"), 22 | ); 23 | let listen_addr: DestinationAddr = match listener.local_addr() { 24 | Ok(addr) => addr, 25 | // TODO: log error 26 | Err(_) => return, 27 | } 28 | .into(); 29 | let mut buf = [0u8; 4096]; 30 | loop { 31 | let (size, from) = match listener.recv_from(&mut buf).await { 32 | Ok(r) => r, 33 | Err(_) => { 34 | // TODO: log error 35 | break; 36 | } 37 | }; 38 | let tx = session_map.entry(from).or_insert_with(|| { 39 | let (tx, rx) = bounded(64); 40 | if let Some(next) = next.upgrade() { 41 | next.on_session( 42 | Box::new(MultiplexedDatagramSessionAdapter::new( 43 | InboundUdpSession { 44 | socket: listener.clone(), 45 | tx_buf: None, 46 | }, 47 | rx.into_stream(), 48 | 120, 49 | )), 50 | Box::new(FlowContext::new_af_sensitive(from, listen_addr.clone())), 51 | ); 52 | } 53 | tx 54 | }); 55 | if let Err(SendError(_)) = tx 56 | .send_async((listen_addr.clone(), buf[..size].to_vec())) 57 | .await 58 | { 59 | session_map.remove(&from); 60 | } 61 | } 62 | })) 63 | } 64 | 65 | struct InboundUdpSession { 66 | socket: Arc, 67 | tx_buf: Option<(SocketAddr, Buffer)>, 68 | } 69 | 70 | impl MultiplexedDatagramSession for InboundUdpSession { 71 | fn on_close(&mut self) {} 72 | 73 | fn poll_send_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { 74 | let _ = ready!(self.socket.poll_send_ready(cx)).ok(); 75 | if let Some((addr, buf)) = &mut self.tx_buf { 76 | let _ = ready!(self.socket.poll_send_to(cx, buf, *addr)); 77 | self.tx_buf = None; 78 | } 79 | Poll::Ready(()) 80 | } 81 | 82 | fn send_to(&mut self, src: DestinationAddr, buf: Buffer) { 83 | let HostName::Ip(ip) = &src.host else { 84 | return; 85 | }; 86 | self.tx_buf = Some((SocketAddr::new(*ip, src.port), buf)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/util.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use crate::flow::{DestinationAddr, HostName}; 4 | 5 | pub fn write_dest(w: &mut Vec, remote_peer: &DestinationAddr) { 6 | match &remote_peer.host { 7 | HostName::DomainName(domain) => { 8 | w.push(0x03); 9 | let domain = domain.trim_end_matches('.').as_bytes(); 10 | w.push(domain.len() as u8); 11 | w.extend_from_slice(domain); 12 | } 13 | HostName::Ip(IpAddr::V4(ipv4)) => { 14 | w.push(0x01); 15 | w.extend_from_slice(&ipv4.octets()[..]); 16 | } 17 | HostName::Ip(IpAddr::V6(ipv6)) => { 18 | w.push(0x04); 19 | w.extend_from_slice(&ipv6.octets()); 20 | } 21 | } 22 | w.extend_from_slice(&remote_peer.port.to_be_bytes()); 23 | } 24 | 25 | pub fn parse_dest(w: &[u8]) -> Option<(DestinationAddr, usize)> { 26 | if w.len() < 2 { 27 | return None; 28 | } 29 | let dest_type = w[0]; 30 | let (dest_addr, port_offset) = match dest_type { 31 | 0x01 if w.len() >= 7 => { 32 | let mut ipv4 = [0u8; 4]; 33 | ipv4.copy_from_slice(&w[1..5]); 34 | (HostName::Ip(IpAddr::V4(ipv4.into())), 5) 35 | } 36 | 0x04 if w.len() >= 19 => { 37 | let mut ipv6 = [0u8; 16]; 38 | ipv6.copy_from_slice(&w[1..17]); 39 | (HostName::Ip(IpAddr::V6(ipv6.into())), 17) 40 | } 41 | 0x03 if w.len() >= w[1] as usize + 4 => { 42 | let domain_end = w[1] as usize + 2; 43 | let domain = String::from_utf8_lossy(&w[2..domain_end]).to_string(); 44 | (HostName::from_domain_name(domain).ok()?, domain_end) 45 | } 46 | _ => return None, 47 | }; 48 | let port = u16::from_be_bytes([w[port_offset], w[port_offset + 1]]); 49 | Some(( 50 | DestinationAddr { 51 | host: dest_addr, 52 | port, 53 | }, 54 | port_offset + 2, 55 | )) 56 | } 57 | 58 | /// https://github.com/shadowsocks/shadowsocks-crypto/blob/bf3c3f0cdf3ebce6a19ce15a96248ccd94a82848/src/v1/cipher.rs#LL33-L58C2 59 | /// Key derivation of OpenSSL's [EVP_BytesToKey](https://wiki.openssl.org/index.php/Manual:EVP_BytesToKey(3)) 60 | pub fn openssl_bytes_to_key(password: &[u8]) -> [u8; K] { 61 | use md5::{Digest, Md5}; 62 | 63 | let mut key = [0u8; K]; 64 | 65 | let mut last_digest = None; 66 | 67 | let mut offset = 0usize; 68 | while offset < K { 69 | let mut m = Md5::new(); 70 | if let Some(digest) = last_digest { 71 | m.update(digest); 72 | } 73 | 74 | m.update(password); 75 | 76 | let digest = m.finalize(); 77 | 78 | let amt = std::cmp::min(K - offset, digest.len()); 79 | key[offset..offset + amt].copy_from_slice(&digest[..amt]); 80 | 81 | offset += amt; 82 | last_digest = Some(digest); 83 | } 84 | key 85 | } 86 | 87 | pub fn increase_num_buf(buf: &mut [u8]) { 88 | let mut c = buf[0] as u16 + 1; 89 | buf[0] = c as u8; 90 | c >>= 8; 91 | let mut n = 1; 92 | while n < buf.len() { 93 | c += buf[n] as u16; 94 | buf[n] = c as u8; 95 | c >>= 8; 96 | n += 1; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ytflow/src/flow/multiplexed_datagram.rs: -------------------------------------------------------------------------------- 1 | use std::mem::ManuallyDrop; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use flume::r#async::RecvStream; 6 | use futures::{ready, Stream}; 7 | use tokio::time::{interval, Interval}; 8 | 9 | use super::{Buffer, DatagramSession, DestinationAddr, FlowResult}; 10 | 11 | pub trait MultiplexedDatagramSession: Send + Sync { 12 | fn on_close(&mut self); 13 | fn poll_send_ready(&mut self, cx: &mut Context<'_>) -> Poll<()>; 14 | fn send_to(&mut self, src: DestinationAddr, buf: Buffer); 15 | } 16 | 17 | type MultiplexedDatagramRx = RecvStream<'static, (DestinationAddr, Buffer)>; 18 | 19 | pub struct MultiplexedDatagramSessionAdapter { 20 | inner: S, 21 | rx: Option, 22 | has_io_within_tick: bool, 23 | timer: ManuallyDrop, 24 | } 25 | 26 | impl Drop for MultiplexedDatagramSessionAdapter { 27 | fn drop(&mut self) { 28 | self.close(); 29 | } 30 | } 31 | 32 | impl MultiplexedDatagramSessionAdapter { 33 | pub fn new(inner: S, rx: MultiplexedDatagramRx, timeout: u64) -> Self { 34 | Self { 35 | inner, 36 | rx: Some(rx), 37 | has_io_within_tick: true, 38 | timer: ManuallyDrop::new(interval(tokio::time::Duration::from_secs(timeout))), 39 | } 40 | } 41 | 42 | fn close(&mut self) { 43 | if self.rx.take().is_some() { 44 | // Safety: rx is taken out exactly once. 45 | unsafe { drop(ManuallyDrop::take(&mut self.timer)) }; 46 | self.inner.on_close(); 47 | } 48 | } 49 | } 50 | 51 | impl DatagramSession for MultiplexedDatagramSessionAdapter { 52 | fn poll_send_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { 53 | self.inner.poll_send_ready(cx) 54 | } 55 | fn send_to(&mut self, src: DestinationAddr, buf: Buffer) { 56 | self.has_io_within_tick = true; 57 | if self.rx.is_none() { 58 | // Already closed 59 | return; 60 | } 61 | self.inner.send_to(src, buf); 62 | } 63 | fn poll_recv_from(&mut self, cx: &mut Context) -> Poll> { 64 | let rx = Pin::new(match self.rx.as_mut() { 65 | Some(rx) => rx, 66 | None => return Poll::Ready(None), 67 | }); 68 | match rx.poll_next(cx) { 69 | Poll::Ready(None) => Poll::Ready(None), 70 | Poll::Ready(Some((dst, buf))) => { 71 | self.has_io_within_tick = true; 72 | Poll::Ready(Some((dst, buf))) 73 | } 74 | Poll::Pending => { 75 | ready!(self.timer.poll_tick(cx)); 76 | // Time is up at this point 77 | if std::mem::replace(&mut self.has_io_within_tick, false) { 78 | Poll::Pending 79 | } else { 80 | self.close(); 81 | Poll::Ready(None) 82 | } 83 | } 84 | } 85 | } 86 | fn poll_shutdown(&mut self, cx: &mut Context<'_>) -> Poll> { 87 | self.poll_send_ready(cx).map(|()| Ok(())) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ytflow/src/plugin/shadowsocks/crypto/aead.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub struct RustCryptoAead { 4 | inner: Inner, 5 | nonce: GenericArray, 6 | } 7 | 8 | impl ShadowCrypto for RustCryptoAead 9 | where 10 | Inner: AeadCore + KeyInit + AeadInPlace + Send + Sync + Unpin + 'static, 11 | GenericArray: Send + Sync + Unpin + 'static, 12 | { 13 | const KEY_LEN: usize = Inner::KeySize::USIZE; 14 | const IV_LEN: usize = SALT_LEN; 15 | const PRE_CHUNK_OVERHEAD: usize = 2 + 16; 16 | const POST_CHUNK_OVERHEAD: usize = 16; 17 | 18 | fn create_crypto(key: &[u8; Self::KEY_LEN], iv: &[u8; Self::IV_LEN]) -> Self { 19 | let mut subkey = [0u8; Self::KEY_LEN]; 20 | Hkdf::::new(Some(iv), key) 21 | .expand(b"ss-subkey", &mut subkey) 22 | .unwrap(); 23 | 24 | Self { 25 | inner: Inner::new_from_slice(&subkey).unwrap(), 26 | nonce: Default::default(), 27 | } 28 | } 29 | 30 | fn encrypt( 31 | &mut self, 32 | pre_overhead: &mut [u8; Self::PRE_CHUNK_OVERHEAD], 33 | data: &mut [u8], 34 | post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD], 35 | ) { 36 | pre_overhead[0..2].copy_from_slice(&(data.len() as u16).to_be_bytes()); 37 | let tag = self 38 | .inner 39 | .encrypt_in_place_detached(&self.nonce, &[], &mut pre_overhead[0..2]) 40 | .unwrap(); 41 | pre_overhead[2..].copy_from_slice(&tag); 42 | increase_num_buf(&mut self.nonce); 43 | let tag = self 44 | .inner 45 | .encrypt_in_place_detached(&self.nonce, &[], data) 46 | .unwrap(); 47 | post_overhead.copy_from_slice(&tag); 48 | increase_num_buf(&mut self.nonce); 49 | } 50 | fn encrypt_all( 51 | &mut self, 52 | data: &mut [u8], 53 | post_overhead: &mut [u8; Self::POST_CHUNK_OVERHEAD], 54 | ) { 55 | let tag = self 56 | .inner 57 | .encrypt_in_place_detached(&self.nonce, &[], data) 58 | .unwrap(); 59 | post_overhead.copy_from_slice(&tag); 60 | increase_num_buf(&mut self.nonce); 61 | } 62 | 63 | fn decrypt_size( 64 | &mut self, 65 | pre_overhead: &mut [u8; Self::PRE_CHUNK_OVERHEAD], 66 | ) -> Option { 67 | let (size_buf, size_tag) = pre_overhead.split_at_mut(2); 68 | if self 69 | .inner 70 | .decrypt_in_place_detached(&self.nonce, &[], size_buf, (&*size_tag).into()) 71 | .is_err() 72 | { 73 | return None; 74 | } 75 | increase_num_buf(&mut self.nonce); 76 | let size = u16::from_be_bytes(size_buf.try_into().unwrap()) & 0x3fff; 77 | NonZeroUsize::new(size as usize) 78 | } 79 | 80 | fn decrypt( 81 | &mut self, 82 | data: &mut [u8], 83 | post_overhead: &[u8; Self::POST_CHUNK_OVERHEAD], 84 | ) -> bool { 85 | let res = self 86 | .inner 87 | .decrypt_in_place_detached(&self.nonce, &[], data, post_overhead.into()) 88 | .is_ok(); 89 | increase_num_buf(&mut self.nonce); 90 | res 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ytflow/src/plugin/fallback.rs: -------------------------------------------------------------------------------- 1 | use std::mem::ManuallyDrop; 2 | 3 | use std::sync::Weak; 4 | use std::task::{Context, Poll}; 5 | 6 | use crate::flow::*; 7 | 8 | struct FallbackStream) + Unpin> { 9 | tx_closed: bool, 10 | lower: ManuallyDrop>, 11 | on_fallback: ManuallyDrop, 12 | } 13 | 14 | impl) + Send + Sync + Unpin> Stream for FallbackStream { 15 | fn poll_request_size(&mut self, cx: &mut Context<'_>) -> Poll> { 16 | self.lower.poll_request_size(cx) 17 | } 18 | 19 | fn commit_rx_buffer(&mut self, buffer: Buffer) -> Result<(), (Buffer, FlowError)> { 20 | self.lower.commit_rx_buffer(buffer) 21 | } 22 | 23 | fn poll_rx_buffer( 24 | &mut self, 25 | cx: &mut Context<'_>, 26 | ) -> Poll> { 27 | self.lower.poll_rx_buffer(cx) 28 | } 29 | 30 | fn poll_tx_buffer( 31 | &mut self, 32 | cx: &mut Context<'_>, 33 | size: std::num::NonZeroUsize, 34 | ) -> Poll> { 35 | self.lower.poll_tx_buffer(cx, size) 36 | } 37 | 38 | fn commit_tx_buffer(&mut self, buffer: Buffer) -> FlowResult<()> { 39 | self.lower.commit_tx_buffer(buffer) 40 | } 41 | 42 | fn poll_flush_tx(&mut self, cx: &mut Context<'_>) -> Poll> { 43 | self.lower.poll_flush_tx(cx) 44 | } 45 | 46 | fn poll_close_tx(&mut self, cx: &mut Context<'_>) -> Poll> { 47 | self.tx_closed = true; 48 | self.lower.as_mut().poll_close_tx(cx) 49 | } 50 | } 51 | 52 | impl) + Unpin> Drop for FallbackStream { 53 | fn drop(&mut self) { 54 | unsafe { 55 | let lower = ManuallyDrop::take(&mut self.lower); 56 | let on_fallback = ManuallyDrop::take(&mut self.on_fallback); 57 | if !self.tx_closed { 58 | (on_fallback)(lower); 59 | } 60 | } 61 | } 62 | } 63 | 64 | pub struct FallbackHandler { 65 | next: Weak, 66 | fallback: Weak, 67 | } 68 | 69 | impl StreamHandler for FallbackHandler { 70 | fn on_stream(&self, lower: Box, initial_data: Buffer, context: Box) { 71 | let fallback = self.fallback.clone(); 72 | let context_clone = Box::new(FlowContext { 73 | local_peer: context.local_peer, 74 | remote_peer: context.remote_peer.clone(), 75 | af_sensitive: context.af_sensitive, 76 | application_layer_protocol: context.application_layer_protocol.clone(), 77 | }); 78 | let next = match self.next.upgrade() { 79 | Some(n) => n, 80 | None => return, 81 | }; 82 | next.on_stream( 83 | Box::new(FallbackStream { 84 | tx_closed: false, 85 | lower: ManuallyDrop::new(lower), 86 | on_fallback: ManuallyDrop::new(move |lower| { 87 | if let Some(fallback) = fallback.upgrade() { 88 | fallback.on_stream(lower, Buffer::new(), context) 89 | } 90 | }), 91 | }), 92 | initial_data, 93 | context_clone, 94 | ); 95 | } 96 | } 97 | 98 | // TODO: FallbackOutputHandler 99 | -------------------------------------------------------------------------------- /ytflow-bin-shared/src/edit.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::PathBuf; 3 | 4 | use anyhow::{Context, Result}; 5 | use clap::{arg, value_parser, ArgMatches}; 6 | use crossterm::{ 7 | event::{DisableMouseCapture, EnableMouseCapture}, 8 | execute, 9 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 10 | }; 11 | use tui::{backend::CrosstermBackend, Terminal}; 12 | 13 | mod gen; 14 | mod views; 15 | use ytflow::data::{Connection, Database}; 16 | 17 | pub fn main() -> Result<()> { 18 | let args = get_args(); 19 | let conn = get_db_conn(&args)?; 20 | run_tui(conn)?; 21 | Ok(()) 22 | } 23 | 24 | fn get_args() -> ArgMatches { 25 | clap::command!() 26 | .arg(arg!( "Path to the database file").value_parser(value_parser!(PathBuf))) 27 | .get_matches() 28 | } 29 | 30 | fn get_db_conn(args: &ArgMatches) -> Result { 31 | let db_path: &PathBuf = args.get_one("PATH").expect("Cannot get database path"); 32 | let db = Database::open(db_path).context("Could not prepare database")?; 33 | let conn = db.connect().context("Could not connect to database")?; 34 | Ok(conn) 35 | } 36 | 37 | fn run_tui(conn: Connection) -> Result<()> { 38 | let mut stdout = io::stdout(); 39 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); 40 | let backend = CrosstermBackend::new(stdout); 41 | let mut terminal = Terminal::new(backend).context("Could not create terminal")?; 42 | terminal.clear().unwrap(); 43 | enable_raw_mode().unwrap(); 44 | terminal.hide_cursor().unwrap(); 45 | let mut ctx = AppContext { 46 | term: terminal, 47 | conn, 48 | }; 49 | let res = run_main_loop(&mut ctx); 50 | let mut terminal = ctx.term; 51 | execute!( 52 | terminal.backend_mut(), 53 | LeaveAlternateScreen, 54 | DisableMouseCapture 55 | ) 56 | .unwrap(); 57 | terminal.show_cursor().unwrap(); 58 | disable_raw_mode().unwrap(); 59 | res 60 | } 61 | 62 | pub struct AppContext { 63 | term: Terminal>, 64 | conn: Connection, 65 | } 66 | 67 | fn run_main_loop(ctx: &mut AppContext) -> Result<()> { 68 | use views::NavChoice; 69 | 70 | let mut nav_choices = vec![NavChoice::MainView]; 71 | loop { 72 | let next_nav_choice = match nav_choices.last_mut() { 73 | Some(NavChoice::MainView) => views::run_main_view(ctx)?, 74 | Some(NavChoice::NewProfileView) => views::run_new_profile_view(ctx)?, 75 | Some(NavChoice::ProfileView(id)) => views::run_profile_view(ctx, *id)?, 76 | Some(NavChoice::PluginTypeView(profile_id, plugin)) => { 77 | views::run_plugin_type_view(ctx, *profile_id, plugin)? 78 | } 79 | Some(NavChoice::NewProxyGroupView) => views::run_new_proxy_group_view(ctx)?, 80 | Some(NavChoice::ProxyGroupView(id)) => views::run_proxy_group_view(ctx, *id)?, 81 | Some(NavChoice::ProxyTypeView(group_id)) => views::run_proxy_type_view(ctx, *group_id)?, 82 | Some(NavChoice::InputView(req)) => views::run_input_view(ctx, req)?, 83 | Some(NavChoice::Back) => { 84 | nav_choices.pop(); // Pop "Back" out 85 | nav_choices.pop(); // Pop this page out 86 | continue; 87 | } 88 | None => break Ok(()), 89 | }; 90 | nav_choices.push(next_nav_choice); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ytflow/src/config/plugin/socket_listener.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::factory::*; 4 | use crate::config::*; 5 | 6 | #[derive(Deserialize)] 7 | pub struct SocketListenerFactory<'a> { 8 | #[serde(borrow)] 9 | #[serde(default)] 10 | tcp_listen: Vec<&'a str>, 11 | #[serde(borrow)] 12 | #[serde(default)] 13 | udp_listen: Vec<&'a str>, 14 | tcp_next: &'a str, 15 | udp_next: &'a str, 16 | } 17 | 18 | impl<'de> SocketListenerFactory<'de> { 19 | pub(in super::super) fn parse(plugin: &'de Plugin) -> ConfigResult> { 20 | let Plugin { param, name, .. } = plugin; 21 | let config: Self = parse_param(name, param)?; 22 | Ok(ParsedPlugin { 23 | requires: (!config.tcp_listen.is_empty()) 24 | .then_some(Descriptor { 25 | descriptor: config.tcp_next, 26 | r#type: AccessPointType::STREAM_HANDLER, 27 | }) 28 | .into_iter() 29 | .chain((!config.udp_listen.is_empty()).then_some(Descriptor { 30 | descriptor: config.udp_next, 31 | r#type: AccessPointType::DATAGRAM_SESSION_HANDLER, 32 | })) 33 | .collect(), 34 | factory: config, 35 | provides: vec![], 36 | resources: vec![], 37 | }) 38 | } 39 | } 40 | 41 | impl<'de> Factory for SocketListenerFactory<'de> { 42 | #[cfg(feature = "plugins")] 43 | fn load(&mut self, plugin_name: String, set: &mut PartialPluginSet) -> LoadResult<()> { 44 | use crate::plugin::reject::RejectHandler; 45 | use crate::plugin::socket; 46 | 47 | if !self.tcp_listen.is_empty() { 48 | let tcp_next = set 49 | .get_or_create_stream_handler(plugin_name.clone(), self.tcp_next) 50 | .unwrap_or_else(|e| { 51 | set.errors.push(e); 52 | Arc::downgrade(&(Arc::new(RejectHandler) as _)) 53 | }); 54 | for tcp_listen in &self.tcp_listen { 55 | match socket::listen_tcp(tcp_next.clone(), (*tcp_listen).to_owned()) { 56 | Ok(handle) => set.fully_constructed.long_running_tasks.push(handle), 57 | Err(e) => { 58 | set.errors.push(LoadError::Io { 59 | plugin: plugin_name.clone(), 60 | error: e, 61 | }); 62 | } 63 | } 64 | } 65 | } 66 | if !self.udp_listen.is_empty() { 67 | let udp_next = set 68 | .get_or_create_datagram_handler(plugin_name.clone(), self.udp_next) 69 | .unwrap_or_else(|e| { 70 | set.errors.push(e); 71 | Arc::downgrade(&(Arc::new(RejectHandler) as _)) 72 | }); 73 | for udp_listen in &self.udp_listen { 74 | match socket::listen_udp(udp_next.clone(), (*udp_listen).to_owned()) { 75 | Ok(handle) => set.fully_constructed.long_running_tasks.push(handle), 76 | Err(e) => { 77 | set.errors.push(LoadError::Io { 78 | plugin: plugin_name.clone(), 79 | error: e, 80 | }); 81 | } 82 | } 83 | } 84 | } 85 | Ok(()) 86 | } 87 | } 88 | --------------------------------------------------------------------------------