├── .gitignore ├── rust-toolchain.toml ├── assets ├── realm.png └── conf_tree ├── rustfmt.toml ├── Dockerfile ├── Dockerfile.alpine ├── examples ├── basic.toml ├── gen.sh ├── legacy.json ├── basic.json ├── tls.toml ├── ws.toml ├── good.toml ├── wss.toml ├── tls.json ├── ws.json ├── good.json ├── wss.json ├── balance.toml ├── balance.json ├── full.toml └── full.json ├── src ├── lib.rs ├── cmd │ ├── sub.rs │ ├── mod.rs │ └── flag.rs ├── consts.rs ├── bin.rs └── conf │ ├── log.rs │ ├── mod.rs │ ├── legacy │ └── mod.rs │ ├── net.rs │ ├── endpoint.rs │ └── dns.rs ├── realm_hook ├── examples │ ├── allow_all.cpp │ ├── reject_all.cpp │ └── makefile ├── Cargo.toml ├── src │ ├── lib.rs │ └── pre_conn.rs ├── realm.h └── README.md ├── realm_core ├── README.md ├── src │ ├── lib.rs │ ├── tcp │ │ ├── plain.rs │ │ ├── hook.rs │ │ ├── mod.rs │ │ ├── transport.rs │ │ ├── socket.rs │ │ ├── middle.rs │ │ └── proxy.rs │ ├── udp │ │ ├── mod.rs │ │ ├── sockmap.rs │ │ ├── socket.rs │ │ └── middle.rs │ ├── trick.rs │ ├── time.rs │ ├── dns │ │ └── mod.rs │ └── endpoint.rs ├── Cargo.toml └── tests │ ├── tcp.rs │ ├── udp.rs │ ├── proxy_v1.rs │ └── proxy_v2.rs ├── realm_syscall ├── README.md ├── src │ ├── lib.rs │ ├── daemon.rs │ ├── nofile.rs │ └── socket.rs └── Cargo.toml ├── realm_lb ├── README.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── balancer.rs │ ├── round_robin.rs │ └── ip_hash.rs ├── realm_io ├── Cargo.toml ├── README.md └── src │ ├── mem_copy.rs │ ├── lib.rs │ ├── statistic.rs │ ├── buf.rs │ ├── bidi_copy.rs │ ├── peek.rs │ └── zero_copy.rs ├── .github └── workflows │ ├── ci.yml │ ├── container-image.yml │ ├── cross_compile.yml │ └── release.yml ├── LICENSE ├── Cargo.toml ├── readme.container.md └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /debug 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /assets/realm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zephyrchien/realm/HEAD/assets/realm.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | reorder_imports = false 3 | reorder_modules = false 4 | #fn_args_layout = "Vertical" 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye-slim 2 | 3 | COPY target/x86_64-unknown-linux-musl/release/realm /usr/bin 4 | RUN chmod +x /usr/bin/realm 5 | 6 | ENTRYPOINT ["/usr/bin/realm"] -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | COPY target/x86_64-unknown-linux-musl/release/realm /usr/bin 4 | RUN chmod +x /usr/bin/realm 5 | 6 | ENTRYPOINT ["/usr/bin/realm"] -------------------------------------------------------------------------------- /examples/basic.toml: -------------------------------------------------------------------------------- 1 | [[endpoints]] 2 | listen = "0.0.0.0:5000" 3 | remote = "1.1.1.1:443" 4 | 5 | [[endpoints]] 6 | listen = "0.0.0.0:10000" 7 | remote = "www.google.com:443" 8 | -------------------------------------------------------------------------------- /examples/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for toml in ./*.toml; do 4 | json="${toml%.toml}.json" 5 | echo convert ${toml} into ${json} 6 | cat ${toml}| tomlq > ${json} 7 | done 8 | -------------------------------------------------------------------------------- /examples/legacy.json: -------------------------------------------------------------------------------- 1 | { 2 | "listening_addresses": ["0.0.0.0"], 3 | "listening_ports": ["8080-8088"], 4 | "remote_addresses": ["1.1.1.1","2.2.2.2"], 5 | "remote_ports": ["443"] 6 | } 7 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | pub mod conf; 3 | pub mod consts; 4 | pub use realm_core as core; 5 | 6 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 7 | pub const ENV_CONFIG: &str = "REALM_CONF"; 8 | -------------------------------------------------------------------------------- /realm_hook/examples/allow_all.cpp: -------------------------------------------------------------------------------- 1 | #include "../realm.h" 2 | 3 | 4 | uint32_t realm_first_pkt_len() 5 | { 6 | return 0; 7 | }; 8 | 9 | 10 | int32_t realm_decide_remote_idx(int32_t idx, const char *buf) 11 | { 12 | return 0; 13 | }; 14 | -------------------------------------------------------------------------------- /realm_hook/examples/reject_all.cpp: -------------------------------------------------------------------------------- 1 | #include "../realm.h" 2 | 3 | 4 | uint32_t realm_first_pkt_len() 5 | { 6 | return 0; 7 | }; 8 | 9 | 10 | int32_t realm_decide_remote_idx(int32_t idx, const char *buf) 11 | { 12 | return -1; 13 | }; 14 | -------------------------------------------------------------------------------- /examples/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoints": [ 3 | { 4 | "listen": "0.0.0.0:5000", 5 | "remote": "1.1.1.1:443" 6 | }, 7 | { 8 | "listen": "0.0.0.0:10000", 9 | "remote": "www.google.com:443" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /realm_core/README.md: -------------------------------------------------------------------------------- 1 | # Realm Core 2 | 3 | [![crates.io](https://img.shields.io/crates/v/realm_core.svg)](https://crates.io/crates/realm_core) 4 | [![Released API docs](https://docs.rs/realm_core/badge.svg)](https://docs.rs/realm_core) 5 | 6 | Realm's core facilities. 7 | -------------------------------------------------------------------------------- /examples/tls.toml: -------------------------------------------------------------------------------- 1 | [[endpoints]] 2 | listen = "127.0.0.1:10000" 3 | remote = "127.0.0.1:20000" 4 | remote_transport = "tls;sni=example.com;insecure" 5 | 6 | [[endpoints]] 7 | listen = "127.0.0.1:20000" 8 | remote = "127.0.0.1:30000" 9 | listen_transport = "tls;servername=example.com" 10 | -------------------------------------------------------------------------------- /examples/ws.toml: -------------------------------------------------------------------------------- 1 | [[endpoints]] 2 | listen = "127.0.0.1:10000" 3 | remote = "127.0.0.1:20000" 4 | remote_transport = "ws;host=example.com;path=/chat" 5 | 6 | [[endpoints]] 7 | listen = "127.0.0.1:20000" 8 | remote = "127.0.0.1:30000" 9 | listen_transport = "ws;host=example.com;path=/chat" 10 | -------------------------------------------------------------------------------- /realm_syscall/README.md: -------------------------------------------------------------------------------- 1 | # Realm Syscall 2 | 3 | [![crates.io](https://img.shields.io/crates/v/realm_syscall.svg)](https://crates.io/crates/realm_syscall) 4 | [![Released API docs](https://docs.rs/realm_syscall/badge.svg)](https://docs.rs/realm_syscall) 5 | 6 | Realm's convenient syscall collections. 7 | -------------------------------------------------------------------------------- /examples/good.toml: -------------------------------------------------------------------------------- 1 | [log] 2 | level = "warn" 3 | output = "/var/log/realm.log" 4 | 5 | [network] 6 | no_tcp = false 7 | use_udp = true 8 | 9 | [[endpoints]] 10 | listen = "0.0.0.0:5000" 11 | remote = "1.1.1.1:443" 12 | 13 | [[endpoints]] 14 | listen = "0.0.0.0:10000" 15 | remote = "www.google.com:443" 16 | -------------------------------------------------------------------------------- /realm_hook/examples/makefile: -------------------------------------------------------------------------------- 1 | CXX := g++ 2 | CXXFLAGS += -std=c++11 3 | CXXFLAGS += -s -O2 -shared -fPIC 4 | 5 | SRCS := $(wildcard *.cpp) 6 | DYLIBS := $(SRCS:%.cpp=%.so) 7 | 8 | all: $(DYLIBS) 9 | 10 | $(DYLIBS): %.so: %.cpp 11 | $(CXX) $(CXXFLAGS) $< -o $@ 12 | 13 | 14 | .PHONY: clean 15 | clean: 16 | rm $(DYLIBS) 17 | -------------------------------------------------------------------------------- /realm_syscall/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Realm's convenient syscall collections. 2 | 3 | #[cfg(unix)] 4 | mod daemon; 5 | #[cfg(unix)] 6 | pub use daemon::*; 7 | 8 | #[cfg(all(unix, not(target_os = "android")))] 9 | mod nofile; 10 | #[cfg(all(unix, not(target_os = "android")))] 11 | pub use nofile::*; 12 | 13 | mod socket; 14 | pub use socket::*; 15 | -------------------------------------------------------------------------------- /realm_lb/README.md: -------------------------------------------------------------------------------- 1 | # Realm Load Balance 2 | 3 | [![crates.io](https://img.shields.io/crates/v/realm_lb.svg)](https://crates.io/crates/realm_lb) 4 | [![Released API docs](https://docs.rs/realm_lb/badge.svg)](https://docs.rs/realm_lb) 5 | 6 | Realm's load balance strategies. 7 | 8 | Current implemented: 9 | 10 | - IP Hash 11 | - Round Robin 12 | -------------------------------------------------------------------------------- /examples/wss.toml: -------------------------------------------------------------------------------- 1 | [[endpoints]] 2 | listen = "127.0.0.1:10000" 3 | remote = "127.0.0.1:20000" 4 | remote_transport = "ws;host=example.com;path=/chat;tls;sni=example.com;insecure" 5 | 6 | [[endpoints]] 7 | listen = "127.0.0.1:20000" 8 | remote = "127.0.0.1:30000" 9 | listen_transport = "ws;host=example.com;path=/chat;tls;servername=example.com" 10 | -------------------------------------------------------------------------------- /examples/tls.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoints": [ 3 | { 4 | "listen": "127.0.0.1:10000", 5 | "remote": "127.0.0.1:20000", 6 | "remote_transport": "tls;sni=example.com;insecure" 7 | }, 8 | { 9 | "listen": "127.0.0.1:20000", 10 | "remote": "127.0.0.1:30000", 11 | "listen_transport": "tls;servername=example.com" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/ws.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoints": [ 3 | { 4 | "listen": "127.0.0.1:10000", 5 | "remote": "127.0.0.1:20000", 6 | "remote_transport": "ws;host=example.com;path=/chat" 7 | }, 8 | { 9 | "listen": "127.0.0.1:20000", 10 | "remote": "127.0.0.1:30000", 11 | "listen_transport": "ws;host=example.com;path=/chat" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "level": "warn", 4 | "output": "/var/log/realm.log" 5 | }, 6 | "network": { 7 | "no_tcp": false, 8 | "use_udp": true 9 | }, 10 | "endpoints": [ 11 | { 12 | "listen": "0.0.0.0:5000", 13 | "remote": "1.1.1.1:443" 14 | }, 15 | { 16 | "listen": "0.0.0.0:10000", 17 | "remote": "www.google.com:443" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /realm_lb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "realm_lb" 3 | version = "0.1.0" 4 | authors = ["zephyr "] 5 | description = "Realm's flexible hooks." 6 | repository = "https://github.com/zhboner/realm" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/realm_lb" 9 | keywords = ["lb", "balance", "network"] 10 | edition = "2021" 11 | license = "MIT" 12 | 13 | [dev-dependencies] 14 | average = "0.13" 15 | -------------------------------------------------------------------------------- /realm_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Realm's core facilities. 2 | 3 | pub mod dns; 4 | pub mod tcp; 5 | pub mod udp; 6 | pub mod time; 7 | pub mod trick; 8 | pub mod endpoint; 9 | 10 | pub use realm_io; 11 | pub use realm_syscall; 12 | 13 | #[cfg(feature = "hook")] 14 | pub use realm_hook as hook; 15 | 16 | #[cfg(feature = "balance")] 17 | pub use realm_lb as balance; 18 | 19 | #[cfg(feature = "transport")] 20 | pub use kaminari; 21 | -------------------------------------------------------------------------------- /examples/wss.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoints": [ 3 | { 4 | "listen": "127.0.0.1:10000", 5 | "remote": "127.0.0.1:20000", 6 | "remote_transport": "ws;host=example.com;path=/chat;tls;sni=example.com;insecure" 7 | }, 8 | { 9 | "listen": "127.0.0.1:20000", 10 | "remote": "127.0.0.1:30000", 11 | "listen_transport": "ws;host=example.com;path=/chat;tls;servername=example.com" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /realm_hook/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "realm_hook" 3 | version = "0.1.4" 4 | authors = ["zephyr "] 5 | description = "Realm's flexible hooks." 6 | repository = "https://github.com/zhboner/realm/realm_hook" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/realm_hook" 9 | keywords = ["ffi", "hook"] 10 | edition = "2021" 11 | license = "MIT" 12 | 13 | 14 | [dependencies] 15 | once_cell = "1" 16 | libloading = "0.7" 17 | -------------------------------------------------------------------------------- /examples/balance.toml: -------------------------------------------------------------------------------- 1 | [log] 2 | level = "warn" 3 | output = "/var/log/realm.log" 4 | 5 | [network] 6 | no_tcp = false 7 | use_udp = true 8 | 9 | [[endpoints]] 10 | listen = "0.0.0.0:10000" 11 | remote = "127.0.0.1:20000" 12 | extra_remotes = ["127.0.0.1:20001", "127.0.0.1:20002"] 13 | balance = "roundrobin: 4, 2, 1" 14 | 15 | [[endpoints]] 16 | listen = "0.0.0.0:20000" 17 | remote = "127.0.0.1:30000" 18 | extra_remotes = ["127.0.0.1:30001"] 19 | balance = "iphash: 2, 1" 20 | -------------------------------------------------------------------------------- /realm_syscall/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "realm_syscall" 3 | version = "0.1.6" 4 | authors = ["zephyr "] 5 | description = "Realm's convenient syscall collections." 6 | repository = "https://github.com/zhboner/realm/realm_syscall" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/realm_syscall" 9 | keywords = ["network", "syscall"] 10 | edition = "2021" 11 | license = "MIT" 12 | 13 | [dependencies] 14 | libc = "0.2" 15 | socket2 = "0.4" 16 | 17 | [target.'cfg(unix)'.dependencies] 18 | daemonize = "0.4" 19 | -------------------------------------------------------------------------------- /realm_hook/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Realm's flexible hooks. 2 | //! 3 | //! ## Pre-connect Hook 4 | //! 5 | //! [`first_pkt_len`](pre_conn::first_pkt_len) 6 | //! 7 | //! [`decide_remote_idx`](pre_conn::decide_remote_idx) 8 | //! 9 | 10 | pub mod pre_conn; 11 | 12 | macro_rules! call_ffi { 13 | ($dylib: expr, $symbol: expr => $t: ty $(, $arg: expr)*) => { 14 | unsafe { 15 | let fp = $dylib.get().unwrap().get::<$t>($symbol).unwrap(); 16 | fp($($arg,)*) 17 | } 18 | }; 19 | } 20 | 21 | pub(crate) use call_ffi; 22 | -------------------------------------------------------------------------------- /realm_hook/realm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef __cplusplus 6 | extern "C" { 7 | #endif 8 | 9 | // Get the required length of first packet. 10 | uint32_t realm_first_pkt_len(); 11 | 12 | // Get the index of the selected remote peer. 13 | // 14 | // Remote peers are defined in `remote`(default) and `extra_remotes`(extended), 15 | // where there should be at least 1 remote peer whose idx is 0. 16 | // 17 | // idx < 0 means **ban**. 18 | // idx = 0 means **default**. 19 | int32_t realm_decide_remote_idx(int32_t max_remote_idx, const char *pkt); 20 | 21 | 22 | 23 | #ifdef __cplusplus 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /realm_io/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "realm_io" 3 | version = "0.3.5" 4 | authors = ["zephyr "] 5 | description = "Realm's high performance IO collections." 6 | repository = "https://github.com/zhboner/realm/realm_io" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/realm_io" 9 | keywords = ["network", "zero-copy", "relay"] 10 | edition = "2021" 11 | license = "MIT" 12 | 13 | [dependencies] 14 | libc = "0.2" 15 | tokio = "1.9" 16 | 17 | [target.'cfg(unix)'.dependencies] 18 | tokio = { version = "1.9", features = ["net"] } 19 | 20 | [features] 21 | default = [] 22 | brutal-shutdown = [] 23 | peek = [] 24 | statistic = [] 25 | -------------------------------------------------------------------------------- /realm_lb/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(unchecked_math)] 2 | 3 | /// Peer token. 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 5 | pub struct Token(pub u8); 6 | 7 | /// Load balance traits. 8 | pub trait Balance { 9 | type State; 10 | 11 | /// Constructor. 12 | fn new(weights: &[u8]) -> Self; 13 | 14 | /// Get next peer. 15 | fn next(&self, state: &Self::State) -> Option; 16 | 17 | /// Total peers. 18 | fn total(&self) -> u8; 19 | } 20 | 21 | /// Iphash impl. 22 | pub mod ip_hash; 23 | 24 | /// Round-robin impl. 25 | pub mod round_robin; 26 | 27 | mod balancer; 28 | pub use balancer::{Balancer, BalanceCtx, Strategy}; 29 | -------------------------------------------------------------------------------- /examples/balance.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "level": "warn", 4 | "output": "/var/log/realm.log" 5 | }, 6 | "network": { 7 | "no_tcp": false, 8 | "use_udp": true 9 | }, 10 | "endpoints": [ 11 | { 12 | "listen": "0.0.0.0:10000", 13 | "remote": "127.0.0.1:20000", 14 | "extra_remotes": [ 15 | "127.0.0.1:20001", 16 | "127.0.0.1:20002" 17 | ], 18 | "balance": "roundrobin: 4, 2, 1" 19 | }, 20 | { 21 | "listen": "0.0.0.0:20000", 22 | "remote": "127.0.0.1:30000", 23 | "extra_remotes": [ 24 | "127.0.0.1:30001", 25 | "127.0.0.1:30002" 26 | ], 27 | "balance": "iphash: 4, 2, 1" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /realm_core/src/tcp/plain.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use tokio::net::TcpStream; 3 | 4 | const EINVAL: i32 = 22; 5 | 6 | #[inline] 7 | pub async fn run_relay(mut local: TcpStream, mut remote: TcpStream) -> Result<()> { 8 | #[cfg(all(target_os = "linux"))] 9 | { 10 | match realm_io::bidi_zero_copy(&mut local, &mut remote).await { 11 | Ok(()) => Ok(()), 12 | Err(ref e) if e.raw_os_error().map_or(false, |ec| ec == EINVAL) => { 13 | realm_io::bidi_copy(&mut local, &mut remote).await 14 | } 15 | Err(e) => Err(e), 16 | } 17 | } 18 | 19 | #[cfg(not(target_os = "linux"))] 20 | { 21 | realm_io::bidi_copy(&mut local, &mut remote).await 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /realm_hook/README.md: -------------------------------------------------------------------------------- 1 | # Realm Hook 2 | 3 | [![crates.io](https://img.shields.io/crates/v/realm_hook.svg)](https://crates.io/crates/realm_hook) 4 | [![Released API docs](https://docs.rs/realm_hook/badge.svg)](https://docs.rs/realm_hook) 5 | 6 | Realm's flexible hooks. 7 | 8 | ## Pre-connect Hook 9 | 10 | ```c 11 | // Get the required length of first packet. 12 | uint32_t realm_first_pkt_len(); 13 | ``` 14 | 15 | ```c 16 | // Get the index of the selected remote peer. 17 | // 18 | // Remote peers are defined in `remote`(default) and `extra_remotes`(extended), 19 | // where there should be at least 1 remote peer whose idx is 0. 20 | // 21 | // idx < 0 means **ban**. 22 | // idx = 0 means **default**. 23 | int32_t realm_decide_remote_idx(int32_t max_remote_idx, const char *pkt); 24 | ``` 25 | -------------------------------------------------------------------------------- /realm_syscall/src/daemon.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use daemonize::Daemonize; 3 | 4 | /// Daemonize. 5 | /// 6 | /// Fork the process in the background, 7 | /// disassociate from process group and the control terminal. 8 | /// 9 | /// Keep current working directory, 10 | /// redirect all standard streams to `/dev/null`. 11 | /// 12 | /// Finally, print a message if succeeds or an error occurs. 13 | #[cfg(unix)] 14 | pub fn daemonize(msg: &'static str) { 15 | let pwd = current_dir().unwrap().canonicalize().unwrap(); 16 | 17 | let daemon = Daemonize::new() 18 | .umask(0) 19 | .working_directory(pwd) 20 | .exit_action(move || println!("{}", msg)); 21 | 22 | daemon 23 | .start() 24 | .unwrap_or_else(|e| eprintln!("failed to daemonize: {}", e)); 25 | } 26 | -------------------------------------------------------------------------------- /examples/full.toml: -------------------------------------------------------------------------------- 1 | [log] 2 | level = "warn" 3 | output = "/var/log/realm.log" 4 | 5 | [dns] 6 | mode = "ipv4_only" 7 | protocol = "tcp_and_udp" 8 | nameservers = ["8.8.8.8:53", "8.8.4.4:53"] 9 | min_ttl = 600 10 | max_ttl = 3600 11 | cache_size = 256 12 | 13 | [network] 14 | no_tcp = false 15 | use_udp = true 16 | tcp_timeout = 5 17 | udp_timeout = 30 18 | send_proxy = true 19 | send_proxy_version = 2 20 | accept_proxy = true 21 | accept_proxy_timeout = 5 22 | 23 | [[endpoints]] 24 | listen = "0.0.0.0:5000" 25 | remote = "1.1.1.1:443" 26 | extra_remotes = ["1.1.1.2:443", "1.1.1.3:443"] 27 | balance = "roundrobin: 4, 2, 1" 28 | through = "0.0.0.0" 29 | interface = "lo" 30 | 31 | [[endpoints]] 32 | listen = "0.0.0.0:10000" 33 | remote = "www.google.com:443" 34 | extra_remotes = ["www.youtube.com:443"] 35 | balance = "roundrobin: 2, 1" 36 | through = "0.0.0.0" 37 | interface = "wlan0" 38 | -------------------------------------------------------------------------------- /realm_core/src/udp/mod.rs: -------------------------------------------------------------------------------- 1 | //! UDP relay entrance. 2 | 3 | mod socket; 4 | mod sockmap; 5 | mod middle; 6 | 7 | use std::io::Result; 8 | 9 | use crate::endpoint::Endpoint; 10 | 11 | use sockmap::SockMap; 12 | use middle::associate_and_relay; 13 | 14 | /// UDP Buffer size. 15 | pub const BUF_SIZE: usize = 2048; 16 | 17 | /// Launch a udp relay. 18 | pub async fn run_udp(endpoint: Endpoint) -> Result<()> { 19 | let Endpoint { 20 | laddr, 21 | raddr, 22 | conn_opts, 23 | .. 24 | } = endpoint; 25 | 26 | let sockmap = SockMap::new(); 27 | 28 | let lis = socket::bind(&laddr).unwrap_or_else(|e| panic!("[udp]failed to bind {}: {}", laddr, e)); 29 | 30 | loop { 31 | if let Err(e) = associate_and_relay(&lis, &raddr, &conn_opts, &sockmap).await { 32 | log::error!("[udp]error: {}", e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | clippy_check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: nightly 15 | components: clippy 16 | default: true 17 | override: true 18 | - uses: actions-rs/clippy-check@v1 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | args: --all-features 22 | run_test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: nightly 29 | default: true 30 | override: true 31 | - run: cargo test -v --all --no-fail-fast 32 | -------------------------------------------------------------------------------- /realm_core/src/tcp/hook.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, Error, ErrorKind}; 2 | 3 | use tokio::net::TcpStream; 4 | use realm_hook::pre_conn::{self, first_pkt_len, decide_remote_idx}; 5 | 6 | use crate::endpoint::RemoteAddr; 7 | 8 | pub async fn pre_connect_hook<'a>( 9 | local: &mut TcpStream, 10 | raddr: &'a RemoteAddr, 11 | extra_raddrs: &'a Vec, 12 | ) -> Result<&'a RemoteAddr> { 13 | if !pre_conn::is_loaded() { 14 | return Ok(raddr); 15 | } 16 | 17 | let len = first_pkt_len() as usize; 18 | let mut buf = Vec::::new(); 19 | 20 | if len != 0 { 21 | buf.resize(len, 0); 22 | while local.peek(&mut buf).await? < len {} 23 | } 24 | 25 | let mut idx = extra_raddrs.len() as i32; 26 | idx = decide_remote_idx(idx, buf.as_ptr()); 27 | 28 | match idx { 29 | 0 => Ok(raddr), 30 | i if i >= 1 && i <= idx => Ok(&extra_raddrs[i as usize - 1]), 31 | _ => Err(Error::new(ErrorKind::Other, "rejected by pre-connect hook")), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 zhboner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /realm_core/src/udp/sockmap.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::sync::{Arc, RwLock}; 3 | use std::collections::HashMap; 4 | 5 | use tokio::net::UdpSocket; 6 | 7 | pub struct SockMap(RwLock>>); 8 | 9 | impl SockMap { 10 | pub fn new() -> Self { 11 | Self(RwLock::new(HashMap::new())) 12 | } 13 | 14 | #[inline] 15 | pub fn find(&self, addr: &SocketAddr) -> Option> { 16 | // fetch the lock 17 | 18 | let sockmap = self.0.read().unwrap(); 19 | 20 | sockmap.get(addr).cloned() 21 | 22 | // drop the lock 23 | } 24 | 25 | #[inline] 26 | pub fn insert(&self, addr: SocketAddr, socket: Arc) { 27 | // fetch the lock 28 | let mut sockmap = self.0.write().unwrap(); 29 | 30 | let _ = sockmap.insert(addr, socket); 31 | 32 | // drop the lock 33 | } 34 | 35 | #[inline] 36 | pub fn remove(&self, addr: &SocketAddr) { 37 | // fetch the lock 38 | let mut sockmap = self.0.write().unwrap(); 39 | 40 | let _ = sockmap.remove(addr); 41 | 42 | // drop the lock 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /realm_core/src/trick.rs: -------------------------------------------------------------------------------- 1 | //! Unsafe tricks. 2 | 3 | use core::ops::Deref; 4 | 5 | /// A reference wrapper with static lifetime. 6 | /// 7 | /// # Safety 8 | /// 9 | /// Inner pointer comes from a immutable reference, which is not null. 10 | /// 11 | /// Pointee memory must remain valid during the eventloop. 12 | pub struct Ref(*const T); 13 | 14 | unsafe impl Send for Ref {} 15 | unsafe impl Sync for Ref {} 16 | 17 | impl Copy for Ref {} 18 | 19 | impl Clone for Ref { 20 | fn clone(&self) -> Self { 21 | *self 22 | } 23 | } 24 | 25 | impl Deref for Ref { 26 | type Target = T; 27 | 28 | #[inline] 29 | fn deref(&self) -> &Self::Target { 30 | unsafe { &*self.0 } 31 | } 32 | } 33 | 34 | impl AsRef for Ref { 35 | #[inline] 36 | fn as_ref(&self) -> &T { 37 | unsafe { &*self.0 } 38 | } 39 | } 40 | 41 | impl From<&T> for Ref { 42 | #[inline] 43 | fn from(x: &T) -> Self { 44 | Ref(x as *const _) 45 | } 46 | } 47 | 48 | impl Ref { 49 | #[inline] 50 | pub const fn new(x: &T) -> Self { 51 | Self(x as *const _) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/full.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "level": "warn", 4 | "output": "/var/log/realm.log" 5 | }, 6 | "dns": { 7 | "mode": "ipv4_only", 8 | "protocol": "tcp_and_udp", 9 | "nameservers": [ 10 | "8.8.8.8:53", 11 | "8.8.4.4:53" 12 | ], 13 | "min_ttl": 600, 14 | "max_ttl": 3600, 15 | "cache_size": 256 16 | }, 17 | "network": { 18 | "no_tcp": false, 19 | "use_udp": true, 20 | "tcp_timeout": 5, 21 | "udp_timeout": 30, 22 | "send_proxy": true, 23 | "send_proxy_version": 2, 24 | "accept_proxy": true, 25 | "accept_proxy_timeout": 5 26 | }, 27 | "endpoints": [ 28 | { 29 | "listen": "0.0.0.0:5000", 30 | "remote": "1.1.1.1:443", 31 | "extra_remotes": [ 32 | "1.1.1.2:443", 33 | "1.1.1.3:443" 34 | ], 35 | "balance": "roundrobin: 4, 2, 1", 36 | "through": "0.0.0.0", 37 | "interface": "lo" 38 | }, 39 | { 40 | "listen": "0.0.0.0:10000", 41 | "remote": "www.google.com:443", 42 | "extra_remotes": [ 43 | "www.youtube.com:443" 44 | ], 45 | "balance": "roundrobin: 2, 1", 46 | "through": "0.0.0.0", 47 | "interface": "wlan0" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /realm_core/src/udp/socket.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::net::SocketAddr; 3 | 4 | use tokio::net::UdpSocket; 5 | use realm_syscall::new_udp_socket; 6 | 7 | use crate::endpoint::ConnectOpts; 8 | 9 | #[allow(clippy::clone_on_copy)] 10 | pub fn bind(laddr: &SocketAddr) -> Result { 11 | let socket = new_udp_socket(laddr)?; 12 | 13 | // ignore error 14 | let _ = socket.set_reuse_address(true); 15 | 16 | socket.bind(&laddr.clone().into())?; 17 | 18 | UdpSocket::from_std(socket.into()) 19 | } 20 | 21 | pub async fn associate(raddr: &SocketAddr, conn_opts: &ConnectOpts) -> Result { 22 | let ConnectOpts { 23 | bind_address, 24 | 25 | #[cfg(target_os = "linux")] 26 | bind_interface, 27 | .. 28 | } = conn_opts; 29 | 30 | let socket = new_udp_socket(raddr)?; 31 | 32 | // ignore error 33 | let _ = socket.set_reuse_address(true); 34 | 35 | if let Some(addr) = *bind_address { 36 | socket.bind(&addr.into())?; 37 | } 38 | 39 | #[cfg(target_os = "linux")] 40 | if let Some(iface) = bind_interface { 41 | realm_syscall::bind_to_device(&socket, iface)?; 42 | } 43 | 44 | UdpSocket::from_std(socket.into()) 45 | } 46 | -------------------------------------------------------------------------------- /realm_hook/src/pre_conn.rs: -------------------------------------------------------------------------------- 1 | //! Pre-connect hook. 2 | 3 | use once_cell::unsync::OnceCell; 4 | use libloading::Library; 5 | 6 | use super::call_ffi; 7 | 8 | static mut LOAD: bool = false; 9 | static mut DYLIB: OnceCell = OnceCell::new(); 10 | 11 | /// Load a dynamic library. 12 | /// 13 | /// This is not thread-safe and must be called before interacting with FFI. 14 | pub fn load_dylib(path: &str) { 15 | unsafe { 16 | DYLIB.set(Library::new(path).unwrap()).unwrap(); 17 | LOAD = true; 18 | } 19 | } 20 | 21 | /// Check if the dynamic library is loaded. 22 | pub fn is_loaded() -> bool { 23 | unsafe { LOAD } 24 | } 25 | 26 | /// Get the required length of first packet. 27 | pub fn first_pkt_len() -> u32 { 28 | call_ffi!(DYLIB, b"realm_first_pkt_len" => unsafe extern "C" fn() -> u32) 29 | } 30 | 31 | /// Get the index of the selected remote peer. 32 | /// 33 | /// Remote peers are defined in `remote`(default) and `extra_remotes`(extended), 34 | /// where there should be at least 1 remote peer whose idx is 0. 35 | /// 36 | /// idx < 0 means **ban**. 37 | /// idx = 0 means **default**. 38 | pub fn decide_remote_idx(idx: i32, buf: *const u8) -> i32 { 39 | call_ffi!( 40 | DYLIB, b"realm_decide_remote_idx" => unsafe extern "C" fn(i32, *const u8) -> i32, 41 | idx, buf 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /realm_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "realm_core" 3 | version = "0.3.3" 4 | authors = ["Realm Contributors"] 5 | description = "Realm's core facilities." 6 | repository = "https://github.com/zhboner/realm/realm_core" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/realm_core" 9 | keywords = ["network", "relay", "zero-copy", "websocket", "tls"] 10 | edition = "2021" 11 | license = "MIT" 12 | 13 | [dependencies] 14 | # realm 15 | realm_io = "0.3" 16 | realm_syscall = "0.1.6" 17 | realm_hook = { version = "0.1.4", optional = true } 18 | realm_lb = { version = "0.1.0", optional = true } 19 | kaminari = { version = "0.9", features = ["ws", "tls", "mix"], optional = true } 20 | 21 | # other 22 | futures = "0.3" 23 | log = "0.4" 24 | bytes = { version = "1", optional = true } 25 | once_cell = "1" 26 | pin-project = "1.0.11" 27 | trust-dns-resolver = "0.22" 28 | tokio = { version = "1.18", features = ["rt", "net", "time"] } 29 | proxy-protocol = { version = "0.5", optional = true } 30 | 31 | [features] 32 | default = [] 33 | hook = ["realm_hook"] 34 | balance = ["realm_lb"] 35 | brutal-shutdown = ["realm_io/brutal-shutdown"] 36 | transport = ["kaminari"] 37 | transport-boost = [] 38 | proxy = ["proxy-protocol", "bytes", "tokio/io-util"] 39 | 40 | [dev-dependencies] 41 | env_logger = "0.9" 42 | tokio = { version = "1", features = ["macros"] } 43 | -------------------------------------------------------------------------------- /src/cmd/sub.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use clap::{Command, ArgMatches}; 3 | use crate::conf::{FullConf, LegacyConf}; 4 | 5 | #[allow(clippy::let_and_return)] 6 | pub fn add_all(app: Command) -> Command { 7 | let app = add_convert(app); 8 | app 9 | } 10 | 11 | pub fn add_convert(app: Command) -> Command { 12 | let cvt = Command::new("convert") 13 | .version("0.1.0") 14 | .about("convert your legacy configuration into an advanced one") 15 | .allow_missing_positional(true) 16 | .arg_required_else_help(true) 17 | .arg(clap::arg!([config]).required(true)) 18 | .arg( 19 | clap::arg!(-t --type ) 20 | .required(false) 21 | .default_value("toml") 22 | .display_order(0), 23 | ) 24 | .arg(clap::arg!(-o --output ).required(false).display_order(1)); 25 | 26 | app.subcommand(cvt) 27 | } 28 | 29 | pub fn handle_convert(matches: &ArgMatches) { 30 | let old = matches.value_of("config").unwrap(); 31 | let old = fs::read(old).unwrap(); 32 | 33 | let data: LegacyConf = serde_json::from_slice(&old).unwrap(); 34 | let data: FullConf = data.into(); 35 | 36 | let data = match matches.value_of("type").unwrap() { 37 | "toml" => toml::to_string(&data).unwrap(), 38 | "json" => serde_json::to_string(&data).unwrap(), 39 | _ => unreachable!(), 40 | }; 41 | 42 | if let Some(out) = matches.value_of("output") { 43 | fs::write(out, &data).unwrap(); 44 | } else { 45 | println!("{}", &data) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /realm_core/src/tcp/mod.rs: -------------------------------------------------------------------------------- 1 | //! TCP relay entrance. 2 | 3 | mod socket; 4 | mod middle; 5 | mod plain; 6 | 7 | #[cfg(feature = "hook")] 8 | mod hook; 9 | 10 | #[cfg(feature = "proxy")] 11 | mod proxy; 12 | 13 | #[cfg(feature = "transport")] 14 | mod transport; 15 | 16 | use std::io::Result; 17 | 18 | use crate::trick::Ref; 19 | use crate::endpoint::Endpoint; 20 | 21 | use middle::connect_and_relay; 22 | 23 | /// Launch a tcp relay. 24 | pub async fn run_tcp(endpoint: Endpoint) -> Result<()> { 25 | let Endpoint { 26 | laddr, 27 | raddr, 28 | conn_opts, 29 | extra_raddrs, 30 | } = endpoint; 31 | 32 | let raddr = Ref::new(&raddr); 33 | let conn_opts = Ref::new(&conn_opts); 34 | let extra_raddrs = Ref::new(&extra_raddrs); 35 | 36 | let lis = socket::bind(&laddr).unwrap_or_else(|e| panic!("[tcp]failed to bind {}: {}", &laddr, e)); 37 | 38 | loop { 39 | let (local, addr) = match lis.accept().await { 40 | Ok(x) => x, 41 | Err(e) => { 42 | log::error!("[tcp]failed to accept: {}", e); 43 | break; 44 | } 45 | }; 46 | 47 | // ignore error 48 | let _ = local.set_nodelay(true); 49 | 50 | tokio::spawn(async move { 51 | match connect_and_relay(local, raddr, conn_opts, extra_raddrs).await { 52 | Ok(..) => log::debug!("[tcp]{} => {}, finish", addr, raddr.as_ref()), 53 | Err(e) => log::error!("[tcp]{} => {}, error: {}", addr, raddr.as_ref(), e), 54 | } 55 | }); 56 | } 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /realm_io/README.md: -------------------------------------------------------------------------------- 1 | # Realm IO 2 | 3 | [![crates.io](https://img.shields.io/crates/v/realm_io.svg)](https://crates.io/crates/realm_io) 4 | [![Released API docs](https://docs.rs/realm_io/badge.svg)](https://docs.rs/realm_io) 5 | 6 | Realm's high performance IO collections. 7 | 8 | ## Example 9 | 10 | ```rust 11 | use tokio::net::TcpStream; 12 | use realm_io::{bidi_copy, bidi_zero_copy, bidi_copy_buf}; 13 | use realm_io::{Pipe, CopyBuffer}; 14 | 15 | let mut left = TcpStream::connect("abc").await.unwrap(); 16 | let mut right = TcpStream::connect("def").await.unwrap(); 17 | 18 | // direct copy 19 | bidi_copy(&mut left, &mut right).await; 20 | 21 | // zero copy 22 | bidi_zero_copy(&mut left, &mut right).await; 23 | 24 | // use custom buffer(vector) 25 | let buf1 = CopyBuffer::new(vec![0; 0x2000]); 26 | let buf2 = CopyBuffer::new(vec![0; 0x2000]); 27 | bidi_copy_buf(&mut left, &mut right, buf1, buf2).await; 28 | 29 | // use custom buffer(pipe) 30 | let buf1 = CopyBuffer::new(Pipe::new().unwrap()); 31 | let buf2 = CopyBuffer::new(Pipe::new().unwrap()); 32 | bidi_copy_buf(&mut left, &mut right, buf1, buf2).await; 33 | ``` 34 | 35 | ## About Brutal Shutdown 36 | 37 | By default, `bidi_copy_buf` and other IO functions perform a **graceful shutdown**. 38 | 39 | With the feature `brutal-shutdown` enabled, these IO functions will decide to 40 | perform a **brutal shutdown** once a `FIN` packet reaches, which will forcefully 41 | close two connections on both sides without waiting for a reply packet. 42 | 43 | This is helpful when handling connections from a poorly implemented client or server, 44 | which may never shutdown its write side nor close the underlying socket. 45 | -------------------------------------------------------------------------------- /realm_syscall/src/nofile.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, Result}; 2 | use libc::{rlimit, rlim_t, RLIMIT_NOFILE}; 3 | 4 | /// Set nofile limits. 5 | /// 6 | /// `CAP_NET_ADMIN` privilege is required if exceeds hard limitation. 7 | /// 8 | /// Reference: 9 | /// - [man](https://man7.org/linux/man-pages/man2/setrlimit.2.html) 10 | /// - [shadowsocks-rust](https://github.com/shadowsocks/shadowsocks-rust/blob/master/crates/shadowsocks-service/src/sys/unix/mod.rs) 11 | #[cfg(all(unix, not(target_os = "android")))] 12 | pub fn set_nofile_limit(nofile: u64) -> Result<()> { 13 | let lim = rlimit { 14 | rlim_cur: nofile as rlim_t, 15 | rlim_max: nofile as rlim_t, 16 | }; 17 | 18 | if unsafe { libc::setrlimit(RLIMIT_NOFILE, &lim as *const _) } < 0 { 19 | Err(Error::last_os_error()) 20 | } else { 21 | Ok(()) 22 | } 23 | } 24 | 25 | /// Get current nofile limits. 26 | /// 27 | /// Reference: [man](https://man7.org/linux/man-pages/man2/setrlimit.2.html). 28 | #[cfg(all(unix, not(target_os = "android")))] 29 | pub fn get_nofile_limit() -> Result<(u64, u64)> { 30 | let mut lim = rlimit { 31 | rlim_cur: 0, 32 | rlim_max: 0, 33 | }; 34 | 35 | if unsafe { libc::getrlimit(RLIMIT_NOFILE, &mut lim as *mut _) } < 0 { 36 | Err(Error::last_os_error()) 37 | } else { 38 | Ok((lim.rlim_cur as u64, lim.rlim_max as u64)) 39 | } 40 | } 41 | 42 | /// Bump nofile limits. 43 | #[cfg(all(unix, not(target_os = "android")))] 44 | pub fn bump_nofile_limit() -> Result<()> { 45 | let (cur, max) = get_nofile_limit()?; 46 | if cur < max { 47 | set_nofile_limit(max)?; 48 | } 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /realm_core/src/time.rs: -------------------------------------------------------------------------------- 1 | //! Timeout utilities. 2 | 3 | use std::pin::Pin; 4 | use std::task::{Poll, Context}; 5 | use std::future::Future; 6 | use std::time::Duration; 7 | use std::io::{Result, ErrorKind}; 8 | 9 | use tokio::time::Sleep; 10 | 11 | use pin_project::pin_project; 12 | 13 | #[allow(clippy::large_enum_variant)] 14 | #[pin_project(project = DelayP)] 15 | enum Delay { 16 | Some(#[pin] Sleep), 17 | 18 | None, 19 | } 20 | 21 | /// Timeout future. 22 | #[pin_project] 23 | pub struct Timeout { 24 | #[pin] 25 | value: T, 26 | 27 | #[pin] 28 | delay: Delay, 29 | } 30 | 31 | impl Future for Timeout { 32 | type Output = Result; 33 | 34 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 35 | use Poll::{Pending, Ready}; 36 | 37 | let this = self.project(); 38 | 39 | if let Ready(v) = this.value.poll(cx) { 40 | return Ready(Ok(v)); 41 | } 42 | 43 | if let DelayP::Some(delay) = this.delay.project() { 44 | let delay: Pin<&mut Sleep> = delay; 45 | if delay.poll(cx).is_ready() { 46 | return Ready(Err(ErrorKind::TimedOut.into())); 47 | } 48 | } 49 | 50 | Pending 51 | } 52 | } 53 | 54 | /// Wrap a future with timeout. 55 | /// 56 | /// Timeout = 0 means never timeout. 57 | pub fn timeoutfut(future: F, timeout: usize) -> Timeout { 58 | use tokio::time::sleep; 59 | let delay = match timeout { 60 | 0 => Delay::None, 61 | x => Delay::Some(sleep(Duration::from_secs(x as u64))), 62 | }; 63 | Timeout { value: future, delay } 64 | } 65 | -------------------------------------------------------------------------------- /assets/conf_tree: -------------------------------------------------------------------------------- 1 | ## log 2 | ### level 3 | ### output 4 | ## dns 5 | ### mode 6 | ### protocol 7 | ### nameservers 8 | ### min_ttl 9 | ### max_ttl 10 | ### cache_size 11 | ## network 12 | ### no_tcp 13 | ### use_udp 14 | ### tcp_timeout 15 | ### udp_timeout 16 | ### send_proxy 17 | ### send_proxy_version 18 | ### accept_proxy 19 | ### accept_proxy_timeout 20 | ## endpoints 21 | ### listen 22 | ### remote 23 | ### extra_remotes 24 | ### balance 25 | ### through 26 | ### interface 27 | ### listen_transport 28 | ### remote_transport 29 | ### network 30 | 31 | 32 | 33 | ## log 34 | ### level 35 | ### output 36 | ## dns 37 | ### mode 38 | ### protocol 39 | ### nameservers 40 | ### min_ttl 41 | ### max_ttl 42 | ### cache_size 43 | ## network 44 | ### no_tcp 45 | ### use_udp 46 | ### tcp_timeout 47 | ### udp_timeout 48 | ### send_proxy 49 | ### send_proxy_version 50 | ### accept_proxy 51 | ### accept_proxy_timeout 52 | ## endpoints 53 | ### listen 54 | ### remote 55 | ### extra_remotes 56 | ### balance 57 | ### through 58 | ### interface 59 | ### listen_transport 60 | ### remote_transport 61 | ### network 62 | 63 | 64 | ├── log 65 | │ ├── level 66 | │ └── output 67 | ├── dns 68 | │ ├── mode 69 | │ ├── protocol 70 | │ ├── nameservers 71 | │ ├── min_ttl 72 | │ ├── max_ttl 73 | │ └── cache_size 74 | ├── network 75 | │ ├── no_tcp 76 | │ ├── use_udp 77 | │ ├── tcp_timeout 78 | │ ├── udp_timeout 79 | │ ├── send_proxy 80 | │ ├── send_proxy_version 81 | │ ├── accept_proxy 82 | │ └── accept_proxy_timeout 83 | └── endpoints 84 | ├── listen 85 | ├── remote 86 | ├── extra_remotes 87 | ├── balance 88 | ├── through 89 | ├── interface 90 | ├── listen_transport 91 | ├── remote_transport 92 | └── network 93 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "realm" 3 | version = "2.4.4" 4 | authors = ["zhboner "] 5 | edition = "2021" 6 | 7 | 8 | [workspace] 9 | members = [ 10 | "realm_io", 11 | "realm_lb", 12 | "realm_core", 13 | "realm_hook", 14 | "realm_syscall" 15 | ] 16 | 17 | 18 | [lib] 19 | name = "realm" 20 | path = "src/lib.rs" 21 | 22 | [[bin]] 23 | name = "realm" 24 | path = "src/bin.rs" 25 | 26 | 27 | [dependencies] 28 | # realm 29 | realm_core = { path = "realm_core" } 30 | 31 | # common 32 | cfg-if = "1" 33 | futures = "0.3" 34 | 35 | # runtime 36 | tokio = { version = "1", features = ["rt"] } 37 | 38 | # logger 39 | log = "0.4" 40 | fern = "0.6" 41 | chrono = "0.4" 42 | 43 | # command 44 | clap = "3.1" 45 | toml = "0.5" 46 | serde = { version = "1", features = ["derive"] } 47 | serde_json = "1" 48 | 49 | # malloc 50 | mimalloc = { version = "0.1", optional = true } 51 | 52 | [target.'cfg(not(target_env = "msvc"))'.dependencies] 53 | jemallocator = { version = "0.5", optional = true } 54 | mmap-allocator = { version = "0.3", optional = true } 55 | 56 | 57 | [dev-dependencies] 58 | env_logger = "0.9" 59 | tokio = { version = "1", features = ["macros"] } 60 | 61 | 62 | [features] 63 | default = ["hook", "proxy", "balance", "multi-thread", "transport", "brutal-shutdown" ] 64 | hook = ["realm_core/hook"] 65 | proxy = ["realm_core/proxy"] 66 | brutal-shutdown = ["realm_core/brutal-shutdown"] 67 | balance = ["realm_core/balance"] 68 | transport = ["realm_core/transport", "realm_core/transport-boost"] 69 | multi-thread = ["tokio/rt-multi-thread"] 70 | jemalloc = ["jemallocator"] 71 | mi-malloc = ["mimalloc"] 72 | page-alloc = ["mmap-allocator"] 73 | 74 | 75 | [profile.release] 76 | opt-level = 3 77 | lto = true 78 | codegen-units = 1 79 | incremental = false 80 | panic = "unwind" 81 | strip = true 82 | -------------------------------------------------------------------------------- /realm_core/tests/tcp.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::time::Duration; 3 | 4 | use tokio::net::{TcpStream, TcpListener}; 5 | use tokio::time::sleep; 6 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 7 | 8 | use realm_core::tcp::run_tcp; 9 | use realm_core::endpoint::{Endpoint, RemoteAddr}; 10 | 11 | #[tokio::test] 12 | async fn tcp() { 13 | env_logger::init(); 14 | let endpoint = Endpoint { 15 | laddr: "127.0.0.1:10000".parse().unwrap(), 16 | raddr: "127.0.0.1:20000" 17 | .parse::() 18 | .map(RemoteAddr::SocketAddr) 19 | .unwrap(), 20 | conn_opts: Default::default(), 21 | extra_raddrs: Vec::new(), 22 | }; 23 | 24 | tokio::spawn(run_tcp(endpoint)); 25 | 26 | let task1 = async { 27 | sleep(Duration::from_millis(500)).await; 28 | let mut stream = TcpStream::connect("127.0.0.1:10000").await.unwrap(); 29 | 30 | let mut buf = vec![0; 32]; 31 | 32 | for _ in 0..20 { 33 | stream.write(b"Ping Ping Ping").await.unwrap(); 34 | let n = stream.read(&mut buf).await.unwrap(); 35 | log::debug!("a got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 36 | assert_eq!(b"Pong Pong Pong", &buf[..n]); 37 | } 38 | }; 39 | 40 | let task2 = async { 41 | let lis = TcpListener::bind("127.0.0.1:20000").await.unwrap(); 42 | let (mut stream, _) = lis.accept().await.unwrap(); 43 | 44 | let mut buf = vec![0; 32]; 45 | 46 | for _ in 0..20 { 47 | let n = stream.read(&mut buf).await.unwrap(); 48 | log::debug!("b got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 49 | assert_eq!(b"Ping Ping Ping", &buf[..n]); 50 | stream.write(b"Pong Pong Pong").await.unwrap(); 51 | } 52 | }; 53 | 54 | tokio::join!(task1, task2); 55 | } 56 | -------------------------------------------------------------------------------- /readme.container.md: -------------------------------------------------------------------------------- 1 | # Use realm in container 2 | 3 | We push the OCI-based image to [Github Container Registry](https://ghcr.io) with name: `ghcr.io/zhboner/realm`. 4 | 5 | These are some tag of this image: 6 | 7 | - `latest`, `v1.*` base on debian:bullseye-silm, recommend 8 | - `alpine`, `v1.*-alpine` base on alpine:latest 9 | 10 | ## Docker 11 | 12 | ```bash 13 | docker run -d -p 9000:9000 ghcr.io/zhboner/realm:latest -l 0.0.0.0:9000 -r 192.168.233.2:9000 14 | ``` 15 | 16 | ## Docker Swarm (Docker Compose) 17 | 18 | ```yaml 19 | # ./realm.yml 20 | version: '3' 21 | services: 22 | port-9000: 23 | image: ghcr.io/zhboner/realm:latest 24 | ports: 25 | - 9000:9000 26 | command: -l 0.0.0.0:9000 -r 192.168.233.2:9000 27 | ``` 28 | 29 | ```bash 30 | docker-compose -f ./realm.yml -p realm up -d 31 | ``` 32 | 33 | ## Kubernetes 34 | 35 | ```yaml 36 | # ./realm.yml 37 | apiVersion: apps/v1 38 | kind: Deployment 39 | metadata: 40 | name: realm-demo-deployment 41 | labels: 42 | app: realm 43 | namespace: default 44 | spec: 45 | replicas: 1 46 | selector: 47 | matchLabels: 48 | app: realm 49 | template: 50 | metadata: 51 | labels: 52 | app: realm 53 | spec: 54 | containers: 55 | - name: realm 56 | image: ghcr.io/zhboner/realm:latest 57 | args: 58 | - "-l=0.0.0.0:9000" 59 | - "-r=192.168.233.2:9000" 60 | ports: 61 | - containerPort: 9000 62 | resources: 63 | requests: 64 | memory: "64Mi" 65 | cpu: "250m" 66 | limits: 67 | memory: "128Mi" 68 | cpu: "500m" 69 | --- 70 | apiVersion: v1 71 | kind: Service 72 | metadata: 73 | name: realm-lb 74 | namespace: default 75 | spec: 76 | type: LoadBalancer 77 | selector: 78 | app: realm 79 | ports: 80 | - name: edge 81 | port: 9000 82 | ``` 83 | -------------------------------------------------------------------------------- /realm_core/tests/udp.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::time::Duration; 3 | 4 | use tokio::net::UdpSocket; 5 | use tokio::time::sleep; 6 | 7 | use realm_core::udp::run_udp; 8 | use realm_core::endpoint::{Endpoint, RemoteAddr}; 9 | 10 | #[tokio::test] 11 | async fn udp() { 12 | env_logger::init(); 13 | let endpoint = Endpoint { 14 | laddr: "127.0.0.1:10000".parse().unwrap(), 15 | raddr: "127.0.0.1:20000" 16 | .parse::() 17 | .map(RemoteAddr::SocketAddr) 18 | .unwrap(), 19 | conn_opts: Default::default(), 20 | extra_raddrs: Vec::new(), 21 | }; 22 | 23 | tokio::spawn(run_udp(endpoint)); 24 | 25 | let task1 = async { 26 | sleep(Duration::from_millis(500)).await; 27 | 28 | let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); 29 | 30 | let mut buf = vec![0; 32]; 31 | let peer: SocketAddr = "127.0.0.1:10000".parse().unwrap(); 32 | 33 | for _ in 0..20 { 34 | socket.send_to(b"Ping Ping Ping", &peer).await.unwrap(); 35 | let (n, peer2) = socket.recv_from(&mut buf).await.unwrap(); 36 | assert_eq!(peer, peer2); 37 | log::debug!("a got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 38 | assert_eq!(b"Pong Pong Pong", &buf[..n]); 39 | } 40 | }; 41 | 42 | let task2 = async { 43 | let socket = UdpSocket::bind("127.0.0.1:20000").await.unwrap(); 44 | 45 | let mut buf = vec![0; 32]; 46 | 47 | for _ in 0..20 { 48 | let (n, peer) = socket.recv_from(&mut buf).await.unwrap(); 49 | log::debug!("b got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 50 | assert_eq!(b"Ping Ping Ping", &buf[..n]); 51 | socket.send_to(b"Pong Pong Pong", peer).await.unwrap(); 52 | } 53 | }; 54 | 55 | tokio::join!(task1, task2); 56 | } 57 | -------------------------------------------------------------------------------- /realm_core/src/tcp/transport.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use futures::try_join; 3 | 4 | use kaminari::{AsyncAccept, AsyncConnect, IOStream}; 5 | use kaminari::mix::{MixAccept, MixConnect}; 6 | 7 | use realm_io::{CopyBuffer, bidi_copy_buf, buf_size}; 8 | 9 | pub async fn run_relay(src: S, dst: S, ac: &MixAccept, cc: &MixConnect) -> Result<()> { 10 | macro_rules! hs_relay { 11 | ($ac: expr, $cc: expr) => { 12 | handshake_and_relay(src, dst, $ac, $cc).await 13 | }; 14 | } 15 | 16 | #[cfg(feature = "transport-boost")] 17 | { 18 | use MixConnect::*; 19 | if let Some(ac) = ac.as_plain() { 20 | return match cc { 21 | Plain(cc) => hs_relay!(ac, cc), 22 | Ws(cc) => hs_relay!(ac, cc), 23 | Tls(cc) => hs_relay!(ac, cc), 24 | Wss(cc) => hs_relay!(ac, cc), 25 | }; 26 | } 27 | } 28 | 29 | #[cfg(feature = "transport-boost")] 30 | { 31 | use MixAccept::*; 32 | if let Some(cc) = cc.as_plain() { 33 | return match ac { 34 | Plain(ac) => hs_relay!(ac, cc), 35 | Ws(ac) => hs_relay!(ac, cc), 36 | Tls(ac) => hs_relay!(ac, cc), 37 | Wss(ac) => hs_relay!(ac, cc), 38 | }; 39 | } 40 | } 41 | 42 | hs_relay!(ac, cc) 43 | } 44 | 45 | async fn handshake_and_relay(src: S, dst: S, ac: &AC, cc: &CC) -> Result<()> 46 | where 47 | S: IOStream, 48 | AC: AsyncAccept, 49 | CC: AsyncConnect, 50 | { 51 | let mut buf1 = vec![0; buf_size()]; 52 | let mut buf2 = vec![0; buf_size()]; 53 | 54 | let (mut src, mut dst) = try_join!(ac.accept(src, &mut buf1), cc.connect(dst, &mut buf2))?; 55 | 56 | let buf1 = CopyBuffer::new(buf1); 57 | let buf2 = CopyBuffer::new(buf2); 58 | 59 | bidi_copy_buf(&mut src, &mut dst, buf1, buf2).await 60 | } 61 | -------------------------------------------------------------------------------- /realm_io/src/mem_copy.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::pin::Pin; 3 | use std::task::{Poll, Context}; 4 | 5 | use tokio::io::ReadBuf; 6 | use tokio::io::{AsyncRead, AsyncWrite}; 7 | 8 | use super::{CopyBuffer, AsyncIOBuf}; 9 | use super::bidi_copy_buf; 10 | 11 | impl AsyncIOBuf for CopyBuffer 12 | where 13 | B: AsMut<[u8]>, 14 | SR: AsyncRead + AsyncWrite + Unpin, 15 | SW: AsyncRead + AsyncWrite + Unpin, 16 | { 17 | type StreamR = SR; 18 | type StreamW = SW; 19 | 20 | #[inline] 21 | fn poll_read_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamR) -> Poll> { 22 | let mut buf = ReadBuf::new(self.buf.as_mut()); 23 | Pin::new(stream).poll_read(cx, &mut buf).map_ok(|_| buf.filled().len()) 24 | } 25 | 26 | #[inline] 27 | fn poll_write_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamW) -> Poll> { 28 | Pin::new(stream).poll_write(cx, &self.buf.as_mut()[self.pos..self.cap]) 29 | } 30 | 31 | #[inline] 32 | fn poll_flush_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamW) -> Poll> { 33 | Pin::new(stream).poll_flush(cx) 34 | } 35 | } 36 | 37 | /// Copy data bidirectionally between two streams with userspace buffer. 38 | pub async fn bidi_copy(a: &mut A, b: &mut B) -> Result<()> 39 | where 40 | A: AsyncRead + AsyncWrite + Unpin, 41 | B: AsyncRead + AsyncWrite + Unpin, 42 | { 43 | let a_to_b_buf = CopyBuffer::new(vec![0u8; buf_size()].into_boxed_slice()); 44 | let b_to_a_buf = CopyBuffer::new(vec![0u8; buf_size()].into_boxed_slice()); 45 | bidi_copy_buf(a, b, a_to_b_buf, b_to_a_buf).await 46 | } 47 | 48 | mod buf_ctl { 49 | pub const DF_BUF_SIZE: usize = 0x2000; 50 | static mut BUF_SIZE: usize = DF_BUF_SIZE; 51 | 52 | /// Get current buffer size. 53 | #[inline] 54 | pub fn buf_size() -> usize { 55 | unsafe { BUF_SIZE } 56 | } 57 | 58 | /// Set current buffer size. 59 | #[inline] 60 | pub fn set_buf_size(n: usize) { 61 | unsafe { BUF_SIZE = n } 62 | } 63 | } 64 | 65 | pub use buf_ctl::{buf_size, set_buf_size}; 66 | -------------------------------------------------------------------------------- /realm_io/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Realm's high performance IO collections. 2 | //! 3 | //! ## Example 4 | //! 5 | //! ```no_run 6 | //! async { 7 | //! use tokio::net::TcpStream; 8 | //! use realm_io::{bidi_copy, bidi_zero_copy, bidi_copy_buf}; 9 | //! use realm_io::{Pipe, CopyBuffer}; 10 | //! 11 | //! let mut left = TcpStream::connect("abc").await.unwrap(); 12 | //! let mut right = TcpStream::connect("def").await.unwrap(); 13 | //! 14 | //! // direct copy 15 | //! bidi_copy(&mut left, &mut right).await; 16 | //! 17 | //! // zero copy 18 | //! bidi_zero_copy(&mut left, &mut right).await; 19 | //! 20 | //! // use custom buffer(vector) 21 | //! let buf1 = CopyBuffer::new(vec![0; 0x2000]); 22 | //! let buf2 = CopyBuffer::new(vec![0; 0x2000]); 23 | //! bidi_copy_buf(&mut left, &mut right, buf1, buf2).await; 24 | //! 25 | //! // use custom buffer(pipe) 26 | //! let buf1 = CopyBuffer::new(Pipe::new().unwrap()); 27 | //! let buf2 = CopyBuffer::new(Pipe::new().unwrap()); 28 | //! bidi_copy_buf(&mut left, &mut right, buf1, buf2).await; 29 | //! }; 30 | //! ``` 31 | //! 32 | //! ## About Brutal Shutdown 33 | //! 34 | //! By default, [`bidi_copy_buf`] and other IO functions perform a **graceful shutdown**. 35 | //! 36 | //! With the feature `brutal-shutdown` enabled, these IO functions will decide to 37 | //! perform a **brutal shutdown** once a `FIN` packet reaches, which will forcefully 38 | //! close two connections on both sides without waiting for a reply packet. 39 | //! 40 | //! This is helpful when handling connections from a poorly implemented client or server, 41 | //! which may never shutdown its write side nor close the underlying socket. 42 | //! 43 | 44 | mod buf; 45 | mod mem_copy; 46 | mod bidi_copy; 47 | 48 | pub use buf::{AsyncIOBuf, CopyBuffer}; 49 | pub use bidi_copy::bidi_copy_buf; 50 | pub use mem_copy::{bidi_copy, buf_size, set_buf_size}; 51 | 52 | #[cfg(target_os = "linux")] 53 | mod zero_copy; 54 | 55 | #[cfg(target_os = "linux")] 56 | pub use zero_copy::{Pipe, AsyncRawIO, bidi_zero_copy, pipe_size, set_pipe_size}; 57 | 58 | #[cfg(feature = "peek")] 59 | pub mod peek; 60 | 61 | #[cfg(feature = "statistic")] 62 | pub mod statistic; 63 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | // default logfile 4 | pub const DEFAULT_LOG_FILE: &str = "stdout"; 5 | 6 | // default timeout 7 | pub const TCP_TIMEOUT: usize = 5; 8 | pub const UDP_TIMEOUT: usize = 30; 9 | 10 | // default haproxy proxy-protocol version 11 | pub const PROXY_PROTOCOL_VERSION: usize = 2; 12 | 13 | // default haproxy proxy-protocol version 14 | pub const PROXY_PROTOCOL_TIMEOUT: usize = 5; 15 | 16 | // features 17 | macro_rules! def_feat { 18 | ($fet: ident, $name: expr) => { 19 | pub const $fet: bool = if cfg!(feature = $name) { true } else { false }; 20 | }; 21 | } 22 | 23 | def_feat!(FEATURE_HOOK, "hook"); 24 | def_feat!(FEATURE_PROXY, "proxy"); 25 | def_feat!(FEATURE_BALANCE, "balance"); 26 | def_feat!(FEATURE_MIMALLOC, "mimalloc"); 27 | def_feat!(FEATURE_JEMALLOC, "jemalloc"); 28 | def_feat!(FEATURE_MULTI_THREAD, "multi-thread"); 29 | def_feat!(FEATURE_TRANSPORT, "transport"); 30 | def_feat!(FEATURE_BRUTAL_SHUTDOWN, "brutal-shutdown"); 31 | 32 | pub struct Features { 33 | pub mimalloc: bool, 34 | pub jemalloc: bool, 35 | pub multi_thread: bool, 36 | pub hook: bool, 37 | pub proxy: bool, 38 | pub balance: bool, 39 | pub transport: bool, 40 | pub brutal_shutdown: bool, 41 | } 42 | 43 | pub const FEATURES: Features = Features { 44 | mimalloc: FEATURE_MIMALLOC, 45 | jemalloc: FEATURE_JEMALLOC, 46 | multi_thread: FEATURE_MULTI_THREAD, 47 | hook: FEATURE_HOOK, 48 | proxy: FEATURE_PROXY, 49 | balance: FEATURE_BALANCE, 50 | transport: FEATURE_TRANSPORT, 51 | brutal_shutdown: FEATURE_BRUTAL_SHUTDOWN, 52 | }; 53 | 54 | impl Display for Features { 55 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 56 | macro_rules! disp_feat { 57 | ($field: ident, $show: expr) => { 58 | if self.$field { 59 | write!(f, "[{}]", $show)?; 60 | } 61 | }; 62 | } 63 | 64 | disp_feat!(hook, "hook"); 65 | disp_feat!(proxy, "proxy"); 66 | disp_feat!(balance, "balance"); 67 | disp_feat!(brutal_shutdown, "brutal"); 68 | disp_feat!(transport, "transport"); 69 | disp_feat!(multi_thread, "multi-thread"); 70 | disp_feat!(mimalloc, "mimalloc"); 71 | disp_feat!(jemalloc, "jemalloc"); 72 | Ok(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /realm_core/src/tcp/socket.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, Error, ErrorKind}; 2 | use std::net::SocketAddr; 3 | 4 | use realm_syscall::new_tcp_socket; 5 | use tokio::net::{TcpSocket, TcpStream, TcpListener}; 6 | 7 | use crate::dns::resolve_addr; 8 | use crate::time::timeoutfut; 9 | use crate::endpoint::{RemoteAddr, ConnectOpts}; 10 | 11 | #[allow(clippy::clone_on_copy)] 12 | pub fn bind(laddr: &SocketAddr) -> Result { 13 | let socket = new_tcp_socket(laddr)?; 14 | 15 | // ignore error 16 | let _ = socket.set_reuse_address(true); 17 | 18 | socket.bind(&laddr.clone().into())?; 19 | socket.listen(1024)?; 20 | 21 | TcpListener::from_std(socket.into()) 22 | } 23 | 24 | pub async fn connect(raddr: &RemoteAddr, conn_opts: &ConnectOpts) -> Result { 25 | let ConnectOpts { 26 | connect_timeout, 27 | bind_address, 28 | 29 | #[cfg(target_os = "linux")] 30 | bind_interface, 31 | .. 32 | } = conn_opts; 33 | 34 | let mut last_err = None; 35 | 36 | for addr in resolve_addr(raddr).await?.iter() { 37 | log::debug!("[tcp]{} resolved as {}", raddr, &addr); 38 | 39 | let socket = new_tcp_socket(&addr)?; 40 | 41 | // ignore error 42 | let _ = socket.set_nodelay(true); 43 | let _ = socket.set_reuse_address(true); 44 | 45 | if let Some(addr) = *bind_address { 46 | socket.bind(&addr.into())?; 47 | } 48 | 49 | #[cfg(target_os = "linux")] 50 | if let Some(iface) = bind_interface { 51 | realm_syscall::bind_to_device(&socket, iface)?; 52 | } 53 | 54 | let socket = TcpSocket::from_std_stream(socket.into()); 55 | 56 | match timeoutfut(socket.connect(addr), *connect_timeout).await { 57 | Ok(Ok(stream)) => { 58 | log::debug!("[tcp]connect to {} as {}", raddr, &addr,); 59 | return Ok(stream); 60 | } 61 | Ok(Err(e)) => { 62 | log::warn!("[tcp]connect to {} as {}: {}, try next ip", raddr, &addr, &e); 63 | last_err = Some(e); 64 | } 65 | Err(_) => log::warn!("[tcp]connect to {} as {} timeout, try next ip", raddr, &addr), 66 | } 67 | } 68 | 69 | Err(last_err.unwrap_or_else(|| Error::new(ErrorKind::InvalidInput, "could not connect to any address"))) 70 | } 71 | -------------------------------------------------------------------------------- /realm_io/src/statistic.rs: -------------------------------------------------------------------------------- 1 | //! Statistic impl. 2 | 3 | use std::ops::AddAssign; 4 | use std::io::{Result, IoSlice}; 5 | use std::pin::Pin; 6 | use std::task::{Poll, Context}; 7 | 8 | use tokio::io::{ReadBuf, AsyncRead, AsyncWrite}; 9 | 10 | /// A wrapper to count written bytes. 11 | pub struct StatStream { 12 | pub io: T, 13 | pub stat: U, 14 | } 15 | 16 | impl StatStream { 17 | pub const fn new(io: T, stat: U) -> Self { 18 | Self { io, stat } 19 | } 20 | } 21 | 22 | impl AsyncRead for StatStream 23 | where 24 | T: AsyncRead + Unpin, 25 | U: Unpin, 26 | { 27 | fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { 28 | Pin::new(&mut self.get_mut().io).poll_read(cx, buf) 29 | } 30 | } 31 | 32 | impl AsyncWrite for StatStream 33 | where 34 | T: AsyncWrite + Unpin, 35 | U: AddAssign + Unpin, 36 | { 37 | #[inline] 38 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 39 | let this = self.get_mut(); 40 | 41 | match Pin::new(&mut this.io).poll_write(cx, buf) { 42 | Poll::Ready(Ok(n)) => { 43 | this.stat += n; 44 | Poll::Ready(Ok(n)) 45 | } 46 | Poll::Ready(Err(e)) => Poll::Ready(Err(e)), 47 | Poll::Pending => Poll::Pending, 48 | } 49 | } 50 | 51 | #[inline] 52 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 53 | Pin::new(&mut self.get_mut().io).poll_flush(cx) 54 | } 55 | 56 | #[inline] 57 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 58 | Pin::new(&mut self.get_mut().io).poll_shutdown(cx) 59 | } 60 | 61 | #[inline] 62 | fn is_write_vectored(&self) -> bool { 63 | self.io.is_write_vectored() 64 | } 65 | 66 | #[inline] 67 | fn poll_write_vectored(self: Pin<&mut Self>, cx: &mut Context<'_>, iovec: &[IoSlice<'_>]) -> Poll> { 68 | let this = self.get_mut(); 69 | 70 | match Pin::new(&mut this.io).poll_write_vectored(cx, iovec) { 71 | Poll::Ready(Ok(n)) => { 72 | this.stat += n; 73 | Poll::Ready(Ok(n)) 74 | } 75 | Poll::Ready(Err(e)) => Poll::Ready(Err(e)), 76 | Poll::Pending => Poll::Pending, 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /realm_core/tests/proxy_v1.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::time::Duration; 3 | 4 | use tokio::net::{TcpStream, TcpListener}; 5 | use tokio::time::sleep; 6 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 7 | 8 | use realm_core::tcp::run_tcp; 9 | use realm_core::endpoint::{Endpoint, RemoteAddr, ConnectOpts, ProxyOpts}; 10 | 11 | #[tokio::test] 12 | #[cfg(feature = "proxy")] 13 | async fn proxy_v1() { 14 | env_logger::init(); 15 | 16 | let endpoint1 = Endpoint { 17 | laddr: "127.0.0.1:10000".parse().unwrap(), 18 | raddr: "127.0.0.1:15000" 19 | .parse::() 20 | .map(RemoteAddr::SocketAddr) 21 | .unwrap(), 22 | conn_opts: ConnectOpts { 23 | proxy_opts: ProxyOpts { 24 | send_proxy: true, 25 | send_proxy_version: 1, 26 | ..Default::default() 27 | }, 28 | ..Default::default() 29 | }, 30 | extra_raddrs: Vec::new(), 31 | }; 32 | 33 | let endpoint2 = Endpoint { 34 | laddr: "127.0.0.1:15000".parse().unwrap(), 35 | raddr: "127.0.0.1:20000" 36 | .parse::() 37 | .map(RemoteAddr::SocketAddr) 38 | .unwrap(), 39 | conn_opts: ConnectOpts { 40 | proxy_opts: ProxyOpts { 41 | accept_proxy: true, 42 | accept_proxy_timeout: 5, 43 | ..Default::default() 44 | }, 45 | ..Default::default() 46 | }, 47 | extra_raddrs: Vec::new(), 48 | }; 49 | 50 | tokio::spawn(run_tcp(endpoint1)); 51 | tokio::spawn(run_tcp(endpoint2)); 52 | 53 | let task1 = async { 54 | sleep(Duration::from_millis(500)).await; 55 | 56 | let mut stream = TcpStream::connect("127.0.0.1:10000").await.unwrap(); 57 | 58 | let mut buf = vec![0; 32]; 59 | 60 | for _ in 0..20 { 61 | stream.write(b"Ping Ping Ping").await.unwrap(); 62 | let n = stream.read(&mut buf).await.unwrap(); 63 | log::debug!("a got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 64 | assert_eq!(b"Pong Pong Pong", &buf[..n]); 65 | } 66 | }; 67 | 68 | let task2 = async { 69 | let lis = TcpListener::bind("127.0.0.1:20000").await.unwrap(); 70 | let (mut stream, _) = lis.accept().await.unwrap(); 71 | 72 | let mut buf = vec![0; 32]; 73 | 74 | for _ in 0..20 { 75 | let n = stream.read(&mut buf).await.unwrap(); 76 | log::debug!("b got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 77 | assert_eq!(b"Ping Ping Ping", &buf[..n]); 78 | stream.write(b"Pong Pong Pong").await.unwrap(); 79 | } 80 | }; 81 | 82 | tokio::join!(task1, task2); 83 | } 84 | -------------------------------------------------------------------------------- /realm_core/tests/proxy_v2.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::time::Duration; 3 | 4 | use tokio::net::{TcpStream, TcpListener}; 5 | use tokio::time::sleep; 6 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 7 | 8 | use realm_core::tcp::run_tcp; 9 | use realm_core::endpoint::{Endpoint, RemoteAddr, ConnectOpts, ProxyOpts}; 10 | 11 | #[tokio::test] 12 | #[cfg(feature = "proxy")] 13 | async fn proxy_v2() { 14 | env_logger::init(); 15 | 16 | let endpoint1 = Endpoint { 17 | laddr: "127.0.0.1:10000".parse().unwrap(), 18 | raddr: "127.0.0.1:15000" 19 | .parse::() 20 | .map(RemoteAddr::SocketAddr) 21 | .unwrap(), 22 | conn_opts: ConnectOpts { 23 | proxy_opts: ProxyOpts { 24 | send_proxy: true, 25 | send_proxy_version: 2, 26 | ..Default::default() 27 | }, 28 | ..Default::default() 29 | }, 30 | extra_raddrs: Vec::new(), 31 | }; 32 | 33 | let endpoint2 = Endpoint { 34 | laddr: "127.0.0.1:15000".parse().unwrap(), 35 | raddr: "127.0.0.1:20000" 36 | .parse::() 37 | .map(RemoteAddr::SocketAddr) 38 | .unwrap(), 39 | conn_opts: ConnectOpts { 40 | proxy_opts: ProxyOpts { 41 | accept_proxy: true, 42 | accept_proxy_timeout: 5, 43 | ..Default::default() 44 | }, 45 | ..Default::default() 46 | }, 47 | extra_raddrs: Vec::new(), 48 | }; 49 | 50 | tokio::spawn(run_tcp(endpoint1)); 51 | tokio::spawn(run_tcp(endpoint2)); 52 | 53 | let task1 = async { 54 | sleep(Duration::from_millis(500)).await; 55 | 56 | let mut stream = TcpStream::connect("127.0.0.1:10000").await.unwrap(); 57 | 58 | let mut buf = vec![0; 32]; 59 | 60 | for _ in 0..20 { 61 | stream.write(b"Ping Ping Ping").await.unwrap(); 62 | let n = stream.read(&mut buf).await.unwrap(); 63 | log::debug!("a got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 64 | assert_eq!(b"Pong Pong Pong", &buf[..n]); 65 | } 66 | }; 67 | 68 | let task2 = async { 69 | let lis = TcpListener::bind("127.0.0.1:20000").await.unwrap(); 70 | let (mut stream, _) = lis.accept().await.unwrap(); 71 | 72 | let mut buf = vec![0; 32]; 73 | 74 | for _ in 0..20 { 75 | let n = stream.read(&mut buf).await.unwrap(); 76 | log::debug!("b got: {:?}", std::str::from_utf8(&buf[..n]).unwrap()); 77 | assert_eq!(b"Ping Ping Ping", &buf[..n]); 78 | stream.write(b"Pong Pong Pong").await.unwrap(); 79 | } 80 | }; 81 | 82 | tokio::join!(task1, task2); 83 | } 84 | -------------------------------------------------------------------------------- /realm_core/src/udp/middle.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::net::SocketAddr; 3 | use std::sync::Arc; 4 | 5 | use tokio::net::UdpSocket; 6 | 7 | use super::SockMap; 8 | use super::BUF_SIZE; 9 | use super::socket; 10 | 11 | use crate::trick::Ref; 12 | use crate::time::timeoutfut; 13 | use crate::dns::resolve_addr; 14 | use crate::endpoint::{RemoteAddr, ConnectOpts}; 15 | 16 | pub async fn associate_and_relay( 17 | lis: &UdpSocket, 18 | raddr: &RemoteAddr, 19 | conn_opts: &ConnectOpts, 20 | sockmap: &SockMap, 21 | ) -> Result<()> { 22 | let mut buf = vec![0u8; BUF_SIZE]; 23 | let associate_timeout = conn_opts.associate_timeout; 24 | 25 | loop { 26 | let (n, laddr) = lis.recv_from(&mut buf).await?; 27 | log::debug!("[udp]recvfrom client {}", &laddr); 28 | 29 | let addr = resolve_addr(raddr).await?.iter().next().unwrap(); 30 | log::debug!("[udp]{} resolved as {}", raddr, &addr); 31 | 32 | // get the socket associated with a unique client 33 | let remote = match sockmap.find(&laddr) { 34 | Some(x) => x, 35 | None => { 36 | log::info!("[udp]new association {} => {} as {}", &laddr, raddr, &addr); 37 | 38 | let remote = Arc::new(socket::associate(&addr, conn_opts).await?); 39 | 40 | sockmap.insert(laddr, remote.clone()); 41 | 42 | // spawn sending back task 43 | tokio::spawn(send_back( 44 | Ref::new(lis), 45 | laddr, 46 | remote.clone(), 47 | Ref::new(sockmap), 48 | associate_timeout, 49 | )); 50 | 51 | remote 52 | } 53 | }; 54 | 55 | remote.send_to(&buf[..n], &addr).await?; 56 | } 57 | } 58 | 59 | async fn send_back( 60 | lis: Ref, 61 | laddr: SocketAddr, 62 | remote: Arc, 63 | sockmap: Ref, 64 | associate_timeout: usize, 65 | ) { 66 | let mut buf = vec![0u8; BUF_SIZE]; 67 | 68 | loop { 69 | let res = match timeoutfut(remote.recv_from(&mut buf), associate_timeout).await { 70 | Ok(x) => x, 71 | Err(_) => { 72 | log::debug!("[udp]association for {} timeout", &laddr); 73 | break; 74 | } 75 | }; 76 | 77 | let (n, raddr) = match res { 78 | Ok(x) => x, 79 | Err(e) => { 80 | log::error!("[udp]failed to recvfrom remote: {}", e); 81 | continue; 82 | } 83 | }; 84 | 85 | log::debug!("[udp]recvfrom remote {}", &raddr); 86 | 87 | if let Err(e) = lis.send_to(&buf[..n], &laddr).await { 88 | log::error!("[udp]failed to sendto client{}: {}", &laddr, e); 89 | continue; 90 | } 91 | } 92 | 93 | sockmap.remove(&laddr); 94 | log::debug!("[udp]remove association for {}", &laddr); 95 | } 96 | -------------------------------------------------------------------------------- /.github/workflows/container-image.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | tag: 5 | description: 'Tag of image' 6 | required: true 7 | default: 'test' 8 | push: 9 | tags: 10 | - v* 11 | 12 | name: container 13 | 14 | jobs: 15 | build: 16 | name: Build Rust project 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Install latest nightly 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: nightly 25 | target: x86_64-unknown-linux-musl 26 | override: true 27 | components: rustfmt, clippy 28 | 29 | - name: Cache cargo dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: | 33 | ~/.cargo/bin/ 34 | ~/.cargo/registry/index/ 35 | ~/.cargo/registry/cache/ 36 | ~/.cargo/git/db/ 37 | target/ 38 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 39 | 40 | - name: Run cargo build 41 | uses: actions-rs/cargo@v1 42 | with: 43 | use-cross: true 44 | command: build 45 | args: --release --target x86_64-unknown-linux-musl 46 | 47 | - uses: actions/upload-artifact@v2 48 | with: 49 | name: realm 50 | path: target/x86_64-unknown-linux-musl/release/realm 51 | 52 | build-and-push: 53 | name: Build and push image 54 | needs: build 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | - name: Create release tmp folder 59 | run: mkdir -p target/x86_64-unknown-linux-musl/release 60 | - uses: actions/download-artifact@v2 61 | with: 62 | name: realm 63 | path: target/x86_64-unknown-linux-musl/release/realm 64 | - name: Set up Docker Buildx 65 | id: buildx 66 | uses: docker/setup-buildx-action@v1 67 | with: 68 | driver-opts: network=host 69 | 70 | - name: Set env 71 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 72 | - name: Echo release version 73 | run: echo ${{ env.RELEASE_VERSION }} 74 | 75 | - name: Log in to the Container registry 76 | uses: docker/login-action@v1 77 | with: 78 | registry: ghcr.io 79 | username: ${{ github.actor }} 80 | password: ${{ secrets.GITHUB_TOKEN }} 81 | - name: Build and push images with latest(debian) 82 | uses: docker/build-push-action@v2 83 | with: 84 | context: . 85 | file: ./Dockerfile 86 | push: true 87 | tags: ghcr.io/${{ github.repository }}:latest, ghcr.io/${{ github.repository }}:${{ github.event.inputs.tag || env.RELEASE_VERSION }} 88 | 89 | - name: Build and push images with alpine 90 | uses: docker/build-push-action@v2 91 | with: 92 | context: . 93 | file: ./Dockerfile.alpine 94 | push: true 95 | tags: ghcr.io/${{ github.repository }}:alpine, ghcr.io/${{ github.repository }}:${{ github.event.inputs.tag || env.RELEASE_VERSION }}-alpine 96 | 97 | -------------------------------------------------------------------------------- /realm_core/src/tcp/middle.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | 3 | use tokio::net::TcpStream; 4 | 5 | use super::socket; 6 | use super::plain; 7 | 8 | #[cfg(feature = "hook")] 9 | use super::hook; 10 | 11 | #[cfg(feature = "proxy")] 12 | use super::proxy; 13 | 14 | #[cfg(feature = "transport")] 15 | use super::transport; 16 | 17 | use crate::trick::Ref; 18 | use crate::endpoint::{RemoteAddr, ConnectOpts}; 19 | 20 | #[allow(unused)] 21 | pub async fn connect_and_relay( 22 | mut local: TcpStream, 23 | raddr: Ref, 24 | conn_opts: Ref, 25 | extra_raddrs: Ref>, 26 | ) -> Result<()> { 27 | let ConnectOpts { 28 | #[cfg(feature = "proxy-protocol")] 29 | proxy_opts, 30 | 31 | #[cfg(feature = "transport")] 32 | transport, 33 | 34 | #[cfg(feature = "balance")] 35 | balancer, 36 | .. 37 | } = conn_opts.as_ref(); 38 | 39 | // before connect: 40 | // - pre-connect hook 41 | // - load balance 42 | // .. 43 | let raddr = { 44 | #[cfg(feature = "hook")] 45 | { 46 | // accept or deny connection. 47 | #[cfg(feature = "balance")] 48 | { 49 | hook::pre_connect_hook(&mut local, raddr.as_ref(), extra_raddrs.as_ref()).await?; 50 | } 51 | 52 | // accept or deny connection, or select a remote peer. 53 | #[cfg(not(feature = "balance"))] 54 | { 55 | hook::pre_connect_hook(&mut local, raddr.as_ref(), extra_raddrs.as_ref()).await? 56 | } 57 | } 58 | 59 | #[cfg(feature = "balance")] 60 | { 61 | use realm_lb::{Token, BalanceCtx}; 62 | let token = balancer.next(BalanceCtx { 63 | src_ip: &local.peer_addr()?.ip(), 64 | }); 65 | log::debug!("[tcp]select remote peer, token: {:?}", token); 66 | match token { 67 | None | Some(Token(0)) => raddr.as_ref(), 68 | Some(Token(idx)) => &extra_raddrs.as_ref()[idx as usize - 1], 69 | } 70 | } 71 | 72 | #[cfg(not(any(feature = "hook", feature = "balance")))] 73 | raddr.as_ref() 74 | }; 75 | 76 | // connect! 77 | let mut remote = socket::connect(raddr, conn_opts.as_ref()).await?; 78 | log::info!("[tcp]{} => {} as {}", local.peer_addr()?, raddr, remote.peer_addr()?); 79 | 80 | // after connected 81 | // .. 82 | #[cfg(feature = "proxy")] 83 | if proxy_opts.enabled() { 84 | proxy::handle_proxy(&mut local, &mut remote, *proxy_opts).await?; 85 | } 86 | 87 | // relay 88 | let res = { 89 | #[cfg(feature = "transport")] 90 | { 91 | if let Some((ac, cc)) = transport { 92 | transport::run_relay(local, remote, ac, cc).await 93 | } else { 94 | plain::run_relay(local, remote).await 95 | } 96 | } 97 | #[cfg(not(feature = "transport"))] 98 | { 99 | plain::run_relay(local, remote).await 100 | } 101 | }; 102 | 103 | // ignore relay error 104 | if let Err(e) = res { 105 | log::debug!("[tcp]forward error: {}, ignored", e); 106 | } 107 | 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /realm_core/src/dns/mod.rs: -------------------------------------------------------------------------------- 1 | //! Global dns resolver. 2 | 3 | use std::io::{Result, Error, ErrorKind}; 4 | use std::net::SocketAddr; 5 | 6 | use trust_dns_resolver as resolver; 7 | use resolver::TokioAsyncResolver; 8 | use resolver::system_conf::read_system_conf; 9 | use resolver::lookup_ip::{LookupIp, LookupIpIter}; 10 | pub use resolver::config; 11 | use config::{ResolverOpts, ResolverConfig}; 12 | 13 | use once_cell::unsync::{OnceCell, Lazy}; 14 | 15 | use crate::endpoint::RemoteAddr; 16 | 17 | /// Dns config. 18 | #[derive(Debug, Clone)] 19 | pub struct DnsConf { 20 | pub conf: ResolverConfig, 21 | pub opts: ResolverOpts, 22 | } 23 | 24 | /// Use system config on unix(except android) or windows, 25 | /// otherwise use google's public dns servers. 26 | impl Default for DnsConf { 27 | fn default() -> Self { 28 | #[cfg(any(all(unix, not(target_os = "android")), windows))] 29 | let (conf, opts) = read_system_conf().unwrap(); 30 | 31 | #[cfg(not(any(all(unix, not(target_os = "android")), windows)))] 32 | let (conf, opts) = Default::default(); 33 | 34 | Self { conf, opts } 35 | } 36 | } 37 | 38 | static mut DNS_CONF: OnceCell = OnceCell::new(); 39 | 40 | static mut DNS: Lazy = Lazy::new(|| { 41 | let DnsConf { conf, opts } = unsafe { DNS_CONF.take().unwrap() }; 42 | TokioAsyncResolver::tokio(conf, opts).unwrap() 43 | }); 44 | 45 | /// Setup global dns resolver. This is not thread-safe! 46 | pub fn build(conf: Option, opts: Option) { 47 | let mut dns_conf = DnsConf::default(); 48 | 49 | if let Some(conf) = conf { 50 | dns_conf.conf = conf; 51 | } 52 | 53 | if let Some(opts) = opts { 54 | dns_conf.opts = opts; 55 | } 56 | 57 | unsafe { 58 | DNS_CONF.set(dns_conf).unwrap(); 59 | Lazy::force(&DNS); 60 | } 61 | } 62 | 63 | /// Lookup ip with global dns resolver. 64 | pub async fn resolve_ip(ip: &str) -> Result { 65 | unsafe { 66 | DNS.lookup_ip(ip) 67 | .await 68 | .map_or_else(|e| Err(Error::new(ErrorKind::Other, e)), Ok) 69 | } 70 | } 71 | 72 | /// Lookup socketaddr with global dns resolver. 73 | pub async fn resolve_addr(addr: &RemoteAddr) -> Result> { 74 | use RemoteAddr::*; 75 | use LookupRemoteAddr::*; 76 | match addr { 77 | SocketAddr(addr) => Ok(NoLookup(addr)), 78 | DomainName(ip, port) => resolve_ip(ip).await.map(|ip| Dolookup(ip, *port)), 79 | } 80 | } 81 | 82 | /// Resolved result. 83 | pub enum LookupRemoteAddr<'a> { 84 | NoLookup(&'a SocketAddr), 85 | Dolookup(LookupIp, u16), 86 | } 87 | 88 | impl LookupRemoteAddr<'_> { 89 | /// Get view of resolved result. 90 | pub fn iter(&self) -> LookupRemoteAddrIter { 91 | use LookupRemoteAddr::*; 92 | match self { 93 | NoLookup(addr) => LookupRemoteAddrIter::NoLookup(std::iter::once(addr)), 94 | Dolookup(ip, port) => LookupRemoteAddrIter::DoLookup(ip.iter(), *port), 95 | } 96 | } 97 | } 98 | 99 | /// View of resolved result. 100 | pub enum LookupRemoteAddrIter<'a> { 101 | NoLookup(std::iter::Once<&'a SocketAddr>), 102 | DoLookup(LookupIpIter<'a>, u16), 103 | } 104 | 105 | impl Iterator for LookupRemoteAddrIter<'_> { 106 | type Item = SocketAddr; 107 | 108 | fn next(&mut self) -> Option { 109 | use LookupRemoteAddrIter::*; 110 | match self { 111 | NoLookup(addr) => addr.next().copied(), 112 | DoLookup(ip, port) => ip.next().map(|ip| SocketAddr::new(ip, *port)), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /realm_syscall/src/socket.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::net::SocketAddr; 3 | use socket2::{Socket, Domain, Type}; 4 | 5 | /// Create a new non-blocking socket. 6 | /// 7 | /// On unix-like platforms, [`SOCK_NONBLOCK`](libc::SOCK_NONBLOCK) and 8 | /// [`SOCK_CLOEXEC`](libc::SOCK_CLOEXEC) are assigned 9 | /// when creating a new socket, which saves a [`fcntl`](libc::fcntl) syscall. 10 | /// 11 | /// On other platforms, a socket is created without extra flags 12 | /// then set to `non_blocking`. 13 | #[cfg(any( 14 | target_os = "android", 15 | target_os = "dragonfly", 16 | target_os = "freebsd", 17 | target_os = "illumos", 18 | target_os = "linux", 19 | target_os = "netbsd", 20 | target_os = "openbsd" 21 | ))] 22 | #[inline] 23 | pub fn new_socket(domain: Domain, ty: Type) -> Result { 24 | use std::os::unix::prelude::FromRawFd; 25 | use libc::{SOCK_NONBLOCK, SOCK_CLOEXEC}; 26 | 27 | let fd = unsafe { libc::socket(domain.into(), libc::c_int::from(ty) | SOCK_NONBLOCK | SOCK_CLOEXEC, 0) }; 28 | 29 | if fd < 0 { 30 | Err(std::io::Error::last_os_error()) 31 | } else { 32 | Ok(unsafe { Socket::from_raw_fd(fd) }) 33 | } 34 | } 35 | 36 | /// Create a new non-blocking socket. 37 | #[cfg(not(any( 38 | target_os = "android", 39 | target_os = "dragonfly", 40 | target_os = "freebsd", 41 | target_os = "illumos", 42 | target_os = "linux", 43 | target_os = "netbsd", 44 | target_os = "openbsd" 45 | )))] 46 | #[inline] 47 | pub fn new_socket(domain: Domain, ty: Type) -> Result { 48 | let socket = Socket::new(domain, ty, None)?; 49 | socket.set_nonblocking(true)?; 50 | Ok(socket) 51 | } 52 | 53 | /// Create a new non-blocking TCP socket. 54 | /// 55 | /// On unix-like platforms, [`SOCK_NONBLOCK`](libc::SOCK_NONBLOCK) and 56 | /// [`SOCK_CLOEXEC`](libc::SOCK_CLOEXEC) are assigned 57 | /// when creating a new socket, which saves a [`fcntl`](libc::fcntl) syscall. 58 | /// 59 | /// On other platforms, a socket is created without extra flags 60 | /// then set to `non_blocking`. 61 | #[inline] 62 | pub fn new_tcp_socket(addr: &SocketAddr) -> Result { 63 | let domain = match addr { 64 | SocketAddr::V4(..) => Domain::IPV4, 65 | SocketAddr::V6(..) => Domain::IPV6, 66 | }; 67 | new_socket(domain, Type::STREAM) 68 | } 69 | 70 | /// Create a new non-blocking UDP socket. 71 | /// 72 | /// On unix-like platforms, [`SOCK_NONBLOCK`](libc::SOCK_NONBLOCK) and 73 | /// [`SOCK_CLOEXEC`](libc::SOCK_CLOEXEC) are assigned 74 | /// when creating a new socket, which saves a [`fcntl`](libc::fcntl) syscall. 75 | /// 76 | /// On other platforms, a socket is created without extra flags 77 | /// then set to `non_blocking`. 78 | #[inline] 79 | pub fn new_udp_socket(addr: &SocketAddr) -> Result { 80 | let domain = match addr { 81 | SocketAddr::V4(..) => Domain::IPV4, 82 | SocketAddr::V6(..) => Domain::IPV6, 83 | }; 84 | new_socket(domain, Type::DGRAM) 85 | } 86 | 87 | /// Bind a socket to a specific network interface. 88 | /// 89 | /// It seems `SO_BINDTODEVICE` is not supported on BSDs, we should use `IP_SENDIF` instead. 90 | /// 91 | /// Reference: 92 | /// - [shadowsocks-rust](https://docs.rs/shadowsocks/1.13.1/src/shadowsocks/net/sys/unix/linux/mod.rs.html#256-276). 93 | /// - [freebsd](https://lists.freebsd.org/pipermail/freebsd-net/2012-April/032064.html). 94 | #[cfg(target_os = "linux")] 95 | pub fn bind_to_device(socket: &T, iface: &str) -> std::io::Result<()> { 96 | let iface_bytes = iface.as_bytes(); 97 | 98 | if unsafe { 99 | libc::setsockopt( 100 | socket.as_raw_fd(), 101 | libc::SOL_SOCKET, 102 | libc::SO_BINDTODEVICE, 103 | iface_bytes.as_ptr() as *const _ as *const libc::c_void, 104 | iface_bytes.len() as libc::socklen_t, 105 | ) 106 | } < 0 107 | { 108 | Err(std::io::Error::last_os_error()) 109 | } else { 110 | Ok(()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.github/workflows/cross_compile.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build-corss: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | include: 17 | - target: x86_64-unknown-linux-gnu 18 | toolchain: nightly 19 | output: realm 20 | - target: x86_64-unknown-linux-musl 21 | toolchain: nightly 22 | output: realm 23 | - target: x86_64-linux-android 24 | toolchain: nightly-2022-09-04 25 | output: realm 26 | - target: x86_64-pc-windows-gnu 27 | toolchain: nightly 28 | output: realm.exe 29 | - target: aarch64-unknown-linux-gnu 30 | toolchain: nightly 31 | output: realm 32 | - target: aarch64-unknown-linux-musl 33 | toolchain: nightly 34 | output: realm 35 | - target: aarch64-linux-android 36 | toolchain: nightly-2022-09-04 37 | output: realm 38 | - target: arm-unknown-linux-gnueabi 39 | toolchain: nightly 40 | output: realm 41 | - target: arm-unknown-linux-gnueabihf 42 | toolchain: nightly 43 | output: realm 44 | - target: arm-unknown-linux-musleabihf 45 | toolchain: nightly 46 | output: realm 47 | - target: armv7-unknown-linux-gnueabihf 48 | toolchain: nightly 49 | output: realm 50 | - target: armv7-unknown-linux-musleabihf 51 | toolchain: nightly 52 | output: realm 53 | steps: 54 | - uses: actions/checkout@v2 55 | - name: install toolchain 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | toolchain: ${{ matrix.toolchain }} 59 | target: ${{ matrix.target }} 60 | override: true 61 | - name: compile 62 | uses: actions-rs/cargo@v1 63 | with: 64 | use-cross: true 65 | command: build 66 | args: --release --target=${{ matrix.target }} 67 | - name: upload 68 | uses: actions/upload-artifact@v2 69 | with: 70 | name: realm-${{ matrix.target }} 71 | path: target/${{ matrix.target }}/release/${{ matrix.output }} 72 | build-windows: 73 | runs-on: windows-latest 74 | strategy: 75 | matrix: 76 | target: 77 | - x86_64-pc-windows-msvc 78 | steps: 79 | - uses: actions/checkout@v2 80 | - name: install toolchain 81 | uses: actions-rs/toolchain@v1 82 | with: 83 | toolchain: nightly 84 | target: ${{ matrix.target }} 85 | override: true 86 | - name: compile 87 | uses: actions-rs/cargo@v1 88 | with: 89 | use-cross: false 90 | command: build 91 | args: --release --target=${{ matrix.target }} 92 | - name: upload 93 | uses: actions/upload-artifact@v2 94 | with: 95 | name: realm-${{ matrix.target }} 96 | path: target/${{ matrix.target }}/release/realm.exe 97 | build-apple: 98 | runs-on: macos-latest 99 | strategy: 100 | matrix: 101 | target: 102 | - x86_64-apple-darwin 103 | - aarch64-apple-darwin 104 | - aarch64-apple-ios 105 | steps: 106 | - uses: actions/checkout@v2 107 | - name: install toolchain 108 | uses: actions-rs/toolchain@v1 109 | with: 110 | toolchain: nightly 111 | target: ${{ matrix.target }} 112 | override: true 113 | - name: compile 114 | uses: actions-rs/cargo@v1 115 | with: 116 | use-cross: true 117 | command: build 118 | args: --release --target=${{ matrix.target }} 119 | - name: upload 120 | uses: actions/upload-artifact@v2 121 | with: 122 | name: realm-${{ matrix.target }} 123 | path: target/${{ matrix.target }}/release/realm 124 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{Command, ArgMatches}; 2 | 3 | use realm_core::realm_io; 4 | use realm_core::realm_syscall; 5 | 6 | use crate::conf::CmdOverride; 7 | use crate::conf::EndpointConf; 8 | use crate::conf::{Config, LogConf, DnsConf, NetConf}; 9 | 10 | use crate::VERSION; 11 | use crate::consts::FEATURES; 12 | 13 | mod sub; 14 | mod flag; 15 | 16 | #[allow(clippy::large_enum_variant)] 17 | pub enum CmdInput { 18 | Config(String, CmdOverride), 19 | Endpoint(EndpointConf, CmdOverride), 20 | None, 21 | } 22 | 23 | pub fn scan() -> CmdInput { 24 | let version = format!("{} {}", VERSION, FEATURES); 25 | let app = Command::new("Realm") 26 | .about("A high efficiency relay tool") 27 | .version(version.as_str()); 28 | 29 | let app = app 30 | .disable_help_flag(true) 31 | .disable_help_subcommand(true) 32 | .disable_version_flag(true) 33 | .arg_required_else_help(true) 34 | .override_usage("realm [FLAGS] [OPTIONS]"); 35 | 36 | let app = flag::add_all(app); 37 | let app = sub::add_all(app); 38 | 39 | // do other things 40 | let mut app2 = app.clone(); 41 | let matches = app.get_matches(); 42 | 43 | if matches.is_present("help") { 44 | app2.print_help().unwrap(); 45 | return CmdInput::None; 46 | } 47 | 48 | if matches.is_present("version") { 49 | print!("{}", app2.render_version()); 50 | return CmdInput::None; 51 | } 52 | 53 | #[allow(clippy::single_match)] 54 | match matches.subcommand() { 55 | Some(("convert", sub_matches)) => { 56 | sub::handle_convert(sub_matches); 57 | return CmdInput::None; 58 | } 59 | _ => {} 60 | }; 61 | 62 | // start 63 | handle_matches(matches) 64 | } 65 | 66 | fn handle_matches(matches: ArgMatches) -> CmdInput { 67 | #[cfg(unix)] 68 | if matches.is_present("daemon") { 69 | realm_syscall::daemonize("realm is running in the background"); 70 | } 71 | 72 | #[cfg(all(unix, not(target_os = "android")))] 73 | { 74 | use realm_syscall::get_nofile_limit; 75 | use realm_syscall::set_nofile_limit; 76 | use realm_syscall::bump_nofile_limit; 77 | 78 | // set 79 | if let Some(nofile) = matches.value_of("nofile") { 80 | if let Ok(nofile) = nofile.parse::() { 81 | let _ = set_nofile_limit(nofile); 82 | } else { 83 | eprintln!("invalid nofile value: {}", nofile); 84 | } 85 | } else { 86 | let _ = bump_nofile_limit(); 87 | } 88 | 89 | // get 90 | if let Ok((soft, hard)) = get_nofile_limit() { 91 | println!("fd: soft={}, hard={}", soft, hard); 92 | } 93 | } 94 | 95 | #[cfg(all(target_os = "linux"))] 96 | { 97 | use realm_io::set_pipe_size; 98 | 99 | if let Some(page) = matches.value_of("pipe_page") { 100 | if let Ok(page) = page.parse::() { 101 | set_pipe_size(page * 0x1000); 102 | println!("pipe capacity: {}", page * 0x1000); 103 | } 104 | } 105 | } 106 | 107 | #[cfg(feature = "hook")] 108 | { 109 | use realm_core::hook::pre_conn::load_dylib as load_pre_conn; 110 | if let Some(path) = matches.value_of("pre_conn_hook") { 111 | load_pre_conn(path); 112 | println!("hook: {}", path); 113 | } 114 | } 115 | 116 | let opts = parse_global_opts(&matches); 117 | 118 | if let Some(config) = matches.value_of("config") { 119 | return CmdInput::Config(String::from(config), opts); 120 | } 121 | 122 | if matches.value_of("local").is_some() && matches.value_of("remote").is_some() { 123 | let ep = EndpointConf::from_cmd_args(&matches); 124 | return CmdInput::Endpoint(ep, opts); 125 | } 126 | 127 | CmdInput::None 128 | } 129 | 130 | fn parse_global_opts(matches: &ArgMatches) -> CmdOverride { 131 | let log = LogConf::from_cmd_args(matches); 132 | let dns = DnsConf::from_cmd_args(matches); 133 | let network = NetConf::from_cmd_args(matches); 134 | CmdOverride { log, dns, network } 135 | } 136 | -------------------------------------------------------------------------------- /realm_core/src/endpoint.rs: -------------------------------------------------------------------------------- 1 | //! Relay endpoint. 2 | 3 | use std::fmt::{Display, Formatter}; 4 | use std::net::SocketAddr; 5 | 6 | #[cfg(feature = "transport")] 7 | use kaminari::mix::{MixAccept, MixConnect}; 8 | 9 | #[cfg(feature = "balance")] 10 | use realm_lb::Balancer; 11 | 12 | /// Remote address. 13 | #[derive(Debug, Clone, PartialEq, Eq)] 14 | pub enum RemoteAddr { 15 | SocketAddr(SocketAddr), 16 | DomainName(String, u16), 17 | } 18 | 19 | /// Proxy protocol options. 20 | #[cfg(feature = "proxy")] 21 | #[derive(Debug, Default, Clone, Copy)] 22 | pub struct ProxyOpts { 23 | pub send_proxy: bool, 24 | pub accept_proxy: bool, 25 | pub send_proxy_version: usize, 26 | pub accept_proxy_timeout: usize, 27 | } 28 | 29 | #[cfg(feature = "proxy")] 30 | impl ProxyOpts { 31 | #[inline] 32 | pub(crate) const fn enabled(&self) -> bool { 33 | self.send_proxy || self.accept_proxy 34 | } 35 | } 36 | 37 | /// Connect or associate options. 38 | #[derive(Debug, Default, Clone)] 39 | pub struct ConnectOpts { 40 | pub connect_timeout: usize, 41 | pub associate_timeout: usize, 42 | pub bind_address: Option, 43 | pub bind_interface: Option, 44 | 45 | #[cfg(feature = "proxy")] 46 | pub proxy_opts: ProxyOpts, 47 | 48 | #[cfg(feature = "transport")] 49 | pub transport: Option<(MixAccept, MixConnect)>, 50 | 51 | #[cfg(feature = "balance")] 52 | pub balancer: Balancer, 53 | } 54 | 55 | /// Relay endpoint. 56 | #[derive(Debug, Clone)] 57 | pub struct Endpoint { 58 | pub laddr: SocketAddr, 59 | pub raddr: RemoteAddr, 60 | pub conn_opts: ConnectOpts, 61 | pub extra_raddrs: Vec, 62 | } 63 | 64 | // display impl below 65 | 66 | impl Display for RemoteAddr { 67 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 68 | use RemoteAddr::*; 69 | match self { 70 | SocketAddr(addr) => write!(f, "{}", addr), 71 | DomainName(host, port) => write!(f, "{}:{}", host, port), 72 | } 73 | } 74 | } 75 | 76 | impl Display for Endpoint { 77 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 78 | write!(f, "{} -> [{}", &self.laddr, &self.raddr)?; 79 | for raddr in self.extra_raddrs.iter() { 80 | write!(f, "|{}", raddr)?; 81 | } 82 | write!(f, "]; options: {}", &self.conn_opts) 83 | } 84 | } 85 | 86 | impl Display for ConnectOpts { 87 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 88 | let ConnectOpts { 89 | connect_timeout, 90 | associate_timeout, 91 | bind_address, 92 | bind_interface, 93 | 94 | #[cfg(feature = "proxy")] 95 | proxy_opts, 96 | 97 | #[cfg(feature = "transport")] 98 | transport, 99 | 100 | #[cfg(feature = "balance")] 101 | balancer, 102 | } = self; 103 | 104 | if let Some(iface) = bind_interface { 105 | write!(f, "bind-iface={}, ", iface)?; 106 | } 107 | 108 | if let Some(send_through) = bind_address { 109 | write!(f, "send-through={}; ", send_through)?; 110 | } 111 | 112 | #[cfg(feature = "proxy")] 113 | { 114 | let ProxyOpts { 115 | send_proxy, 116 | accept_proxy, 117 | send_proxy_version, 118 | accept_proxy_timeout, 119 | } = proxy_opts; 120 | write!( 121 | f, 122 | "send-proxy={0}, send-proxy-version={2}, accept-proxy={1}, accept-proxy-timeout={3}s; ", 123 | send_proxy, accept_proxy, send_proxy_version, accept_proxy_timeout 124 | )?; 125 | } 126 | 127 | write!( 128 | f, 129 | "connect-timeout={}s, associate-timeout={}s; ", 130 | connect_timeout, associate_timeout 131 | )?; 132 | 133 | #[cfg(feature = "transport")] 134 | if let Some((ac, cc)) = transport { 135 | write!(f, "transport={}||{}; ", ac, cc)?; 136 | } 137 | 138 | #[cfg(feature = "balance")] 139 | write!(f, "balance={}", balancer.strategy())?; 140 | Ok(()) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use cfg_if::cfg_if; 3 | 4 | use realm::cmd; 5 | use realm::conf::{Config, FullConf, LogConf, DnsConf, EndpointInfo}; 6 | use realm::ENV_CONFIG; 7 | 8 | cfg_if! { 9 | if #[cfg(feature = "mi-malloc")] { 10 | use mimalloc::MiMalloc; 11 | #[global_allocator] 12 | static GLOBAL: MiMalloc = MiMalloc; 13 | } else if #[cfg(all(feature = "jemalloc", not(target_env = "msvc")))] { 14 | use jemallocator::Jemalloc; 15 | #[global_allocator] 16 | static GLOBAL: Jemalloc = Jemalloc; 17 | } else if #[cfg(all(feature = "page-alloc", unix))] { 18 | use mmap_allocator::MmapAllocator; 19 | #[global_allocator] 20 | static GLOBAL: MmapAllocator = MmapAllocator::new(); 21 | } 22 | } 23 | 24 | fn main() { 25 | let conf = (|| { 26 | if let Ok(conf_str) = env::var(ENV_CONFIG) { 27 | if let Ok(conf) = FullConf::from_conf_str(&conf_str) { 28 | return conf; 29 | } 30 | }; 31 | 32 | use cmd::CmdInput; 33 | match cmd::scan() { 34 | CmdInput::Endpoint(ep, opts) => { 35 | let mut conf = FullConf::default(); 36 | conf.add_endpoint(ep).apply_global_opts().apply_cmd_opts(opts); 37 | conf 38 | } 39 | CmdInput::Config(conf, opts) => { 40 | let mut conf = FullConf::from_conf_file(&conf); 41 | conf.apply_global_opts().apply_cmd_opts(opts); 42 | conf 43 | } 44 | CmdInput::None => std::process::exit(0), 45 | } 46 | })(); 47 | 48 | start_from_conf(conf); 49 | } 50 | 51 | fn start_from_conf(full: FullConf) { 52 | let FullConf { 53 | log: log_conf, 54 | dns: dns_conf, 55 | endpoints: endpoints_conf, 56 | .. 57 | } = full; 58 | 59 | setup_log(log_conf); 60 | setup_dns(dns_conf); 61 | 62 | let endpoints: Vec = endpoints_conf 63 | .into_iter() 64 | .map(|x| x.build()) 65 | .inspect(|x| println!("inited: {}", &x.endpoint)) 66 | .collect(); 67 | 68 | execute(endpoints); 69 | } 70 | 71 | fn setup_log(log: LogConf) { 72 | println!("log: {}", &log); 73 | 74 | let (level, output) = log.build(); 75 | fern::Dispatch::new() 76 | .format(|out, message, record| { 77 | out.finish(format_args!( 78 | "{}[{}][{}]{}", 79 | chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), 80 | record.target(), 81 | record.level(), 82 | message 83 | )) 84 | }) 85 | .level(level) 86 | .chain(output) 87 | .apply() 88 | .unwrap_or_else(|e| panic!("failed to setup logger: {}", &e)) 89 | } 90 | 91 | fn setup_dns(dns: DnsConf) { 92 | println!("dns: {}", &dns); 93 | 94 | let (conf, opts) = dns.build(); 95 | realm::core::dns::build(conf, opts); 96 | } 97 | 98 | fn execute(eps: Vec) { 99 | #[cfg(feature = "multi-thread")] 100 | { 101 | tokio::runtime::Builder::new_multi_thread() 102 | .enable_all() 103 | .build() 104 | .unwrap() 105 | .block_on(run(eps)) 106 | } 107 | 108 | #[cfg(not(feature = "multi-thread"))] 109 | { 110 | tokio::runtime::Builder::new_current_thread() 111 | .enable_all() 112 | .build() 113 | .unwrap() 114 | .block_on(run(eps)) 115 | } 116 | } 117 | 118 | async fn run(endpoints: Vec) { 119 | use realm::core::tcp::run_tcp; 120 | use realm::core::udp::run_udp; 121 | use futures::future::join_all; 122 | 123 | let mut workers = Vec::with_capacity(2 * endpoints.len()); 124 | 125 | for EndpointInfo { 126 | endpoint, 127 | no_tcp, 128 | use_udp, 129 | } in endpoints 130 | { 131 | if use_udp { 132 | workers.push(tokio::spawn(run_udp(endpoint.clone()))); 133 | } 134 | 135 | if !no_tcp { 136 | workers.push(tokio::spawn(run_tcp(endpoint))); 137 | } 138 | } 139 | 140 | workers.shrink_to_fit(); 141 | 142 | join_all(workers).await; 143 | } 144 | -------------------------------------------------------------------------------- /src/conf/log.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Display}; 2 | use serde::{Serialize, Deserialize}; 3 | use log::LevelFilter; 4 | use super::Config; 5 | use crate::consts::DEFAULT_LOG_FILE; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum LogLevel { 10 | Off, 11 | Error, 12 | Warn, 13 | Info, 14 | Debug, 15 | Trace, 16 | } 17 | 18 | impl From for LogLevel { 19 | fn from(x: String) -> Self { 20 | use LogLevel::*; 21 | match x.to_ascii_lowercase().as_str() { 22 | "off" => Off, 23 | "error" => Error, 24 | "warn" => Warn, 25 | "info" => Info, 26 | "debug" => Debug, 27 | "trace" => Trace, 28 | _ => Self::default(), 29 | } 30 | } 31 | } 32 | 33 | impl From for LevelFilter { 34 | fn from(x: LogLevel) -> Self { 35 | use LogLevel::*; 36 | match x { 37 | Off => LevelFilter::Off, 38 | Error => LevelFilter::Error, 39 | Warn => LevelFilter::Warn, 40 | Info => LevelFilter::Info, 41 | Debug => LevelFilter::Debug, 42 | Trace => LevelFilter::Trace, 43 | } 44 | } 45 | } 46 | 47 | impl Default for LogLevel { 48 | fn default() -> Self { 49 | LogLevel::Off 50 | } 51 | } 52 | 53 | impl Display for LogLevel { 54 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 55 | use LogLevel::*; 56 | let s = match self { 57 | Off => "off", 58 | Error => "error", 59 | Warn => "warn", 60 | Info => "info", 61 | Debug => "debug", 62 | Trace => "trace", 63 | }; 64 | write!(f, "{}", s) 65 | } 66 | } 67 | 68 | #[derive(Default, Debug, Serialize, Deserialize, Clone)] 69 | pub struct LogConf { 70 | #[serde(default)] 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | pub level: Option, 73 | 74 | #[serde(default)] 75 | #[serde(skip_serializing_if = "Option::is_none")] 76 | pub output: Option, 77 | } 78 | 79 | impl Config for LogConf { 80 | type Output = (LevelFilter, fern::Output); 81 | 82 | fn is_empty(&self) -> bool { 83 | crate::empty![self => level, output] 84 | } 85 | 86 | fn build(self) -> Self::Output { 87 | use std::io; 88 | use std::fs::OpenOptions; 89 | let LogConf { level, output } = self; 90 | let level = level.unwrap_or_default(); 91 | let output = output.unwrap_or_else(|| String::from(DEFAULT_LOG_FILE)); 92 | 93 | let output: fern::Output = match output.as_str() { 94 | "stdout" => io::stdout().into(), 95 | "stderr" => io::stderr().into(), 96 | output => OpenOptions::new() 97 | .write(true) 98 | .create(true) 99 | .append(true) 100 | .open(output) 101 | .unwrap_or_else(|e| panic!("failed to open {}: {}", output, &e)) 102 | .into(), 103 | }; 104 | 105 | (level.into(), output) 106 | } 107 | 108 | fn rst_field(&mut self, other: &Self) -> &mut Self { 109 | use crate::rst; 110 | let other = other.clone(); 111 | 112 | rst!(self, level, other); 113 | rst!(self, output, other); 114 | self 115 | } 116 | 117 | fn take_field(&mut self, other: &Self) -> &mut Self { 118 | use crate::take; 119 | let other = other.clone(); 120 | 121 | take!(self, level, other); 122 | take!(self, output, other); 123 | self 124 | } 125 | 126 | fn from_cmd_args(matches: &clap::ArgMatches) -> Self { 127 | let level = matches.value_of("log_level").map(|x| String::from(x).into()); 128 | 129 | let output = matches.value_of("log_output").map(String::from); 130 | 131 | Self { level, output } 132 | } 133 | } 134 | 135 | impl Display for LogConf { 136 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 137 | let LogConf { level, output } = self.clone(); 138 | let level = level.unwrap_or_default(); 139 | let output = output.unwrap_or_else(|| String::from("stdout")); 140 | 141 | write!(f, "level={}, output={}", level, output) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /realm_lb/src/balancer.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | use std::sync::Arc; 3 | use std::fmt::{Display, Formatter}; 4 | 5 | use crate::{Token, Balance}; 6 | use crate::ip_hash::IpHash; 7 | use crate::round_robin::RoundRobin; 8 | 9 | /// Balance strategy. 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 11 | pub enum Strategy { 12 | Off, 13 | IpHash, 14 | RoundRobin, 15 | } 16 | 17 | impl From<&str> for Strategy { 18 | fn from(s: &str) -> Self { 19 | use Strategy::*; 20 | match s { 21 | "off" => Off, 22 | "iphash" => IpHash, 23 | "roundrobin" => RoundRobin, 24 | _ => panic!("unknown strategy: {}", s), 25 | } 26 | } 27 | } 28 | 29 | impl Display for Strategy { 30 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 31 | match self { 32 | Strategy::Off => write!(f, "off"), 33 | Strategy::IpHash => write!(f, "iphash"), 34 | Strategy::RoundRobin => write!(f, "roundrobin"), 35 | } 36 | } 37 | } 38 | 39 | /// Balance context to select next peer. 40 | #[derive(Debug)] 41 | pub struct BalanceCtx<'a> { 42 | pub src_ip: &'a IpAddr, 43 | } 44 | 45 | /// Combinated load balancer. 46 | #[derive(Debug, Clone)] 47 | pub enum Balancer { 48 | Off, 49 | IpHash(Arc), 50 | RoundRobin(Arc), 51 | } 52 | 53 | impl Balancer { 54 | /// Constructor. 55 | pub fn new(strategy: Strategy, weights: &[u8]) -> Self { 56 | match strategy { 57 | Strategy::Off => Self::Off, 58 | Strategy::IpHash => Self::IpHash(Arc::new(IpHash::new(weights))), 59 | Strategy::RoundRobin => Self::RoundRobin(Arc::new(RoundRobin::new(weights))), 60 | } 61 | } 62 | 63 | /// Get current balance strategy. 64 | pub fn strategy(&self) -> Strategy { 65 | match self { 66 | Balancer::Off => Strategy::Off, 67 | Balancer::IpHash(_) => Strategy::IpHash, 68 | Balancer::RoundRobin(_) => Strategy::RoundRobin, 69 | } 70 | } 71 | 72 | /// Get total peers. 73 | pub fn total(&self) -> u8 { 74 | match self { 75 | Balancer::Off => 0, 76 | Balancer::IpHash(iphash) => iphash.total(), 77 | Balancer::RoundRobin(rr) => rr.total(), 78 | } 79 | } 80 | 81 | /// Select next peer. 82 | pub fn next(&self, ctx: BalanceCtx) -> Option { 83 | match self { 84 | Balancer::Off => Some(Token(0)), 85 | Balancer::IpHash(iphash) => iphash.next(ctx.src_ip), 86 | Balancer::RoundRobin(rr) => rr.next(&()), 87 | } 88 | } 89 | 90 | /// Parse balancer from string. 91 | /// Format: $strategy: $weight1, $weight2, ... 92 | pub fn parse_from_str(s: &str) -> Self { 93 | let (strategy, weights) = s.split_once(':').unwrap(); 94 | 95 | let strategy = Strategy::from(strategy.trim()); 96 | let weights: Vec = weights 97 | .trim() 98 | .split(',') 99 | .filter_map(|s| s.trim().parse().ok()) 100 | .collect(); 101 | 102 | Self::new(strategy, &weights) 103 | } 104 | } 105 | 106 | impl Default for Balancer { 107 | fn default() -> Self { 108 | Balancer::Off 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | 116 | #[test] 117 | fn parse_balancer() { 118 | fn run(strategy: Strategy, weights: &[u8]) { 119 | let mut s = String::with_capacity(128); 120 | s.push_str(&format!("{}: ", strategy)); 121 | 122 | for weight in weights { 123 | s.push_str(&format!("{}, ", weight)); 124 | } 125 | 126 | let balancer = Balancer::parse_from_str(&s); 127 | 128 | println!("balancer: {:?}", balancer); 129 | 130 | assert_eq!(balancer.strategy(), strategy); 131 | assert_eq!(balancer.total(), weights.len() as u8); 132 | } 133 | 134 | run(Strategy::Off, &[]); 135 | run(Strategy::IpHash, &[]); 136 | run(Strategy::IpHash, &[1, 2, 3]); 137 | run(Strategy::IpHash, &[1, 2, 3]); 138 | run(Strategy::IpHash, &[1, 2, 3]); 139 | run(Strategy::RoundRobin, &[]); 140 | run(Strategy::RoundRobin, &[1, 2, 3]); 141 | run(Strategy::RoundRobin, &[1, 2, 3]); 142 | run(Strategy::RoundRobin, &[1, 2, 3]); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /realm_io/src/buf.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, ErrorKind}; 2 | use std::task::{Context, Poll, ready}; 3 | use std::marker::PhantomData; 4 | 5 | use tokio::io::{AsyncRead, AsyncWrite}; 6 | 7 | /// A wrapper of its underlying buffer(array, vector, unix pipe...). 8 | pub struct CopyBuffer { 9 | pub(crate) read_done: bool, 10 | pub(crate) need_flush: bool, 11 | pub(crate) pos: usize, 12 | pub(crate) cap: usize, 13 | pub(crate) buf: B, 14 | _marker: PhantomData, 15 | __marker: PhantomData, 16 | } 17 | 18 | impl CopyBuffer { 19 | /// Constructor, take the provided buffer. 20 | pub const fn new(buf: B) -> Self { 21 | Self { 22 | read_done: false, 23 | need_flush: false, 24 | pos: 0, 25 | cap: 0, 26 | buf, 27 | _marker: PhantomData, 28 | __marker: PhantomData, 29 | } 30 | } 31 | } 32 | 33 | /// Type traits of [`CopyBuffer`]. 34 | pub trait AsyncIOBuf { 35 | type StreamR: AsyncRead + AsyncWrite + Unpin; 36 | type StreamW: AsyncRead + AsyncWrite + Unpin; 37 | 38 | fn poll_read_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamR) -> Poll>; 39 | 40 | fn poll_write_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamW) -> Poll>; 41 | 42 | fn poll_flush_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamW) -> Poll>; 43 | } 44 | 45 | impl CopyBuffer 46 | where 47 | B: Unpin, 48 | SR: AsyncRead + AsyncWrite + Unpin, 49 | SW: AsyncRead + AsyncWrite + Unpin, 50 | CopyBuffer: AsyncIOBuf, 51 | { 52 | /// Copy data from reader to writer via buffer, asynchronously. 53 | pub fn poll_copy( 54 | &mut self, 55 | cx: &mut Context<'_>, 56 | r: &mut as AsyncIOBuf>::StreamR, 57 | w: &mut as AsyncIOBuf>::StreamW, 58 | ) -> Poll> { 59 | loop { 60 | // If our buffer is empty, then we need to read some data to 61 | // continue. 62 | if self.pos == self.cap && !self.read_done { 63 | let n = match self.poll_read_buf(cx, r) { 64 | Poll::Ready(Ok(n)) => n, 65 | Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), 66 | Poll::Pending => { 67 | // Try flushing when the reader has no progress to avoid deadlock 68 | // when the reader depends on buffered writer. 69 | if self.need_flush { 70 | ready!(self.poll_flush_buf(cx, w))?; 71 | self.need_flush = false; 72 | } 73 | 74 | return Poll::Pending; 75 | } 76 | }; 77 | 78 | if n == 0 { 79 | self.read_done = true; 80 | } else { 81 | self.pos = 0; 82 | self.cap = n; 83 | } 84 | } 85 | 86 | // If our buffer has some data, let's write it out! 87 | // Note: send may return ECONNRESET but splice wont, see 88 | // https://man7.org/linux/man-pages/man2/send.2.html 89 | // https://man7.org/linux/man-pages/man2/splice.2.html 90 | while self.pos < self.cap { 91 | let i = ready!(self.poll_write_buf(cx, w))?; 92 | 93 | if i == 0 { 94 | return Poll::Ready(Err(ErrorKind::WriteZero.into())); 95 | } else { 96 | self.pos += i; 97 | self.need_flush = true; 98 | } 99 | } 100 | 101 | // If pos larger than cap, this loop will never stop. 102 | // In particular, user's wrong poll_write implementation returning 103 | // incorrect written length may lead to thread blocking. 104 | debug_assert!(self.pos <= self.cap, "writer returned length larger than input slice"); 105 | 106 | // If we've written all the data and we've seen EOF, flush out the 107 | // data and finish the transfer. 108 | if self.pos == self.cap && self.read_done { 109 | ready!(self.poll_flush_buf(cx, w))?; 110 | return Poll::Ready(Ok(())); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /realm_lb/src/round_robin.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | use super::{Balance, Token}; 4 | 5 | /// Round-robin node. 6 | #[derive(Debug)] 7 | struct Node { 8 | cw: i16, 9 | ew: u8, 10 | weight: u8, 11 | token: Token, 12 | } 13 | 14 | /// Round robin balancer. 15 | #[derive(Debug)] 16 | pub struct RoundRobin { 17 | nodes: Mutex>, 18 | total: u8, 19 | } 20 | 21 | impl Balance for RoundRobin { 22 | type State = (); 23 | 24 | fn total(&self) -> u8 { 25 | self.total 26 | } 27 | 28 | fn new(weights: &[u8]) -> Self { 29 | assert!(weights.len() <= u8::MAX as usize); 30 | 31 | if weights.len() <= 1 { 32 | return Self { 33 | nodes: Mutex::new(Vec::new()), 34 | total: weights.len() as u8, 35 | }; 36 | } 37 | 38 | let nodes = weights 39 | .iter() 40 | .enumerate() 41 | .map(|(i, w)| Node { 42 | ew: *w, 43 | cw: 0, 44 | weight: *w, 45 | token: Token(i as u8), 46 | }) 47 | .collect(); 48 | Self { 49 | nodes: Mutex::new(nodes), 50 | total: weights.len() as u8, 51 | } 52 | } 53 | 54 | #[allow(clippy::significant_drop_in_scrutinee)] 55 | fn next(&self, _: &Self::State) -> Option { 56 | if self.total <= 1 { 57 | return Some(Token(0)); 58 | } 59 | 60 | // lock the whole list 61 | { 62 | let mut nodes = self.nodes.lock().unwrap(); 63 | let mut tw: i16 = 0; 64 | let mut best: Option<&mut Node> = None; 65 | for p in nodes.iter_mut() { 66 | tw += p.ew as i16; 67 | p.cw += p.ew as i16; 68 | 69 | if p.ew < p.weight { 70 | p.ew += 1; 71 | } 72 | 73 | if let Some(ref x) = best { 74 | if p.cw > x.cw { 75 | best = Some(p); 76 | } 77 | } else { 78 | best = Some(p); 79 | } 80 | } 81 | 82 | best.map(|x| { 83 | x.cw -= tw; 84 | x.token 85 | }) 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use average::{Max, Mean, Min}; 94 | 95 | #[test] 96 | fn rr_same_weight() { 97 | let rr = RoundRobin::new(&vec![1; 255]); 98 | let mut distro = [0f64; 255]; 99 | 100 | for _ in 0..1_000_000 { 101 | let token = rr.next(&()).unwrap(); 102 | distro[token.0 as usize] += 1 as f64; 103 | } 104 | 105 | let diffs: Vec = distro 106 | .iter() 107 | .map(|x| *x / 1_000_000.0 - 1.0 / 255.0) 108 | .map(f64::abs) 109 | .inspect(|x| assert!(x < &1e-3)) 110 | .collect(); 111 | 112 | let min_diff: Min = diffs.iter().collect(); 113 | let max_diff: Max = diffs.iter().collect(); 114 | let mean_diff: Mean = diffs.iter().collect(); 115 | 116 | println!("{:?}", distro); 117 | println!("min diff: {}", min_diff.min()); 118 | println!("max diff: {}", max_diff.max()); 119 | println!("mean diff: {}", mean_diff.mean()); 120 | } 121 | 122 | #[test] 123 | fn rr_all_weights() { 124 | let weights: Vec = (1..=255).collect(); 125 | let total_weight: f64 = weights.iter().map(|x| *x as f64).sum(); 126 | let rr = RoundRobin::new(&weights); 127 | let mut distro = [0f64; 255]; 128 | 129 | for _ in 0..1_000_000 { 130 | let token = rr.next(&()).unwrap(); 131 | distro[token.0 as usize] += 1 as f64; 132 | } 133 | 134 | let diffs: Vec = distro 135 | .iter() 136 | .enumerate() 137 | .map(|(i, x)| *x / 1_000_000.0 - (i as f64 + 1.0) / total_weight) 138 | .map(f64::abs) 139 | .inspect(|x| assert!(x < &1e-3)) 140 | .collect(); 141 | 142 | let min_diff: Min = diffs.iter().collect(); 143 | let max_diff: Max = diffs.iter().collect(); 144 | let mean_diff: Mean = diffs.iter().collect(); 145 | 146 | println!("{:?}", distro); 147 | println!("min diff: {}", min_diff.min()); 148 | println!("max diff: {}", max_diff.max()); 149 | println!("mean diff: {}", mean_diff.mean()); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /realm_io/src/bidi_copy.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll, ready}; 4 | use std::future::Future; 5 | 6 | use tokio::io::{AsyncRead, AsyncWrite}; 7 | 8 | use super::{AsyncIOBuf, CopyBuffer}; 9 | 10 | enum TransferState { 11 | Running(CopyBuffer), 12 | ShuttingDown, 13 | Done, 14 | } 15 | 16 | fn transfer( 17 | cx: &mut Context<'_>, 18 | state: &mut TransferState, 19 | r: &mut as AsyncIOBuf>::StreamR, 20 | w: &mut as AsyncIOBuf>::StreamW, 21 | ) -> Poll> 22 | where 23 | B: Unpin, 24 | SL: AsyncRead + AsyncWrite + Unpin, 25 | SR: AsyncRead + AsyncWrite + Unpin, 26 | CopyBuffer: AsyncIOBuf, 27 | CopyBuffer: AsyncIOBuf, 28 | { 29 | loop { 30 | match state { 31 | TransferState::Running(buf) => { 32 | ready!(buf.poll_copy(cx, r, w))?; 33 | 34 | *state = TransferState::ShuttingDown; 35 | } 36 | TransferState::ShuttingDown => { 37 | ready!(Pin::new(&mut *w).poll_shutdown(cx))?; 38 | 39 | *state = TransferState::Done; 40 | } 41 | TransferState::Done => return Poll::Ready(Ok(())), 42 | } 43 | } 44 | } 45 | 46 | fn transfer2( 47 | cx: &mut Context<'_>, 48 | state: &mut TransferState, // reverse 49 | r: &mut as AsyncIOBuf>::StreamW, 50 | w: &mut as AsyncIOBuf>::StreamR, 51 | ) -> Poll> 52 | where 53 | B: Unpin, 54 | SL: AsyncRead + AsyncWrite + Unpin, 55 | SR: AsyncRead + AsyncWrite + Unpin, 56 | CopyBuffer: AsyncIOBuf, 57 | CopyBuffer: AsyncIOBuf, 58 | { 59 | // type equality constraints will save this (one day)! 60 | let r: &mut as AsyncIOBuf>::StreamR = unsafe { std::mem::transmute(r) }; 61 | let w: &mut as AsyncIOBuf>::StreamW = unsafe { std::mem::transmute(w) }; 62 | loop { 63 | match state { 64 | TransferState::Running(buf) => { 65 | ready!(buf.poll_copy(cx, r, w))?; 66 | 67 | *state = TransferState::ShuttingDown; 68 | } 69 | TransferState::ShuttingDown => { 70 | ready!(Pin::new(&mut *w).poll_shutdown(cx))?; 71 | 72 | *state = TransferState::Done; 73 | } 74 | TransferState::Done => return Poll::Ready(Ok(())), 75 | } 76 | } 77 | } 78 | 79 | struct BidiCopy<'a, B, SL, SR> 80 | where 81 | B: Unpin, 82 | SL: AsyncRead + AsyncWrite + Unpin, 83 | SR: AsyncRead + AsyncWrite + Unpin, 84 | CopyBuffer: AsyncIOBuf, 85 | CopyBuffer: AsyncIOBuf, 86 | { 87 | a: &'a mut as AsyncIOBuf>::StreamR, 88 | b: &'a mut as AsyncIOBuf>::StreamW, 89 | a_to_b: TransferState, 90 | b_to_a: TransferState, 91 | } 92 | 93 | impl<'a, B, SL, SR> Future for BidiCopy<'a, B, SL, SR> 94 | where 95 | B: Unpin, 96 | SL: AsyncRead + AsyncWrite + Unpin, 97 | SR: AsyncRead + AsyncWrite + Unpin, 98 | CopyBuffer: AsyncIOBuf, 99 | CopyBuffer: AsyncIOBuf, 100 | { 101 | type Output = Result<()>; 102 | 103 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 104 | // Unpack self into mut refs to each field to avoid borrow check issues. 105 | let BidiCopy { a, b, a_to_b, b_to_a } = self.get_mut(); 106 | 107 | let a_to_b = transfer(cx, a_to_b, a, b)?; 108 | let b_to_a = transfer2::(cx, b_to_a, b, a)?; 109 | 110 | // graceful shutdown 111 | #[cfg(not(feature = "brutal-shutdown"))] 112 | { 113 | ready!(a_to_b); 114 | ready!(b_to_a); 115 | Poll::Ready(Ok(())) 116 | } 117 | 118 | // brutal shutdown 119 | #[cfg(feature = "brutal-shutdown")] 120 | { 121 | if a_to_b.is_ready() || b_to_a.is_ready() { 122 | Poll::Ready(Ok(())) 123 | } else { 124 | Poll::Pending 125 | } 126 | } 127 | } 128 | } 129 | 130 | /// Copy data bidirectionally between two streams with provided buffer. 131 | pub async fn bidi_copy_buf( 132 | a: &mut as AsyncIOBuf>::StreamR, 133 | b: &mut as AsyncIOBuf>::StreamW, 134 | a_to_b_buf: CopyBuffer, 135 | b_to_a_buf: CopyBuffer, 136 | ) -> Result<()> 137 | where 138 | B: Unpin, 139 | SR: AsyncRead + AsyncWrite + Unpin, 140 | SW: AsyncRead + AsyncWrite + Unpin, 141 | CopyBuffer: AsyncIOBuf, 142 | CopyBuffer: AsyncIOBuf, 143 | { 144 | let a_to_b = TransferState::Running(a_to_b_buf); 145 | let b_to_a = TransferState::Running(b_to_a_buf); 146 | 147 | BidiCopy { a, b, a_to_b, b_to_a }.await 148 | } 149 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | 7 | jobs: 8 | release-corss: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | include: 13 | - target: x86_64-unknown-linux-gnu 14 | toolchain: nightly 15 | output: realm 16 | - target: x86_64-unknown-linux-musl 17 | toolchain: nightly 18 | output: realm 19 | - target: x86_64-linux-android 20 | toolchain: nightly-2022-09-04 21 | output: realm 22 | - target: x86_64-pc-windows-gnu 23 | toolchain: nightly 24 | output: realm.exe 25 | - target: aarch64-unknown-linux-gnu 26 | toolchain: nightly 27 | output: realm 28 | - target: aarch64-unknown-linux-musl 29 | toolchain: nightly 30 | output: realm 31 | - target: aarch64-linux-android 32 | toolchain: nightly-2022-09-04 33 | output: realm 34 | - target: arm-unknown-linux-gnueabi 35 | toolchain: nightly 36 | output: realm 37 | - target: arm-unknown-linux-gnueabihf 38 | toolchain: nightly 39 | output: realm 40 | - target: arm-unknown-linux-musleabihf 41 | toolchain: nightly 42 | output: realm 43 | - target: armv7-unknown-linux-gnueabihf 44 | toolchain: nightly 45 | output: realm 46 | - target: armv7-unknown-linux-musleabihf 47 | toolchain: nightly 48 | output: realm 49 | steps: 50 | - uses: actions/checkout@v2 51 | - name: install toolchain 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | toolchain: ${{ matrix.toolchain }} 55 | target: ${{ matrix.target }} 56 | override: true 57 | - name: compile 58 | uses: actions-rs/cargo@v1 59 | with: 60 | use-cross: true 61 | command: build 62 | args: --release --target=${{ matrix.target }} 63 | - name: pack 64 | run: | 65 | mkdir -p release-${{ matrix.target }} 66 | cd release-${{ matrix.target }} 67 | tar -C ../target/${{ matrix.target }}/release -zcf realm-${{ matrix.target }}.tar.gz ${{ matrix.output }} 68 | sha256sum realm-${{ matrix.target }}.tar.gz > realm-${{ matrix.target }}.sha256 69 | - name: release 70 | uses: softprops/action-gh-release@v1 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | files: release-${{ matrix.target }}/* 75 | release-windows: 76 | runs-on: windows-latest 77 | strategy: 78 | matrix: 79 | target: 80 | - x86_64-pc-windows-msvc 81 | steps: 82 | - uses: actions/checkout@v2 83 | - name: install toolchain 84 | uses: actions-rs/toolchain@v1 85 | with: 86 | toolchain: nightly 87 | target: ${{ matrix.target }} 88 | override: true 89 | - name: compile 90 | uses: actions-rs/cargo@v1 91 | with: 92 | use-cross: true 93 | command: build 94 | args: --release --target=${{ matrix.target }} 95 | - name: pack 96 | run: | 97 | mkdir -p release-${{ matrix.target }} 98 | cd release-${{ matrix.target }} 99 | tar -C ../target/${{ matrix.target }}/release/ -zcf realm-${{ matrix.target }}.tar.gz realm 100 | openssl dgst -sha256 -r realm-${{ matrix.target }}.tar.gz > realm-${{ matrix.target }}.sha256 101 | - name: release 102 | uses: softprops/action-gh-release@v1 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | with: 106 | files: release-${{ matrix.target }}/* 107 | release-apple: 108 | runs-on: macos-latest 109 | strategy: 110 | matrix: 111 | target: 112 | - x86_64-apple-darwin 113 | - aarch64-apple-darwin 114 | - aarch64-apple-ios 115 | steps: 116 | - uses: actions/checkout@v2 117 | - name: install toolchain 118 | uses: actions-rs/toolchain@v1 119 | with: 120 | toolchain: nightly 121 | target: ${{ matrix.target }} 122 | override: true 123 | - name: compile 124 | uses: actions-rs/cargo@v1 125 | with: 126 | use-cross: true 127 | command: build 128 | args: --release --target=${{ matrix.target }} 129 | - name: pack 130 | run: | 131 | mkdir -p release-${{ matrix.target }} 132 | cd release-${{ matrix.target }} 133 | tar -C ../target/${{ matrix.target }}/release/ -zcf realm-${{ matrix.target }}.tar.gz realm 134 | shasum -a 256 realm-${{ matrix.target }}.tar.gz > realm-${{ matrix.target }}.sha256 135 | - name: release 136 | uses: softprops/action-gh-release@v1 137 | env: 138 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 139 | with: 140 | files: release-${{ matrix.target }}/* 141 | -------------------------------------------------------------------------------- /src/conf/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{Result, Error, ErrorKind}; 3 | 4 | use clap::ArgMatches; 5 | use serde::{Serialize, Deserialize}; 6 | 7 | mod log; 8 | pub use self::log::{LogLevel, LogConf}; 9 | 10 | mod dns; 11 | pub use dns::{DnsMode, DnsProtocol, DnsConf}; 12 | 13 | mod net; 14 | pub use net::{NetConf, NetInfo}; 15 | 16 | mod endpoint; 17 | pub use endpoint::{EndpointConf, EndpointInfo}; 18 | 19 | mod legacy; 20 | pub use legacy::LegacyConf; 21 | 22 | /// Conig Architecture 23 | /// cmd | file => LogConf => { level, output } 24 | /// cmd | file => DnsConf => { resolve cinfig, opts } 25 | /// cmd | file => NetConf 26 | /// \ 27 | /// cmd | file => EndpointConf => { [local, remote, conn_opts] } 28 | 29 | pub trait Config { 30 | type Output; 31 | 32 | fn is_empty(&self) -> bool; 33 | 34 | fn build(self) -> Self::Output; 35 | 36 | // override self if other not empty 37 | // e.g.: cmd argument overrides global and local option 38 | fn rst_field(&mut self, other: &Self) -> &mut Self; 39 | 40 | // take other only if self empty & other not empty 41 | // e.g.: local field takes global option 42 | fn take_field(&mut self, other: &Self) -> &mut Self; 43 | 44 | fn from_cmd_args(matches: &ArgMatches) -> Self; 45 | } 46 | 47 | #[derive(Debug, Default)] 48 | pub struct CmdOverride { 49 | pub log: LogConf, 50 | pub dns: DnsConf, 51 | pub network: NetConf, 52 | } 53 | 54 | #[derive(Debug, Default, Serialize, Deserialize)] 55 | pub struct FullConf { 56 | #[serde(default)] 57 | #[serde(skip_serializing_if = "Config::is_empty")] 58 | pub log: LogConf, 59 | 60 | #[serde(default)] 61 | #[serde(skip_serializing_if = "Config::is_empty")] 62 | pub dns: DnsConf, 63 | 64 | #[serde(default)] 65 | #[serde(skip_serializing_if = "Config::is_empty")] 66 | pub network: NetConf, 67 | 68 | pub endpoints: Vec, 69 | } 70 | 71 | impl FullConf { 72 | #[allow(unused)] 73 | pub fn new(log: LogConf, dns: DnsConf, network: NetConf, endpoints: Vec) -> Self { 74 | FullConf { 75 | log, 76 | dns, 77 | network, 78 | endpoints, 79 | } 80 | } 81 | 82 | pub fn from_conf_file(file: &str) -> Self { 83 | let conf = fs::read_to_string(file).unwrap_or_else(|e| panic!("unable to open {}: {}", file, &e)); 84 | match Self::from_conf_str(&conf) { 85 | Ok(x) => x, 86 | Err(e) => panic!("failed to parse {}: {}", file, &e), 87 | } 88 | } 89 | 90 | pub fn from_conf_str(s: &str) -> Result { 91 | let toml_err = match toml::from_str(s) { 92 | Ok(x) => return Ok(x), 93 | Err(e) => e, 94 | }; 95 | 96 | let json_err = match serde_json::from_str(s) { 97 | Ok(x) => return Ok(x), 98 | Err(e) => e, 99 | }; 100 | 101 | // to be compatible with old version 102 | let legacy_err = match serde_json::from_str::(s) { 103 | Ok(x) => { 104 | eprintln!("attention: you are using a legacy config file!"); 105 | return Ok(x.into()); 106 | } 107 | Err(e) => e, 108 | }; 109 | 110 | Err(Error::new( 111 | ErrorKind::Other, 112 | format!( 113 | "parse as toml: {0}; parse as json: {1}; parse as legacy: {2}", 114 | toml_err, json_err, legacy_err 115 | ), 116 | )) 117 | } 118 | 119 | pub fn add_endpoint(&mut self, endpoint: EndpointConf) -> &mut Self { 120 | self.endpoints.push(endpoint); 121 | self 122 | } 123 | 124 | // override 125 | pub fn apply_cmd_opts(&mut self, opts: CmdOverride) -> &mut Self { 126 | let CmdOverride { 127 | ref log, 128 | ref dns, 129 | ref network, 130 | } = opts; 131 | 132 | self.log.rst_field(log); 133 | self.dns.rst_field(dns); 134 | self.endpoints.iter_mut().for_each(|x| { 135 | x.network.rst_field(network); 136 | }); 137 | 138 | self 139 | } 140 | 141 | // take inner global opts 142 | pub fn apply_global_opts(&mut self) -> &mut Self { 143 | self.endpoints.iter_mut().for_each(|x| { 144 | x.network.take_field(&self.network); 145 | }); 146 | 147 | self 148 | } 149 | } 150 | 151 | #[macro_export] 152 | macro_rules! rst { 153 | ($this: ident, $field: ident, $other: ident) => { 154 | let Self { $field, .. } = $other; 155 | if $field.is_some() { 156 | $this.$field = $field; 157 | } 158 | }; 159 | } 160 | 161 | #[macro_export] 162 | macro_rules! take { 163 | ($this: ident, $field: ident, $other: ident) => { 164 | let Self { $field, .. } = $other; 165 | if $this.$field.is_none() && $field.is_some() { 166 | $this.$field = $field; 167 | } 168 | }; 169 | } 170 | 171 | #[macro_export] 172 | macro_rules! empty { 173 | ( $this: expr => $( $field: ident ),* ) => {{ 174 | let mut res = true; 175 | $( 176 | res = res && $this.$field.is_none(); 177 | )* 178 | res 179 | }}; 180 | ( $( $value: expr ),* ) => {{ 181 | let mut res = true; 182 | $( 183 | res = res && $value.is_none(); 184 | )* 185 | res 186 | }} 187 | } 188 | -------------------------------------------------------------------------------- /src/conf/legacy/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | 3 | use super::{FullConf, EndpointConf}; 4 | 5 | // from https://github.com/zhboner/realm/blob/8ad8f0405e97cc470ba8b76c059c203b7381d2fb/src/lib.rs#L58-L63 6 | // pub struct ConfigFile { 7 | // pub listening_addresses: Vec, 8 | // pub listening_ports: Vec, 9 | // pub remote_addresses: Vec, 10 | // pub remote_ports: Vec, 11 | // } 12 | #[derive(Serialize, Deserialize)] 13 | pub struct LegacyConf { 14 | #[serde(rename = "listening_addresses")] 15 | pub listen_addrs: Vec, 16 | #[serde(rename = "listening_ports")] 17 | pub listen_ports: Vec, 18 | #[serde(rename = "remote_addresses")] 19 | pub remote_addrs: Vec, 20 | #[serde(rename = "remote_ports")] 21 | pub remote_ports: Vec, 22 | } 23 | 24 | fn flatten_ports(ports: Vec) -> Vec { 25 | ports 26 | .into_iter() 27 | .flat_map(|range| match (range.split('-').next(), range.split('-').nth(1)) { 28 | (Some(start), Some(end)) => start.parse::().unwrap()..end.parse::().unwrap() + 1, 29 | (Some(start), None) => start.parse::().unwrap()..start.parse::().unwrap() + 1, 30 | _ => panic!("failed to parse ports"), 31 | }) 32 | .collect() 33 | } 34 | 35 | fn join_addr_port(addrs: Vec, ports: Vec, len: usize) -> Vec { 36 | use std::iter::repeat; 37 | 38 | let port0 = ports[0]; 39 | let addr0 = addrs[0].clone(); 40 | 41 | let port_iter = ports.into_iter().take(len).chain(repeat(port0)).take(len); 42 | let addr_iter = addrs.into_iter().take(len).chain(repeat(addr0)).take(len); 43 | 44 | addr_iter 45 | .zip(port_iter) 46 | .map(|(addr, port)| format!("{}:{}", addr, port)) 47 | .collect() 48 | } 49 | 50 | impl From for FullConf { 51 | fn from(x: LegacyConf) -> Self { 52 | let LegacyConf { 53 | listen_addrs, 54 | listen_ports, 55 | remote_addrs, 56 | remote_ports, 57 | } = x; 58 | 59 | let listen_ports = flatten_ports(listen_ports); 60 | let remote_ports = flatten_ports(remote_ports); 61 | 62 | let len = listen_ports.len(); 63 | 64 | let listen = join_addr_port(listen_addrs, listen_ports, len); 65 | let remote = join_addr_port(remote_addrs, remote_ports, len); 66 | 67 | let endpoints = listen 68 | .into_iter() 69 | .zip(remote.into_iter()) 70 | .map(|(listen, remote)| EndpointConf { 71 | listen, 72 | remote, 73 | through: None, 74 | interface: None, 75 | listen_transport: None, 76 | remote_transport: None, 77 | network: Default::default(), 78 | extra_remotes: Vec::new(), 79 | balance: None, 80 | }) 81 | .collect(); 82 | 83 | FullConf { 84 | endpoints, 85 | ..Default::default() 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | macro_rules! strvec { 93 | ( $( $x: expr ),+ ) => { 94 | vec![ 95 | $( 96 | String::from($x), 97 | )+ 98 | ] 99 | }; 100 | } 101 | 102 | #[test] 103 | fn flatten_ports() { 104 | let v1 = strvec!["1-4"]; 105 | let v2 = strvec!["1-2", "3-4"]; 106 | let v3 = strvec!["1-3", "4"]; 107 | let v4 = strvec!["1", "2", "3", "4"]; 108 | assert_eq!(super::flatten_ports(v1), [1, 2, 3, 4]); 109 | assert_eq!(super::flatten_ports(v2), [1, 2, 3, 4]); 110 | assert_eq!(super::flatten_ports(v3), [1, 2, 3, 4]); 111 | assert_eq!(super::flatten_ports(v4), [1, 2, 3, 4]); 112 | } 113 | 114 | #[test] 115 | fn join_addr_port() { 116 | let addrs = strvec!["a.com", "b.com", "c.com"]; 117 | let ports = vec![1, 2, 3]; 118 | let result = vec!["a.com:1", "b.com:2", "c.com:3"]; 119 | assert_eq!(super::join_addr_port(addrs, ports, 3), result); 120 | 121 | let addrs = strvec!["a.com", "b.com", "c.com"]; 122 | let ports = vec![1, 2, 3]; 123 | let result = vec!["a.com:1", "b.com:2", "c.com:3"]; 124 | assert_eq!(super::join_addr_port(addrs, ports, 2), result[..2]); 125 | 126 | let addrs = strvec!["a.com", "b.com", "c.com"]; 127 | let ports = vec![1, 2, 3]; 128 | let result = vec!["a.com:1", "b.com:2", "c.com:3", "a.com:1"]; 129 | assert_eq!(super::join_addr_port(addrs, ports, 4), result); 130 | 131 | let addrs = strvec!["a.com", "b.com", "c.com"]; 132 | let ports = vec![1, 2, 3, 4, 5, 6]; 133 | let result = vec!["a.com:1", "b.com:2", "c.com:3", "a.com:4"]; 134 | assert_eq!(super::join_addr_port(addrs, ports, 4), result); 135 | 136 | let addrs = strvec!["a.com", "b.com", "c.com", "d.com", "e.com"]; 137 | let ports = vec![1, 2, 3]; 138 | let result = vec!["a.com:1", "b.com:2", "c.com:3", "d.com:1"]; 139 | assert_eq!(super::join_addr_port(addrs, ports, 4), result); 140 | 141 | let addrs = strvec!["a.com", "b.com", "c.com"]; 142 | let ports = vec![1, 2, 3]; 143 | let result = vec!["a.com:1", "b.com:2", "c.com:3", "a.com:1", "a.com:1"]; 144 | assert_eq!(super::join_addr_port(addrs, ports, 5), result); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /realm_io/src/peek.rs: -------------------------------------------------------------------------------- 1 | //! Peek impl. 2 | 3 | use std::io::{Result, IoSlice}; 4 | use std::pin::Pin; 5 | use std::future::Future; 6 | use std::task::{Poll, Context, ready}; 7 | 8 | use tokio::io::{ReadBuf, AsyncRead, AsyncWrite}; 9 | 10 | /// A wrapper to inspect data without consuming underlying buffer. 11 | pub struct PeekStream { 12 | rd: usize, 13 | wr: usize, 14 | pub io: T, 15 | pub buf: U, 16 | } 17 | 18 | impl PeekStream { 19 | /// Create with provided buffer. 20 | pub const fn new(io: T, buf: U) -> Self { 21 | Self { io, rd: 0, wr: 0, buf } 22 | } 23 | 24 | /// Create and allocate memory on heap. 25 | pub fn new_alloc(io: T, n: usize) -> PeekStream> { 26 | PeekStream { 27 | io, 28 | rd: 0, 29 | wr: 0, 30 | buf: vec![0; n].into_boxed_slice(), 31 | } 32 | } 33 | } 34 | 35 | impl PeekStream 36 | where 37 | U: AsRef<[u8]> + AsMut<[u8]>, 38 | { 39 | /// Return filled slice. 40 | #[inline] 41 | pub fn filled_slice(&self) -> &[u8] { 42 | &self.buf.as_ref()[self.rd..self.wr] 43 | } 44 | 45 | /// Return unfilled mutable slice. 46 | #[inline] 47 | pub fn unfilled_slice(&mut self) -> &mut [u8] { 48 | &mut self.buf.as_mut()[self.wr..] 49 | } 50 | 51 | /// Buffer capacity. 52 | #[inline] 53 | pub fn capacity(&self) -> usize { 54 | self.buf.as_ref().len() 55 | } 56 | 57 | /// Filled(unread) bytes. 58 | #[inline] 59 | pub fn filled(&self) -> usize { 60 | self.wr - self.rd 61 | } 62 | 63 | /// Unfilled(unwrite) bytes. 64 | #[inline] 65 | pub fn unfilled(&self) -> usize { 66 | self.buf.as_ref().len() - self.wr 67 | } 68 | 69 | #[inline] 70 | fn try_reset(&mut self) { 71 | if self.filled() == 0 { 72 | self.rd = 0; 73 | self.wr = 0; 74 | } 75 | } 76 | } 77 | 78 | struct Peek<'a, T, U> { 79 | pk: &'a mut PeekStream, 80 | } 81 | 82 | impl Future for Peek<'_, T, U> 83 | where 84 | T: AsyncRead + Unpin, 85 | U: AsRef<[u8]> + AsMut<[u8]> + Unpin, 86 | { 87 | type Output = Result; 88 | 89 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 90 | let this = self.get_mut(); 91 | 92 | let mut buf = ReadBuf::new(&mut this.pk.buf.as_mut()[this.pk.wr..]); 93 | 94 | ready!(Pin::new(&mut this.pk.io).poll_read(cx, &mut buf))?; 95 | 96 | let n = buf.filled().len(); 97 | this.pk.wr += n; 98 | 99 | Poll::Ready(Ok(n)) 100 | } 101 | } 102 | 103 | impl PeekStream 104 | where 105 | T: AsyncRead + Unpin, 106 | U: AsRef<[u8]> + AsMut<[u8]> + Unpin, 107 | { 108 | /// Peek bytes. 109 | pub async fn peek(&mut self, buf: &mut [u8]) -> Result { 110 | self.try_reset(); 111 | 112 | if self.unfilled() > 0 { 113 | Peek { pk: self }.await?; 114 | } 115 | 116 | let len = std::cmp::min(self.filled(), buf.len()); 117 | let (left, _) = buf.split_at_mut(len); 118 | left.copy_from_slice(&self.filled_slice()[..self.rd + len]); 119 | Ok(len) 120 | } 121 | 122 | /// Peek exact n bytes, fill the provided buffer. 123 | pub async fn peek_exact(&mut self, buf: &mut [u8]) -> Result<()> { 124 | assert!(self.capacity() >= buf.len()); 125 | self.try_reset(); 126 | 127 | let len = buf.len(); 128 | let mut required = len.saturating_sub(self.filled()); 129 | 130 | while required > 0 { 131 | let n = Peek { pk: self }.await?; 132 | required = required.saturating_sub(n); 133 | } 134 | 135 | let (left, _) = buf.split_at_mut(len); 136 | left.copy_from_slice(&self.filled_slice()[..self.rd + len]); 137 | Ok(()) 138 | } 139 | } 140 | 141 | impl AsyncRead for PeekStream 142 | where 143 | T: AsyncRead + Unpin, 144 | U: AsRef<[u8]> + Unpin, 145 | { 146 | fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { 147 | let this = self.get_mut(); 148 | 149 | if this.rd == this.wr { 150 | Pin::new(&mut this.io).poll_read(cx, buf) 151 | } else { 152 | let extra = this.buf.as_ref(); 153 | let len = std::cmp::min(this.wr - this.rd, buf.remaining()); 154 | buf.put_slice(&extra[this.rd..this.rd + len]); 155 | this.rd += len; 156 | Poll::Ready(Ok(())) 157 | } 158 | } 159 | } 160 | 161 | impl AsyncWrite for PeekStream 162 | where 163 | T: AsyncWrite + Unpin, 164 | U: Unpin, 165 | { 166 | #[inline] 167 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 168 | Pin::new(&mut self.get_mut().io).poll_write(cx, buf) 169 | } 170 | 171 | #[inline] 172 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 173 | Pin::new(&mut self.get_mut().io).poll_flush(cx) 174 | } 175 | 176 | #[inline] 177 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 178 | Pin::new(&mut self.get_mut().io).poll_shutdown(cx) 179 | } 180 | 181 | #[inline] 182 | fn is_write_vectored(&self) -> bool { 183 | self.io.is_write_vectored() 184 | } 185 | 186 | #[inline] 187 | fn poll_write_vectored(self: Pin<&mut Self>, cx: &mut Context<'_>, iovec: &[IoSlice<'_>]) -> Poll> { 188 | Pin::new(&mut self.get_mut().io).poll_write_vectored(cx, iovec) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/conf/net.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use realm_core::endpoint::{ConnectOpts, ProxyOpts}; 3 | 4 | use super::Config; 5 | use crate::consts::{TCP_TIMEOUT, UDP_TIMEOUT}; 6 | use crate::consts::PROXY_PROTOCOL_VERSION; 7 | use crate::consts::PROXY_PROTOCOL_TIMEOUT; 8 | 9 | #[derive(Serialize, Debug, Deserialize, Clone, Copy, Default)] 10 | pub struct NetConf { 11 | #[serde(default)] 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub no_tcp: Option, 14 | 15 | #[serde(default)] 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub use_udp: Option, 18 | 19 | #[serde(default)] 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub send_proxy: Option, 22 | 23 | #[serde(default)] 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub accept_proxy: Option, 26 | 27 | #[serde(default)] 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub send_proxy_version: Option, 30 | 31 | #[serde(default)] 32 | pub accept_proxy_timeout: Option, 33 | 34 | #[serde(default)] 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub tcp_timeout: Option, 37 | 38 | #[serde(default)] 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub udp_timeout: Option, 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct NetInfo { 45 | pub conn_opts: ConnectOpts, 46 | pub no_tcp: bool, 47 | pub use_udp: bool, 48 | } 49 | 50 | impl Config for NetConf { 51 | type Output = NetInfo; 52 | 53 | fn is_empty(&self) -> bool { 54 | crate::empty![self => 55 | send_proxy, accept_proxy, send_proxy_version, accept_proxy_timeout, 56 | tcp_timeout, udp_timeout 57 | ] 58 | } 59 | 60 | fn build(self) -> Self::Output { 61 | macro_rules! unbox { 62 | ($field: ident) => { 63 | self.$field.unwrap_or_default() 64 | }; 65 | ($field: ident, $value: expr) => { 66 | self.$field.unwrap_or($value) 67 | }; 68 | } 69 | 70 | let no_tcp = unbox!(no_tcp); 71 | let use_udp = unbox!(use_udp); 72 | 73 | let tcp_timeout = unbox!(tcp_timeout, TCP_TIMEOUT); 74 | let udp_timeout = unbox!(udp_timeout, UDP_TIMEOUT); 75 | 76 | let send_proxy = unbox!(send_proxy); 77 | let send_proxy_version = unbox!(send_proxy_version, PROXY_PROTOCOL_VERSION); 78 | 79 | let accept_proxy = unbox!(accept_proxy); 80 | let accept_proxy_timeout = unbox!(accept_proxy_timeout, PROXY_PROTOCOL_TIMEOUT); 81 | 82 | let conn_opts = ConnectOpts { 83 | connect_timeout: tcp_timeout, 84 | associate_timeout: udp_timeout, 85 | 86 | // from endpoint 87 | bind_address: None, 88 | bind_interface: None, 89 | 90 | #[cfg(feature = "balance")] 91 | balancer: Default::default(), 92 | 93 | #[cfg(feature = "transport")] 94 | transport: None, 95 | 96 | proxy_opts: ProxyOpts { 97 | send_proxy, 98 | accept_proxy, 99 | send_proxy_version, 100 | accept_proxy_timeout, 101 | }, 102 | }; 103 | 104 | NetInfo { 105 | conn_opts, 106 | no_tcp, 107 | use_udp, 108 | } 109 | } 110 | 111 | fn rst_field(&mut self, other: &Self) -> &mut Self { 112 | use crate::rst; 113 | let other = *other; 114 | 115 | rst!(self, no_tcp, other); 116 | rst!(self, use_udp, other); 117 | rst!(self, tcp_timeout, other); 118 | rst!(self, udp_timeout, other); 119 | rst!(self, send_proxy, other); 120 | rst!(self, accept_proxy, other); 121 | rst!(self, send_proxy_version, other); 122 | rst!(self, accept_proxy_timeout, other); 123 | self 124 | } 125 | 126 | fn take_field(&mut self, other: &Self) -> &mut Self { 127 | use crate::take; 128 | let other = *other; 129 | 130 | take!(self, no_tcp, other); 131 | take!(self, use_udp, other); 132 | take!(self, tcp_timeout, other); 133 | take!(self, udp_timeout, other); 134 | take!(self, send_proxy, other); 135 | take!(self, accept_proxy, other); 136 | take!(self, send_proxy_version, other); 137 | take!(self, accept_proxy_timeout, other); 138 | self 139 | } 140 | 141 | fn from_cmd_args(matches: &clap::ArgMatches) -> Self { 142 | macro_rules! unpack { 143 | ($key: expr) => { 144 | if matches.is_present($key) { 145 | Some(true) 146 | } else { 147 | None 148 | } 149 | }; 150 | ($key: expr, $t: ident) => { 151 | matches.value_of($key).map(|x| x.parse::<$t>().unwrap()) 152 | }; 153 | } 154 | 155 | let no_tcp = unpack!("no_tcp"); 156 | let use_udp = unpack!("use_udp"); 157 | 158 | let tcp_timeout = unpack!("tcp_timeout", usize); 159 | let udp_timeout = unpack!("udp_timeout", usize); 160 | 161 | let send_proxy = unpack!("send_proxy"); 162 | let send_proxy_version = unpack!("send_proxy_version", usize); 163 | 164 | let accept_proxy = unpack!("accept_proxy"); 165 | let accept_proxy_timeout = unpack!("accept_proxy_timeout", usize); 166 | 167 | Self { 168 | no_tcp, 169 | use_udp, 170 | tcp_timeout, 171 | udp_timeout, 172 | send_proxy, 173 | accept_proxy, 174 | send_proxy_version, 175 | accept_proxy_timeout, 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /realm_io/src/zero_copy.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, Error, ErrorKind}; 2 | use std::pin::Pin; 3 | use std::task::{Poll, Context, ready}; 4 | use std::os::unix::io::{RawFd, AsRawFd}; 5 | 6 | use tokio::io::Interest; 7 | use tokio::io::{AsyncRead, AsyncWrite}; 8 | 9 | use super::{CopyBuffer, AsyncIOBuf}; 10 | use super::bidi_copy_buf; 11 | 12 | /// Unix pipe. 13 | pub struct Pipe(RawFd, RawFd); 14 | 15 | impl Pipe { 16 | pub fn new() -> Result { 17 | use libc::{c_int, O_NONBLOCK}; 18 | use pipe_ctl::DF_PIPE_SIZE; 19 | 20 | let mut pipe = std::mem::MaybeUninit::<[c_int; 2]>::uninit(); 21 | unsafe { 22 | if libc::pipe2(pipe.as_mut_ptr() as *mut c_int, O_NONBLOCK) < 0 { 23 | return Err(Error::last_os_error()); 24 | } 25 | 26 | let [rd, wr] = pipe.assume_init(); 27 | 28 | // ignore errno 29 | if pipe_size() != DF_PIPE_SIZE { 30 | libc::fcntl(wr, libc::F_SETPIPE_SZ, pipe_size()); 31 | } 32 | 33 | Ok(Pipe(rd, wr)) 34 | } 35 | } 36 | } 37 | 38 | impl Drop for Pipe { 39 | fn drop(&mut self) { 40 | unsafe { 41 | libc::close(self.0); 42 | libc::close(self.1); 43 | } 44 | } 45 | } 46 | 47 | /// Type traits of those can be zero-copied. 48 | pub trait AsyncRawIO: AsyncRead + AsyncWrite + AsRawFd { 49 | fn x_poll_read_ready(&self, cx: &mut Context<'_>) -> Poll>; 50 | fn x_poll_write_ready(&self, cx: &mut Context<'_>) -> Poll>; 51 | fn x_try_io(&self, interest: Interest, f: impl FnOnce() -> Result) -> Result; 52 | } 53 | 54 | impl AsyncIOBuf for CopyBuffer 55 | where 56 | SR: AsyncRawIO + Unpin, 57 | SW: AsyncRawIO + Unpin, 58 | { 59 | type StreamR = SR; 60 | type StreamW = SW; 61 | 62 | fn poll_read_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamR) -> Poll> { 63 | loop { 64 | ready!(stream.x_poll_read_ready(cx))?; 65 | 66 | let mut is_wouldblock = false; 67 | let res = stream.x_try_io(Interest::READABLE, || { 68 | match splice_n(stream.as_raw_fd(), self.buf.1, usize::MAX) { 69 | x if x >= 0 => Ok(x as usize), 70 | _ => Err(handle_wouldblock(&mut is_wouldblock)), 71 | } 72 | }); 73 | 74 | if !is_wouldblock { 75 | return Poll::Ready(res); 76 | } 77 | } 78 | } 79 | 80 | fn poll_write_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamW) -> Poll> { 81 | loop { 82 | ready!(stream.x_poll_write_ready(cx)?); 83 | 84 | let mut is_wouldblock = false; 85 | let res = stream.x_try_io(Interest::WRITABLE, || { 86 | match splice_n(self.buf.0, stream.as_raw_fd(), self.cap - self.pos) { 87 | x if x >= 0 => Ok(x as usize), 88 | _ => Err(handle_wouldblock(&mut is_wouldblock)), 89 | } 90 | }); 91 | 92 | if !is_wouldblock { 93 | return Poll::Ready(res); 94 | } 95 | } 96 | } 97 | 98 | fn poll_flush_buf(&mut self, cx: &mut Context<'_>, stream: &mut Self::StreamW) -> Poll> { 99 | Pin::new(stream).poll_flush(cx) 100 | } 101 | } 102 | 103 | #[inline] 104 | fn splice_n(r: RawFd, w: RawFd, n: usize) -> isize { 105 | use libc::{loff_t, SPLICE_F_MOVE, SPLICE_F_NONBLOCK}; 106 | unsafe { 107 | libc::splice( 108 | r, 109 | std::ptr::null_mut::(), 110 | w, 111 | std::ptr::null_mut::(), 112 | n, 113 | SPLICE_F_MOVE | SPLICE_F_NONBLOCK, 114 | ) 115 | } 116 | } 117 | 118 | #[inline] 119 | fn handle_wouldblock(is_wouldblock: &mut bool) -> Error { 120 | use libc::{EWOULDBLOCK, EAGAIN}; 121 | let err = Error::last_os_error(); 122 | match err.raw_os_error() { 123 | Some(e) if e == EWOULDBLOCK || e == EAGAIN => { 124 | *is_wouldblock = true; 125 | ErrorKind::WouldBlock.into() 126 | } 127 | _ => err, 128 | } 129 | } 130 | 131 | mod tokio_net { 132 | use tokio::net::{TcpStream, UnixStream}; 133 | use super::AsyncRawIO; 134 | use super::*; 135 | 136 | /// Impl [`AsyncRawIO`], delegates to required functions. 137 | #[macro_export] 138 | macro_rules! delegate_impl { 139 | ($stream: ident) => { 140 | impl AsyncRawIO for $stream { 141 | #[inline] 142 | fn x_poll_read_ready(&self, cx: &mut Context<'_>) -> Poll> { 143 | self.poll_read_ready(cx) 144 | } 145 | 146 | #[inline] 147 | fn x_poll_write_ready(&self, cx: &mut Context<'_>) -> Poll> { 148 | self.poll_write_ready(cx) 149 | } 150 | 151 | #[inline] 152 | fn x_try_io(&self, interest: Interest, f: impl FnOnce() -> Result) -> Result { 153 | self.try_io(interest, f) 154 | } 155 | } 156 | }; 157 | } 158 | 159 | delegate_impl!(TcpStream); 160 | delegate_impl!(UnixStream); 161 | } 162 | 163 | /// Copy data bidirectionally between two streams with pipe. 164 | pub async fn bidi_zero_copy(a: &mut A, b: &mut B) -> Result<()> 165 | where 166 | A: AsyncRawIO + Unpin, 167 | B: AsyncRawIO + Unpin, 168 | { 169 | let a_to_b_buf = CopyBuffer::new(Pipe::new()?); 170 | let b_to_a_buf = CopyBuffer::new(Pipe::new()?); 171 | bidi_copy_buf(a, b, a_to_b_buf, b_to_a_buf).await 172 | } 173 | 174 | mod pipe_ctl { 175 | pub const DF_PIPE_SIZE: usize = 16 * 0x1000; 176 | static mut PIPE_SIZE: usize = DF_PIPE_SIZE; 177 | 178 | /// Get pipe capacity. 179 | #[inline] 180 | pub fn pipe_size() -> usize { 181 | unsafe { PIPE_SIZE } 182 | } 183 | 184 | /// Set pipe capacity. 185 | #[inline] 186 | pub fn set_pipe_size(n: usize) { 187 | unsafe { PIPE_SIZE = n } 188 | } 189 | } 190 | 191 | pub use pipe_ctl::{pipe_size, set_pipe_size}; 192 | -------------------------------------------------------------------------------- /src/conf/endpoint.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; 3 | 4 | use realm_core::endpoint::{Endpoint, RemoteAddr}; 5 | 6 | #[cfg(feature = "balance")] 7 | use realm_core::balance::Balancer; 8 | 9 | #[cfg(feature = "transport")] 10 | use realm_core::kaminari::mix::{MixAccept, MixConnect}; 11 | 12 | use super::{Config, NetConf, NetInfo}; 13 | 14 | #[derive(Debug, Serialize, Deserialize)] 15 | pub struct EndpointConf { 16 | pub listen: String, 17 | 18 | pub remote: String, 19 | 20 | #[serde(default)] 21 | #[serde(skip_serializing_if = "Vec::is_empty")] 22 | pub extra_remotes: Vec, 23 | 24 | #[serde(default)] 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub balance: Option, 27 | 28 | #[serde(default)] 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub through: Option, 31 | 32 | #[serde(default)] 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub interface: Option, 35 | 36 | #[serde(default)] 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub listen_transport: Option, 39 | 40 | #[serde(default)] 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub remote_transport: Option, 43 | 44 | #[serde(default)] 45 | #[serde(skip_serializing_if = "Config::is_empty")] 46 | pub network: NetConf, 47 | } 48 | 49 | impl EndpointConf { 50 | fn build_local(&self) -> SocketAddr { 51 | self.listen 52 | .to_socket_addrs() 53 | .expect("invalid local address") 54 | .next() 55 | .unwrap() 56 | } 57 | 58 | fn build_remote(&self) -> RemoteAddr { 59 | Self::build_remote_x(&self.remote) 60 | } 61 | 62 | fn build_remote_x(remote: &str) -> RemoteAddr { 63 | if let Ok(sockaddr) = remote.parse::() { 64 | RemoteAddr::SocketAddr(sockaddr) 65 | } else { 66 | let mut iter = remote.rsplitn(2, ':'); 67 | let port = iter.next().unwrap().parse::().unwrap(); 68 | let addr = iter.next().unwrap().to_string(); 69 | // test addr 70 | remote.to_socket_addrs().unwrap().next().unwrap(); 71 | RemoteAddr::DomainName(addr, port) 72 | } 73 | } 74 | 75 | fn build_send_through(&self) -> Option { 76 | let Self { through, .. } = self; 77 | let through = match through { 78 | Some(x) => x, 79 | None => return None, 80 | }; 81 | match through.to_socket_addrs() { 82 | Ok(mut x) => Some(x.next().unwrap()), 83 | Err(_) => { 84 | let mut ipstr = String::from(through); 85 | ipstr.retain(|c| c != '[' && c != ']'); 86 | ipstr.parse::().map_or(None, |ip| Some(SocketAddr::new(ip, 0))) 87 | } 88 | } 89 | } 90 | 91 | #[cfg(feature = "balance")] 92 | fn build_balancer(&self) -> Balancer { 93 | if let Some(s) = &self.balance { 94 | Balancer::parse_from_str(s) 95 | } else { 96 | Balancer::default() 97 | } 98 | } 99 | 100 | #[cfg(feature = "transport")] 101 | fn build_transport(&self) -> Option<(MixAccept, MixConnect)> { 102 | use realm_core::kaminari::mix::{MixClientConf, MixServerConf}; 103 | use realm_core::kaminari::opt::get_ws_conf; 104 | use realm_core::kaminari::opt::get_tls_client_conf; 105 | use realm_core::kaminari::opt::get_tls_server_conf; 106 | 107 | let Self { 108 | listen_transport, 109 | remote_transport, 110 | .. 111 | } = self; 112 | 113 | let listen_ws = listen_transport.as_ref().and_then(|s| get_ws_conf(s)); 114 | let listen_tls = listen_transport.as_ref().and_then(|s| get_tls_server_conf(s)); 115 | 116 | let remote_ws = remote_transport.as_ref().and_then(|s| get_ws_conf(s)); 117 | let remote_tls = remote_transport.as_ref().and_then(|s| get_tls_client_conf(s)); 118 | 119 | if matches!( 120 | (&listen_ws, &listen_tls, &remote_ws, &remote_tls), 121 | (None, None, None, None) 122 | ) { 123 | None 124 | } else { 125 | let ac = MixAccept::new_shared(MixServerConf { 126 | ws: listen_ws, 127 | tls: listen_tls, 128 | }); 129 | let cc = MixConnect::new_shared(MixClientConf { 130 | ws: remote_ws, 131 | tls: remote_tls, 132 | }); 133 | Some((ac, cc)) 134 | } 135 | } 136 | } 137 | 138 | #[derive(Debug)] 139 | pub struct EndpointInfo { 140 | pub no_tcp: bool, 141 | pub use_udp: bool, 142 | pub endpoint: Endpoint, 143 | } 144 | 145 | impl Config for EndpointConf { 146 | type Output = EndpointInfo; 147 | 148 | fn is_empty(&self) -> bool { 149 | false 150 | } 151 | 152 | fn build(self) -> Self::Output { 153 | let laddr = self.build_local(); 154 | let raddr = self.build_remote(); 155 | 156 | let extra_raddrs = self.extra_remotes.iter().map(|r| Self::build_remote_x(r)).collect(); 157 | 158 | // build partial conn_opts from netconf 159 | let NetInfo { 160 | mut conn_opts, 161 | no_tcp, 162 | use_udp, 163 | } = self.network.build(); 164 | 165 | // build left fields of conn_opts 166 | 167 | conn_opts.bind_address = self.build_send_through(); 168 | 169 | #[cfg(feature = "balance")] 170 | { 171 | conn_opts.balancer = self.build_balancer(); 172 | } 173 | 174 | #[cfg(feature = "transport")] 175 | { 176 | conn_opts.transport = self.build_transport(); 177 | } 178 | 179 | conn_opts.bind_interface = self.interface; 180 | 181 | EndpointInfo { 182 | no_tcp, 183 | use_udp, 184 | endpoint: Endpoint { 185 | laddr, 186 | raddr, 187 | conn_opts, 188 | extra_raddrs, 189 | }, 190 | } 191 | } 192 | 193 | fn rst_field(&mut self, _: &Self) -> &mut Self { 194 | unreachable!() 195 | } 196 | 197 | fn take_field(&mut self, _: &Self) -> &mut Self { 198 | unreachable!() 199 | } 200 | 201 | fn from_cmd_args(matches: &clap::ArgMatches) -> Self { 202 | let listen = matches.value_of("local").unwrap().to_string(); 203 | let remote = matches.value_of("remote").unwrap().to_string(); 204 | let through = matches.value_of("through").map(String::from); 205 | let interface = matches.value_of("interface").map(String::from); 206 | let listen_transport = matches.value_of("listen_transport").map(String::from); 207 | let remote_transport = matches.value_of("remote_transport").map(String::from); 208 | 209 | EndpointConf { 210 | listen, 211 | remote, 212 | through, 213 | interface, 214 | listen_transport, 215 | remote_transport, 216 | network: Default::default(), 217 | extra_remotes: Vec::new(), 218 | balance: None, 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/cmd/flag.rs: -------------------------------------------------------------------------------- 1 | use clap::{Command, Arg}; 2 | 3 | #[allow(clippy::let_and_return)] 4 | pub fn add_all(app: Command) -> Command { 5 | let app = add_flags(app); 6 | let app = add_options(app); 7 | let app = add_global_options(app); 8 | app 9 | } 10 | 11 | pub fn add_flags(app: Command) -> Command { 12 | app.next_help_heading("FLAGS").args(&[ 13 | Arg::new("help") 14 | .short('h') 15 | .long("help") 16 | .help("show help") 17 | .display_order(0), 18 | Arg::new("version") 19 | .short('v') 20 | .long("version") 21 | .help("show version") 22 | .display_order(1), 23 | Arg::new("daemon") 24 | .short('d') 25 | .long("daemon") 26 | .help("run as a unix daemon") 27 | .display_order(2), 28 | Arg::new("use_udp") 29 | .short('u') 30 | .long("udp") 31 | .help("force enable udp forward") 32 | .display_order(3), 33 | Arg::new("no_tcp") 34 | .short('t') 35 | .long("ntcp") 36 | .help("force disable tcp forward") 37 | .display_order(4), 38 | Arg::new("fast_open") 39 | .short('f') 40 | .long("tfo") 41 | .help("force enable tcp fast open -- deprecated") 42 | .display_order(5), 43 | Arg::new("zero_copy") 44 | .short('z') 45 | .long("splice") 46 | .help("force enable tcp zero copy -- deprecated") 47 | .display_order(6), 48 | ]) 49 | } 50 | 51 | pub fn add_options(app: Command) -> Command { 52 | app.next_help_heading("OPTIONS").args(&[ 53 | Arg::new("config") 54 | .short('c') 55 | .long("config") 56 | .help("use config file") 57 | .value_name("path") 58 | .takes_value(true) 59 | .display_order(0), 60 | Arg::new("local") 61 | .short('l') 62 | .long("listen") 63 | .help("listen address") 64 | .value_name("address") 65 | .takes_value(true) 66 | .display_order(1), 67 | Arg::new("remote") 68 | .short('r') 69 | .long("remote") 70 | .help("remote address") 71 | .value_name("address") 72 | .takes_value(true) 73 | .display_order(2), 74 | Arg::new("through") 75 | .short('x') 76 | .long("through") 77 | .help("send through ip or address") 78 | .value_name("address") 79 | .takes_value(true) 80 | .display_order(3), 81 | Arg::new("interface") 82 | .short('i') 83 | .long("interface") 84 | .help("bind to interface") 85 | .value_name("device") 86 | .takes_value(true) 87 | .display_order(4), 88 | Arg::new("listen_transport") 89 | .short('a') 90 | .long("listen-transport") 91 | .help("listen transport") 92 | .value_name("options") 93 | .takes_value(true) 94 | .display_order(5), 95 | Arg::new("remote_transport") 96 | .short('b') 97 | .long("remote-transport") 98 | .help("remote transport") 99 | .value_name("options") 100 | .takes_value(true) 101 | .display_order(6), 102 | ]) 103 | } 104 | 105 | pub fn add_global_options(app: Command) -> Command { 106 | // sys 107 | let app = app.next_help_heading("SYS OPTIONS").args(&[ 108 | Arg::new("nofile") 109 | .short('n') 110 | .long("nofile") 111 | .help("set nofile limit") 112 | .value_name("limit") 113 | .takes_value(true) 114 | .display_order(0), 115 | Arg::new("pipe_page") 116 | .short('p') 117 | .long("pipe-page") 118 | .help("set pipe capacity") 119 | .value_name("number") 120 | .takes_value(true) 121 | .display_order(1), 122 | Arg::new("pre_conn_hook") 123 | .short('j') 124 | .long("pre-conn-hook") 125 | .help("set pre-connect hook") 126 | .value_name("path") 127 | .takes_value(true) 128 | .display_order(2), 129 | ]); 130 | 131 | // log 132 | let app = app.next_help_heading("LOG OPTIONS").args(&[ 133 | Arg::new("log_level") 134 | .long("log-level") 135 | .help("override log level") 136 | .value_name("level") 137 | .takes_value(true) 138 | .display_order(0), 139 | Arg::new("log_output") 140 | .long("log-output") 141 | .help("override log output") 142 | .value_name("path") 143 | .takes_value(true) 144 | .display_order(1), 145 | ]); 146 | 147 | // dns 148 | let app = app.next_help_heading("DNS OPTIONS").args(&[ 149 | Arg::new("dns_mode") 150 | .long("dns-mode") 151 | .help("override dns mode") 152 | .value_name("mode") 153 | .takes_value(true) 154 | .display_order(0), 155 | Arg::new("dns_min_ttl") 156 | .long("dns-min-ttl") 157 | .help("override dns min ttl") 158 | .value_name("second") 159 | .takes_value(true) 160 | .display_order(1), 161 | Arg::new("dns_max_ttl") 162 | .long("dns-max-ttl") 163 | .help("override dns max ttl") 164 | .value_name("second") 165 | .takes_value(true) 166 | .display_order(2), 167 | Arg::new("dns_cache_size") 168 | .long("dns-cache-size") 169 | .help("override dns cache size") 170 | .value_name("number") 171 | .takes_value(true) 172 | .display_order(3), 173 | Arg::new("dns_protocol") 174 | .long("dns-protocol") 175 | .help("override dns protocol") 176 | .value_name("protocol") 177 | .takes_value(true) 178 | .display_order(4), 179 | Arg::new("dns_servers") 180 | .long("dns-servers") 181 | .help("override dns servers") 182 | .value_name("servers") 183 | .takes_value(true) 184 | .display_order(5), 185 | ]); 186 | 187 | // proxy-protocol belogs to network 188 | let app = app.next_help_heading("PROXY OPTIONS").args([ 189 | Arg::new("send_proxy") 190 | .long("send-proxy") 191 | .help("send proxy protocol header") 192 | .display_order(0), 193 | Arg::new("send_proxy_version") 194 | .long("send-proxy-version") 195 | .help("send proxy protocol version") 196 | .value_name("version") 197 | .takes_value(true) 198 | .display_order(1), 199 | Arg::new("accept_proxy") 200 | .long("accept-proxy") 201 | .help("accept proxy protocol header") 202 | .display_order(2), 203 | Arg::new("accept_proxy_timeout") 204 | .long("accept-proxy-timeout") 205 | .help("accept proxy protocol timeout") 206 | .value_name("second") 207 | .takes_value(true) 208 | .display_order(3), 209 | ]); 210 | 211 | // timeout belogs to network 212 | let app = app.next_help_heading("TIMEOUT OPTIONS").args([ 213 | Arg::new("tcp_timeout") 214 | .long("tcp-timeout") 215 | .help("override tcp timeout") 216 | .value_name("second") 217 | .takes_value(true) 218 | .display_order(0), 219 | Arg::new("udp_timeout") 220 | .long("udp-timeout") 221 | .help("override udp timeout") 222 | .value_name("second") 223 | .takes_value(true) 224 | .display_order(1), 225 | ]); 226 | 227 | app 228 | } 229 | -------------------------------------------------------------------------------- /realm_core/src/tcp/proxy.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind, Result}; 2 | use std::mem::MaybeUninit; 3 | use std::net::{SocketAddr, IpAddr, Ipv4Addr, Ipv6Addr}; 4 | 5 | use log::{info, debug}; 6 | use bytes::{BytesMut, Buf}; 7 | 8 | use proxy_protocol::ProxyHeader; 9 | use proxy_protocol::{version1 as v1, version2 as v2}; 10 | use proxy_protocol::{encode, parse}; 11 | 12 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 13 | use tokio::net::TcpStream; 14 | 15 | use crate::endpoint::ProxyOpts; 16 | use crate::time::timeoutfut; 17 | 18 | // TODO: replace the "proxy-protocol" crate, and then avoid heap allocation. 19 | 20 | // client -> relay -> server 21 | pub async fn handle_proxy(src: &mut TcpStream, dst: &mut TcpStream, opts: ProxyOpts) -> Result<()> { 22 | let ProxyOpts { 23 | send_proxy, 24 | accept_proxy, 25 | send_proxy_version, 26 | accept_proxy_timeout, 27 | } = opts; 28 | 29 | let mut client_addr = MaybeUninit::::uninit(); 30 | let mut server_addr = MaybeUninit::::uninit(); 31 | 32 | // buf may not be used 33 | let mut buf = MaybeUninit::::uninit(); 34 | 35 | // with src and dst got from header 36 | let mut fwd_hdr = false; 37 | 38 | // parse PROXY header from client and write log 39 | // may not get src and dst addr 40 | if accept_proxy { 41 | let buf = buf.write(BytesMut::with_capacity(256)); 42 | buf.resize(256, 0); 43 | 44 | // FIXME: may not read the entire header 45 | 46 | // The receiver may apply a short timeout and decide to 47 | // abort the connection if the protocol header is not seen 48 | // within a few seconds (at least 3 seconds to cover a TCP retransmit). 49 | let peek_n = timeoutfut(src.peek(buf), accept_proxy_timeout).await??; 50 | 51 | buf.truncate(peek_n); 52 | debug!("[tcp]peek initial {} bytes: {:#x}", peek_n, buf); 53 | 54 | let mut slice = buf.as_ref(); 55 | 56 | // slice is advanced 57 | let header = parse(&mut slice).map_err(|e| Error::new(ErrorKind::Other, e))?; 58 | let parsed_n = peek_n - slice.remaining(); 59 | debug!("[tcp]proxy-protocol parsed, {} bytes", parsed_n); 60 | 61 | // handle parsed header, and print log 62 | if let Some((src, dst)) = handle_header(header) { 63 | client_addr.write(src); 64 | server_addr.write(dst); 65 | fwd_hdr = true; 66 | } 67 | 68 | // header has been parsed, remove these bytes from sock buffer. 69 | buf.truncate(parsed_n); 70 | src.read_exact(buf).await?; 71 | 72 | // do not send header to server 73 | if !send_proxy { 74 | return Ok(()); 75 | } 76 | } 77 | 78 | // use real addr 79 | if !fwd_hdr { 80 | client_addr.write(src.peer_addr()?); 81 | // FIXME: what is the dst addr here? seems not defined in the doc 82 | // the doc only mentions that this field is similar to X-Origin-To 83 | // which is seldom used 84 | server_addr.write(match unsafe { client_addr.assume_init_ref() } { 85 | SocketAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), 86 | SocketAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0), 87 | }); 88 | } 89 | 90 | // Safety: sockaddr is always initialized 91 | // either parse from PROXY header or use real addr 92 | let client_addr = unsafe { client_addr.assume_init() }; 93 | let server_addr = unsafe { server_addr.assume_init() }; 94 | 95 | // write header 96 | let header = encode(make_header(client_addr, server_addr, send_proxy_version)) 97 | .map_err(|e| Error::new(ErrorKind::Other, e))?; 98 | debug!("[tcp]send initial {} bytes: {:#x}", header.len(), &header); 99 | dst.write_all(&header).await?; 100 | 101 | Ok(()) 102 | } 103 | 104 | macro_rules! unpack { 105 | ($addr: expr, sin4) => { 106 | match $addr { 107 | SocketAddr::V4(x) => x, 108 | _ => unreachable!(), 109 | } 110 | }; 111 | ($addr: expr, sin6) => { 112 | match $addr { 113 | SocketAddr::V6(x) => x, 114 | _ => unreachable!(), 115 | } 116 | }; 117 | } 118 | 119 | fn make_header(client_addr: SocketAddr, server_addr: SocketAddr, send_proxy_version: usize) -> ProxyHeader { 120 | match send_proxy_version { 121 | 2 => make_header_v2(client_addr, server_addr), 122 | 1 => make_header_v1(client_addr, server_addr), 123 | _ => unreachable!(), 124 | } 125 | } 126 | 127 | fn make_header_v1(client_addr: SocketAddr, server_addr: SocketAddr) -> ProxyHeader { 128 | debug!("[tcp]send proxy-protocol-v1: {} => {}", &client_addr, &server_addr); 129 | 130 | if client_addr.is_ipv4() { 131 | ProxyHeader::Version1 { 132 | addresses: v1::ProxyAddresses::Ipv4 { 133 | source: unpack!(client_addr, sin4), 134 | destination: unpack!(server_addr, sin4), 135 | }, 136 | } 137 | } else { 138 | ProxyHeader::Version1 { 139 | addresses: v1::ProxyAddresses::Ipv6 { 140 | source: unpack!(client_addr, sin6), 141 | destination: unpack!(server_addr, sin6), 142 | }, 143 | } 144 | } 145 | } 146 | 147 | fn make_header_v2(client_addr: SocketAddr, server_addr: SocketAddr) -> ProxyHeader { 148 | debug!("[tcp]send proxy-protocol-v2: {} => {}", &client_addr, &server_addr); 149 | 150 | ProxyHeader::Version2 { 151 | command: v2::ProxyCommand::Proxy, 152 | transport_protocol: v2::ProxyTransportProtocol::Stream, 153 | addresses: if client_addr.is_ipv4() { 154 | v2::ProxyAddresses::Ipv4 { 155 | source: unpack!(client_addr, sin4), 156 | destination: unpack!(server_addr, sin4), 157 | } 158 | } else { 159 | v2::ProxyAddresses::Ipv6 { 160 | source: unpack!(client_addr, sin6), 161 | destination: unpack!(server_addr, sin6), 162 | } 163 | }, 164 | } 165 | } 166 | 167 | fn handle_header(header: ProxyHeader) -> Option<(SocketAddr, SocketAddr)> { 168 | use ProxyHeader::{Version1, Version2}; 169 | match header { 170 | Version1 { addresses } => handle_header_v1(addresses), 171 | Version2 { 172 | command, 173 | transport_protocol, 174 | addresses, 175 | } => handle_header_v2(command, transport_protocol, addresses), 176 | _ => { 177 | info!("[tcp]accept proxy-protocol-v?"); 178 | None 179 | } 180 | } 181 | } 182 | 183 | fn handle_header_v1(addr: v1::ProxyAddresses) -> Option<(SocketAddr, SocketAddr)> { 184 | use v1::ProxyAddresses::*; 185 | match addr { 186 | Unknown => { 187 | info!("[tcp]accept proxy-protocol-v1: unknown"); 188 | None 189 | } 190 | Ipv4 { source, destination } => { 191 | info!("[tcp]accept proxy-protocol-v1: {} => {}", &source, &destination); 192 | Some((SocketAddr::V4(source), SocketAddr::V4(destination))) 193 | } 194 | Ipv6 { source, destination } => { 195 | info!("[tcp]accept proxy-protocol-v1: {} => {}", &source, &destination); 196 | Some((SocketAddr::V6(source), SocketAddr::V6(destination))) 197 | } 198 | } 199 | } 200 | 201 | fn handle_header_v2( 202 | cmd: v2::ProxyCommand, 203 | proto: v2::ProxyTransportProtocol, 204 | addr: v2::ProxyAddresses, 205 | ) -> Option<(SocketAddr, SocketAddr)> { 206 | use v2::ProxyCommand as Command; 207 | use v2::ProxyAddresses as Address; 208 | use v2::ProxyTransportProtocol as Protocol; 209 | 210 | // The connection endpoints are the sender and the receiver. 211 | // Such connections exist when the proxy sends health-checks to the server. 212 | // The receiver must accept this connection as valid and must use the 213 | // real connection endpoints and discard the protocol block including the 214 | // family which is ignored 215 | if let Command::Local = cmd { 216 | info!("[tcp]accept proxy-protocol-v2: command = LOCAL, ignore"); 217 | return None; 218 | } 219 | 220 | // only get tcp address 221 | match proto { 222 | Protocol::Stream => {} 223 | Protocol::Unspec => { 224 | info!("[tcp]accept proxy-protocol-v2: protocol = UNSPEC, ignore"); 225 | return None; 226 | } 227 | Protocol::Datagram => { 228 | info!("[tcp]accept proxy-protocol-v2: protocol = DGRAM, ignore"); 229 | return None; 230 | } 231 | } 232 | 233 | match addr { 234 | Address::Ipv4 { source, destination } => { 235 | info!("[tcp]accept proxy-protocol-v2: {} => {}", &source, &destination); 236 | Some((SocketAddr::V4(source), SocketAddr::V4(destination))) 237 | } 238 | Address::Ipv6 { source, destination } => { 239 | info!("[tcp]accept proxy-protocol-v2: {} => {}", &source, &destination); 240 | Some((SocketAddr::V6(source), SocketAddr::V6(destination))) 241 | } 242 | Address::Unspec => { 243 | info!("[tcp]accept proxy-protocol-v2: af_family = AF_UNSPEC, ignore"); 244 | None 245 | } 246 | Address::Unix { .. } => { 247 | info!("[tcp]accept proxy-protocol-v2: af_family = AF_UNIX, ignore"); 248 | None 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /realm_lb/src/ip_hash.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use super::{Balance, Token}; 4 | 5 | /// Iphash node. 6 | #[derive(Debug)] 7 | struct Node { 8 | hash: u32, 9 | token: Token, 10 | } 11 | 12 | /// Iphash balancer. 13 | #[derive(Debug)] 14 | pub struct IpHash { 15 | nodes: Vec, 16 | total: u8, 17 | } 18 | 19 | impl Balance for IpHash { 20 | type State = IpAddr; 21 | 22 | fn total(&self) -> u8 { 23 | self.total 24 | } 25 | 26 | fn new(weights: &[u8]) -> Self { 27 | assert!(weights.len() <= u8::MAX as usize); 28 | 29 | if weights.len() <= 1 { 30 | return Self { 31 | nodes: Vec::new(), 32 | total: weights.len() as u8, 33 | }; 34 | } 35 | 36 | let ratio = replica_ratio(weights); 37 | let count = weights.iter().map(|x| *x as usize * ratio as usize).sum(); 38 | let mut nodes: Vec = Vec::with_capacity(count); 39 | 40 | for (n, weight) in weights.iter().map(|x| *x as usize * ratio as usize).enumerate() { 41 | let token = Token(n as u8); 42 | 43 | for vidx in 0..=weight { 44 | let buf = format!("{0} 114514", vidx); 45 | let hash = chash(buf.as_bytes()); 46 | nodes.push(Node { hash, token }); 47 | } 48 | } 49 | 50 | nodes.sort_unstable_by_key(|node| node.hash); 51 | 52 | Self { 53 | nodes, 54 | total: weights.len() as u8, 55 | } 56 | } 57 | 58 | fn next(&self, state: &Self::State) -> Option { 59 | if self.total <= 1 { 60 | return Some(Token(0)); 61 | } 62 | 63 | let hash = match state { 64 | IpAddr::V4(x) => chash_for_ip(&x.octets()), 65 | IpAddr::V6(x) => chash_for_ip(&x.octets()), 66 | }; 67 | 68 | let idx = match self.nodes.binary_search_by_key(&hash, |node| node.hash) { 69 | Ok(idx) => idx, 70 | Err(idx) if idx >= self.nodes.len() as usize => 0, 71 | Err(idx) => idx, 72 | }; 73 | 74 | Some(self.nodes[idx].token) 75 | } 76 | } 77 | 78 | use chash::{chash, chash_for_ip}; 79 | mod chash { 80 | const SEED: u32 = 0xbc9f1d34; 81 | const M: u32 = 0xc6a4a793; 82 | 83 | macro_rules! c_add { 84 | ($a:expr, $b:expr) => { 85 | unsafe { $a.unchecked_add($b) } 86 | }; 87 | } 88 | 89 | macro_rules! c_mul { 90 | ($a:expr, $b:expr) => { 91 | unsafe { $a.unchecked_mul($b) } 92 | }; 93 | } 94 | 95 | pub fn chash(buf: &[u8]) -> u32 { 96 | let mut h = SEED ^ c_mul!(buf.len() as u32, M); 97 | let mut b = buf; 98 | let mut len = buf.len(); 99 | 100 | while len >= 4 { 101 | h = c_add!( 102 | h, 103 | (b[0] as u32) | ((b[1] as u32) << 8) | ((b[2] as u32) << 16) | ((b[3] as u32) << 24) 104 | ); 105 | 106 | h = c_mul!(h, M); 107 | h ^= h >> 16; 108 | b = &b[4..]; 109 | len -= 4; 110 | } 111 | 112 | if len == 3 { 113 | h = c_add!(h, (b[2] as u32) << 16); 114 | len -= 1; 115 | } 116 | 117 | if len == 2 { 118 | h = c_add!(h, (b[1] as u32) << 8); 119 | len -= 1; 120 | } 121 | 122 | if len == 1 { 123 | h = c_add!(h, b[0] as u32); 124 | h = c_mul!(h, M); 125 | h ^= h >> 24; 126 | } 127 | 128 | h 129 | } 130 | 131 | pub fn chash_for_ip(buf: &[u8]) -> u32 { 132 | let mut h = SEED ^ c_mul!(buf.len() as u32, M); 133 | 134 | let (_, buf, _) = unsafe { buf.align_to::() }; 135 | 136 | for b in buf.iter().map(|x| x.to_le()) { 137 | h = c_add!(h, b); 138 | h = c_mul!(h, M); 139 | h ^= h >> 16; 140 | } 141 | 142 | h 143 | } 144 | } 145 | 146 | fn replica_ratio(weights: &[u8]) -> u8 { 147 | const MIN_REPLICA: u8 = 128; 148 | 149 | let max = *weights.iter().max().unwrap(); 150 | 151 | if max >= MIN_REPLICA { 152 | 1 153 | } else { 154 | f64::ceil(MIN_REPLICA as f64 / max as f64) as u8 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | use average::{Max, Mean, Min}; 162 | use std::net::{Ipv4Addr, Ipv6Addr}; 163 | 164 | #[test] 165 | fn ih_replica_ratios() { 166 | macro_rules! run { 167 | ($weights: expr, $ratio: expr) => {{ 168 | assert_eq!(replica_ratio($weights), $ratio); 169 | }}; 170 | } 171 | 172 | run!(&[1], 128); 173 | run!(&[1, 1, 2], 64); 174 | run!(&[1, 1, 2, 2, 3], 43); 175 | run!(&[1, 1, 2, 2, 3, 3, 4], 32); 176 | run!(&[1, 1, 2, 2, 3, 3, 4, 4, 5], 26); 177 | run!(&[1, 1, 2, 2, 3, 3, 4, 4, 5, 10], 13); 178 | run!(&[1, 1, 2, 2, 3, 3, 4, 4, 5, 10, 20], 7); 179 | run!(&[1, 1, 2, 2, 3, 3, 4, 4, 5, 10, 20, 30], 5); 180 | run!(&[1, 1, 2, 2, 3, 3, 4, 4, 5, 10, 20, 30, 50], 3); 181 | run!(&[1, 1, 2, 2, 3, 3, 4, 4, 5, 10, 20, 30, 50, 100], 2); 182 | run!(&[1, 2, 3, 4, 128], 1); 183 | run!(&[1, 2, 3, 4, 200], 1); 184 | run!(&[1, 2, 3, 4, 255], 1); 185 | } 186 | 187 | #[test] 188 | fn ih_any_hash() { 189 | macro_rules! run { 190 | ($str: expr, $res: expr) => {{ 191 | assert_eq!(chash($str), $res); 192 | }}; 193 | } 194 | 195 | run!(b"", 3164544308); 196 | run!(b"123", 4219602657); 197 | run!(b"1234567", 897539970); 198 | run!(b"abc", 2237464879); 199 | run!(b"abcdefg", 2383090994); 200 | run!(b"123abc", 2851751921); 201 | run!(b"abc123", 4002724297); 202 | run!(b"realm", 885396906); 203 | run!(b"1 realm", 4115282535); 204 | run!(b"2 realm", 1326782105); 205 | run!(b"3 realm", 1796078392); 206 | run!(b"10 realm", 2265248424); 207 | run!(b"100 realm", 4289654351); 208 | } 209 | 210 | #[test] 211 | fn ih_ip_hash() { 212 | macro_rules! run { 213 | ($ip: expr) => {{ 214 | let b = $ip.octets(); 215 | assert_eq!(chash(&b), chash_for_ip(&b)); 216 | }}; 217 | (=> $ip: expr) => {{ 218 | let ip = $ip.parse::().unwrap(); 219 | run!(ip); 220 | }}; 221 | } 222 | 223 | for i in (0..=u32::MAX).step_by(127) { 224 | run!(Ipv4Addr::from(i)); 225 | } 226 | 227 | run!(=>"::0"); 228 | run!(=>"::1"); 229 | run!(=>"::ffff:127.0.0.1"); 230 | run!(=>"2001:4860:4860::8844"); 231 | run!(=>"2001:4860:4860::8888"); 232 | run!(=>"2606:4700:4700::1001"); 233 | run!(=>"2606:4700:4700::1111"); 234 | run!(=>"fd9d:bb35:94bf:c38a:ee1:c75d:8df3:c909"); 235 | } 236 | 237 | #[test] 238 | fn ih_same_ip() { 239 | let ip1 = "1.1.1.1".parse::().unwrap(); 240 | let ip2 = "8.8.8.8".parse::().unwrap(); 241 | let ip3 = "114.51.4.19".parse::().unwrap(); 242 | let ip4 = "2001:4860:4860::8888".parse::().unwrap(); 243 | 244 | let iphash = IpHash::new(&vec![1, 2, 3, 4]); 245 | assert_eq!(iphash.total, 4); 246 | assert!(iphash.nodes.len() >= (1 + 2 + 3 + 4) * 128 / 4); 247 | 248 | let ip1_node = iphash.next(&ip1); 249 | let ip2_node = iphash.next(&ip2); 250 | let ip3_node = iphash.next(&ip3); 251 | let ip4_node = iphash.next(&ip4); 252 | 253 | for _ in 0..16 { 254 | assert_eq!(iphash.next(&ip1), ip1_node); 255 | assert_eq!(iphash.next(&ip2), ip2_node); 256 | assert_eq!(iphash.next(&ip3), ip3_node); 257 | assert_eq!(iphash.next(&ip4), ip4_node); 258 | } 259 | } 260 | 261 | #[test] 262 | fn ih_same_weight() { 263 | let iphash = IpHash::new(&vec![1; 16]); 264 | let mut distro = [0f64; 16]; 265 | 266 | let mut total: usize = 0; 267 | for ip in (0..=u32::MAX).map(Ipv4Addr::from).map(IpAddr::from).step_by(127) { 268 | let token = iphash.next(&ip).unwrap(); 269 | distro[token.0 as usize] += 1 as f64; 270 | total += 1; 271 | } 272 | 273 | let diffs: Vec = distro 274 | .iter() 275 | .map(|x| *x / total as f64 - 1.0 / 16.0) 276 | .map(f64::abs) 277 | .collect(); 278 | 279 | let min_diff: Min = diffs.iter().collect(); 280 | let max_diff: Max = diffs.iter().collect(); 281 | let mean_diff: Mean = diffs.iter().collect(); 282 | 283 | println!("{:?}", distro); 284 | println!("min diff: {}", min_diff.min()); 285 | println!("max diff: {}", max_diff.max()); 286 | println!("mean diff: {}", mean_diff.mean()); 287 | } 288 | 289 | #[test] 290 | fn ih_all_weights() { 291 | let weights: Vec = (1..=16).collect(); 292 | let iphash = IpHash::new(&weights); 293 | let mut distro = [0f64; 16]; 294 | 295 | let mut total: usize = 0; 296 | for ip in (0..=u32::MAX).map(Ipv4Addr::from).map(IpAddr::from).step_by(127) { 297 | let token = iphash.next(&ip).unwrap(); 298 | distro[token.0 as usize] += 1 as f64; 299 | total += 1; 300 | } 301 | 302 | let diffs: Vec = distro 303 | .iter() 304 | .enumerate() 305 | .map(|(i, x)| *x / total as f64 - (i as f64 + 1.0) / 16.0) 306 | .map(f64::abs) 307 | .collect(); 308 | 309 | let min_diff: Min = diffs.iter().collect(); 310 | let max_diff: Max = diffs.iter().collect(); 311 | let mean_diff: Mean = diffs.iter().collect(); 312 | 313 | println!("{:?}", distro); 314 | println!("min diff: {}", min_diff.min()); 315 | println!("max diff: {}", max_diff.max()); 316 | println!("mean diff: {}", mean_diff.mean()); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/conf/dns.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Display}; 2 | use std::net::ToSocketAddrs; 3 | 4 | use serde::{Serialize, Deserialize}; 5 | use realm_core::dns::config; 6 | use config::{LookupIpStrategy, NameServerConfig, Protocol}; 7 | use config::{ResolverConfig, ResolverOpts}; 8 | 9 | use super::Config; 10 | 11 | // dns mode 12 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 13 | #[serde(rename_all = "snake_case")] 14 | pub enum DnsMode { 15 | Ipv4Only, 16 | Ipv6Only, 17 | Ipv4AndIpv6, 18 | Ipv4ThenIpv6, 19 | Ipv6ThenIpv4, 20 | } 21 | 22 | impl Default for DnsMode { 23 | fn default() -> Self { 24 | Self::Ipv4AndIpv6 25 | } 26 | } 27 | 28 | impl Display for DnsMode { 29 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 30 | use DnsMode::*; 31 | let s = match self { 32 | Ipv4Only => "ipv4_only", 33 | Ipv6Only => "ipv6_only", 34 | Ipv4AndIpv6 => "ipv4_and_ipv6", 35 | Ipv4ThenIpv6 => "ipv4_then_ipv6", 36 | Ipv6ThenIpv4 => "ipv6_then_ipv4", 37 | }; 38 | write!(f, "{}", s) 39 | } 40 | } 41 | 42 | impl From for DnsMode { 43 | fn from(s: String) -> Self { 44 | use DnsMode::*; 45 | match s.to_ascii_lowercase().as_str() { 46 | "ipv4_only" => Ipv4Only, 47 | "ipv6_only" => Ipv6Only, 48 | "ipv4_and_ipv6" => Ipv4AndIpv6, 49 | "ipv4_then_ipv6" => Ipv4ThenIpv6, 50 | "ipv6_then_ipv4" => Ipv6ThenIpv4, 51 | _ => Self::default(), 52 | } 53 | } 54 | } 55 | 56 | impl From for LookupIpStrategy { 57 | fn from(mode: DnsMode) -> Self { 58 | match mode { 59 | DnsMode::Ipv4Only => LookupIpStrategy::Ipv4Only, 60 | DnsMode::Ipv6Only => LookupIpStrategy::Ipv6Only, 61 | DnsMode::Ipv4AndIpv6 => LookupIpStrategy::Ipv4AndIpv6, 62 | DnsMode::Ipv4ThenIpv6 => LookupIpStrategy::Ipv4thenIpv6, 63 | DnsMode::Ipv6ThenIpv4 => LookupIpStrategy::Ipv6thenIpv4, 64 | } 65 | } 66 | } 67 | 68 | // dns protocol 69 | #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Copy)] 70 | #[serde(rename_all = "snake_case")] 71 | pub enum DnsProtocol { 72 | Tcp, 73 | Udp, 74 | TcpAndUdp, 75 | } 76 | 77 | impl Default for DnsProtocol { 78 | fn default() -> Self { 79 | Self::TcpAndUdp 80 | } 81 | } 82 | 83 | impl Display for DnsProtocol { 84 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 85 | use DnsProtocol::*; 86 | let s = match self { 87 | Tcp => "tcp", 88 | Udp => "udp", 89 | TcpAndUdp => "tcp+udp", 90 | }; 91 | write!(f, "{}", s) 92 | } 93 | } 94 | 95 | impl From for DnsProtocol { 96 | fn from(s: String) -> Self { 97 | use DnsProtocol::*; 98 | match s.to_ascii_lowercase().as_str() { 99 | "tcp" => Tcp, 100 | "udp" => Udp, 101 | _ => TcpAndUdp, 102 | } 103 | } 104 | } 105 | 106 | impl From for Vec { 107 | fn from(x: DnsProtocol) -> Self { 108 | use DnsProtocol::*; 109 | match x { 110 | Tcp => vec![Protocol::Tcp], 111 | Udp => vec![Protocol::Udp], 112 | TcpAndUdp => vec![Protocol::Tcp, Protocol::Udp], 113 | } 114 | } 115 | } 116 | 117 | // dns config 118 | #[derive(Debug, Default, Serialize, Deserialize, Clone)] 119 | pub struct DnsConf { 120 | // ResolverOpts 121 | #[serde(default)] 122 | #[serde(skip_serializing_if = "Option::is_none")] 123 | pub mode: Option, 124 | 125 | // MAX_TTL: u32 = 86400_u32 126 | // https://docs.rs/trust-dns-resolver/latest/src/trust_dns_resolver/dns_lru.rs.html#26 127 | #[serde(default)] 128 | #[serde(skip_serializing_if = "Option::is_none")] 129 | pub min_ttl: Option, 130 | 131 | #[serde(default)] 132 | #[serde(skip_serializing_if = "Option::is_none")] 133 | pub max_ttl: Option, 134 | 135 | #[serde(default)] 136 | #[serde(skip_serializing_if = "Option::is_none")] 137 | pub cache_size: Option, 138 | 139 | // ResolverConfig 140 | #[serde(default)] 141 | #[serde(skip_serializing_if = "Option::is_none")] 142 | pub protocol: Option, 143 | 144 | #[serde(default)] 145 | #[serde(skip_serializing_if = "Option::is_none")] 146 | pub nameservers: Option>, 147 | } 148 | 149 | impl Display for DnsConf { 150 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 151 | macro_rules! default { 152 | ($ref: expr) => { 153 | match $ref { 154 | Some(x) => *x, 155 | None => Default::default(), 156 | } 157 | }; 158 | ($ref: expr, $value: expr) => { 159 | match $ref { 160 | Some(x) => *x, 161 | None => $value, 162 | } 163 | }; 164 | } 165 | let DnsConf { 166 | mode, 167 | min_ttl, 168 | max_ttl, 169 | cache_size, 170 | protocol, 171 | nameservers, 172 | } = self; 173 | 174 | let mode = default!(mode); 175 | 176 | let min_ttl = default!(min_ttl, 0_u32); 177 | 178 | let max_ttl = default!(max_ttl, 86400_u32); 179 | 180 | let cache_size = default!(cache_size, 32_usize); 181 | 182 | let protocol = default!(protocol); 183 | 184 | let nameservers = match nameservers { 185 | Some(s) => s.join(", "), 186 | None => String::from("system"), 187 | }; 188 | 189 | write!(f, "mode={}, protocol={}, ", &mode, &protocol).unwrap(); 190 | write!( 191 | f, 192 | "min-ttl={}, max-ttl={}, cache-size={}, ", 193 | min_ttl, max_ttl, cache_size 194 | ) 195 | .unwrap(); 196 | write!(f, "servers={}", &nameservers) 197 | } 198 | } 199 | 200 | impl Config for DnsConf { 201 | type Output = (Option, Option); 202 | 203 | fn build(self) -> Self::Output { 204 | use crate::empty; 205 | use std::time::Duration; 206 | 207 | let DnsConf { 208 | mode, 209 | protocol, 210 | nameservers, 211 | min_ttl, 212 | max_ttl, 213 | cache_size, 214 | } = self; 215 | 216 | // parse into ResolverOpts 217 | // default value: 218 | // https://docs.rs/trust-dns-resolver/latest/src/trust_dns_resolver/config.rs.html#681-737 219 | 220 | let opts = if empty![mode, min_ttl, max_ttl, cache_size] { 221 | None 222 | } else { 223 | let ip_strategy: LookupIpStrategy = mode.map(|x| x.into()).unwrap_or_default(); 224 | 225 | let positive_min_ttl = min_ttl.map(|x| Duration::from_secs(x as u64)); 226 | 227 | let positive_max_ttl = max_ttl.map(|x| Duration::from_secs(x as u64)); 228 | 229 | let cache_size = cache_size.unwrap_or({ 230 | let ResolverOpts { cache_size, .. } = Default::default(); 231 | cache_size 232 | }); 233 | 234 | let mut opts = ResolverOpts::default(); 235 | 236 | macro_rules! replace { 237 | ($($x: ident, )+) => { 238 | $( 239 | opts.$x = $x; 240 | )+ 241 | } 242 | } 243 | 244 | replace!(ip_strategy, positive_min_ttl, positive_max_ttl, cache_size,); 245 | 246 | Some(opts) 247 | }; 248 | 249 | // parse into ResolverConfig 250 | let protocol = protocol.unwrap_or_default(); 251 | if nameservers.is_none() && (protocol == DnsProtocol::default()) { 252 | return (None, opts); 253 | } 254 | 255 | let mut conf = ResolverConfig::new(); 256 | let protocols: Vec = protocol.into(); 257 | let nameservers = match nameservers { 258 | Some(addrs) => addrs 259 | .iter() 260 | .map(|x| x.to_socket_addrs().unwrap().next().unwrap()) 261 | .collect(), 262 | None => { 263 | use realm_core::dns::DnsConf as TrustDnsConf; 264 | let TrustDnsConf { conf, .. } = TrustDnsConf::default(); 265 | let mut addrs: Vec = conf.name_servers().iter().map(|x| x.socket_addr).collect(); 266 | addrs.dedup(); 267 | addrs 268 | } 269 | }; 270 | 271 | for socket_addr in nameservers { 272 | for protocol in protocols.clone() { 273 | conf.add_name_server(NameServerConfig { 274 | socket_addr, 275 | protocol, 276 | tls_dns_name: None, 277 | trust_nx_responses: true, 278 | bind_addr: None, 279 | }); 280 | } 281 | } 282 | 283 | (Some(conf), opts) 284 | } 285 | 286 | fn rst_field(&mut self, other: &Self) -> &mut Self { 287 | use crate::rst; 288 | let other = other.clone(); 289 | rst!(self, mode, other); 290 | rst!(self, min_ttl, other); 291 | rst!(self, max_ttl, other); 292 | rst!(self, cache_size, other); 293 | rst!(self, protocol, other); 294 | rst!(self, nameservers, other); 295 | self 296 | } 297 | 298 | fn take_field(&mut self, other: &Self) -> &mut Self { 299 | use crate::take; 300 | let other = other.clone(); 301 | take!(self, mode, other); 302 | take!(self, min_ttl, other); 303 | take!(self, max_ttl, other); 304 | take!(self, cache_size, other); 305 | take!(self, protocol, other); 306 | take!(self, nameservers, other); 307 | self 308 | } 309 | 310 | fn from_cmd_args(matches: &clap::ArgMatches) -> Self { 311 | let mode = matches.value_of("dns_mode").map(|x| String::from(x).into()); 312 | 313 | let min_ttl = matches.value_of("dns_min_ttl").map(|x| x.parse::().unwrap()); 314 | 315 | let max_ttl = matches.value_of("dns_max_ttl").map(|x| x.parse::().unwrap()); 316 | 317 | let cache_size = matches.value_of("dns_cache_size").map(|x| x.parse::().unwrap()); 318 | 319 | let protocol = matches.value_of("dns_protocol").map(|x| String::from(x).into()); 320 | 321 | let nameservers = matches 322 | .value_of("dns_servers") 323 | .map(|x| x.split(',').map(String::from).collect()); 324 | 325 | Self { 326 | mode, 327 | min_ttl, 328 | max_ttl, 329 | cache_size, 330 | protocol, 331 | nameservers, 332 | } 333 | } 334 | 335 | fn is_empty(&self) -> bool { 336 | crate::empty![self => mode, min_ttl, max_ttl, cache_size] 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Realm 2 | 3 | A simple, high performance relay server written in rust. 4 | 5 |

6 | 7 | [![realm](https://github.com/zhboner/realm/workflows/ci/badge.svg)](https://github.com/zhboner/realm/actions) 8 | [![realm](https://github.com/zhboner/realm/workflows/build/badge.svg)](https://github.com/zhboner/realm/actions/workflows/cross_compile.yml) 9 | [![downloads](https://img.shields.io/github/downloads/zhboner/realm/total?color=green)](https://github.com/zhboner/realm/releases) 10 | 11 | 12 | ## Libs 13 | 14 | | lib | doc | 15 | | ----- | ----- | 16 | | realm-core | [![crates.io](https://img.shields.io/crates/v/realm_core.svg)](https://crates.io/crates/realm_core) [![Released API docs](https://docs.rs/realm_core/badge.svg)](https://docs.rs/realm_core) | 17 | | realm-io | [![crates.io](https://img.shields.io/crates/v/realm_io.svg)](https://crates.io/crates/realm_io) [![Released API docs](https://docs.rs/realm_io/badge.svg)](https://docs.rs/realm_io) | 18 | | realm-lb | [![crates.io](https://img.shields.io/crates/v/realm_lb.svg)](https://crates.io/crates/realm_lb) [![Released API docs](https://docs.rs/realm_lb/badge.svg)](https://docs.rs/realm_lb) | 19 | | realm-hook | [![crates.io](https://img.shields.io/crates/v/realm_hook.svg)](https://crates.io/crates/realm_hook) [![Released API docs](https://docs.rs/realm_hook/badge.svg)](https://docs.rs/realm_hook)| 20 | | realm-syscall | [![crates.io](https://img.shields.io/crates/v/realm_syscall.svg)](https://crates.io/crates/realm_syscall) [![Released API docs](https://docs.rs/realm_syscall/badge.svg)](https://docs.rs/realm_syscall) | 21 | 22 | ## Features 23 | 24 | - Zero configuration. Setup and run in one command. 25 | - Concurrency. Bidirectional concurrent traffic leads to high performance. 26 | - Low resources cost. 27 | 28 | ## Container 29 | 30 | Realm can be run in a container with OCI (like Docker, Podman, Kubernetes, etc), see guides [here](readme.container.md). 31 | 32 | ## Build 33 | 34 | Install rust toolchain with [rustup](https://rustup.rs/): 35 | 36 | ```shell 37 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 38 | ``` 39 | 40 | Clone this repository: 41 | 42 | ```shell 43 | git clone https://github.com/zhboner/realm && cd realm 44 | ``` 45 | 46 | Build: 47 | 48 | ```shell 49 | cargo build --release 50 | ``` 51 | 52 | You can also pass `target_cpu=native` to allow more possible optimizations: 53 | 54 | ```shell 55 | RUSTFLAGS='-C target_cpu=native' cargo build --release 56 | ``` 57 | 58 | The `realm` binary will be available in `target/release`. 59 | 60 | ### Build Options 61 | 62 | - ~~udp: enable udp relay~~ builtin. 63 | - ~~tfo: enable tcp-fast-open~~ deprecated. 64 | - ~~trust-dns: enable trust-dns's async dns resolver~~ builtin. 65 | - ~~zero-copy: enable zero-copy on linux~~ builtin. 66 | - brutal-shutdown: see [realm_io/brutal-shutdown](realm_io/README.md#about-brutal-shutdown). 67 | - hook: see [realm_hook](realm_hook/README.md). 68 | - proxy: enable proxy-protocol. 69 | - balance: enable load balance. 70 | - transport: enable ws/tls/wss. 71 | - multi-thread: enable tokio's multi-threaded IO scheduler. 72 | - mi-malloc: custom memory allocator. 73 | - jemalloc: custom memory allocator. 74 | - page-alloc: custom memory allocator. 75 | 76 | Default: hook + proxy + balance + transport + brutal-shutdown + multi-thread. 77 | 78 | See also: [Cargo.toml](Cargo.toml). 79 | 80 | Examples: 81 | 82 | ```shell 83 | # simple tcp 84 | cargo build --release --no-default-features 85 | 86 | # enable other options 87 | cargo build --release --features 'jemalloc' 88 | 89 | # fully customized 90 | cargo build --release 91 | --no-default-features 92 | --features 'transport, multi-thread, jemalloc' 93 | ``` 94 | 95 | ### Cross Compile 96 | 97 | Please refer to [https://rust-lang.github.io/rustup/cross-compilation.html](https://rust-lang.github.io/rustup/cross-compilation.html). You may need to install cross-compilers or other SDKs, and specify them when building the project. 98 | 99 | Or have a look at [Cross](https://github.com/cross-rs/cross), it makes things easier. 100 | 101 | ## Usage 102 | 103 | ```shell 104 | Realm 2.4.0 [hook][proxy][balance][brutal][transport][multi-thread] 105 | A high efficiency relay tool 106 | 107 | USAGE: 108 | realm [FLAGS] [OPTIONS] 109 | 110 | FLAGS: 111 | -h, --help show help 112 | -v, --version show version 113 | -d, --daemon run as a unix daemon 114 | -u, --udp force enable udp forward 115 | -t, --ntcp force disable tcp forward 116 | -f, --tfo force enable tcp fast open -- deprecated 117 | -z, --splice force enable tcp zero copy -- deprecated 118 | 119 | OPTIONS: 120 | -c, --config use config file 121 | -l, --listen
listen address 122 | -r, --remote
remote address 123 | -x, --through
send through ip or address 124 | -i, --interface bind to interface 125 | -a, --listen-transport listen transport 126 | -b, --remote-transport remote transport 127 | 128 | SYS OPTIONS: 129 | -n, --nofile set nofile limit 130 | -p, --pipe-page set pipe capacity 131 | -j, --pre-conn-hook set pre-connect hook 132 | 133 | LOG OPTIONS: 134 | --log-level override log level 135 | --log-output override log output 136 | 137 | DNS OPTIONS: 138 | --dns-mode override dns mode 139 | --dns-min-ttl override dns min ttl 140 | --dns-max-ttl override dns max ttl 141 | --dns-cache-size override dns cache size 142 | --dns-protocol override dns protocol 143 | --dns-servers override dns servers 144 | 145 | PROXY OPTIONS: 146 | --send-proxy send proxy protocol header 147 | --send-proxy-version send proxy protocol version 148 | --accept-proxy accept proxy protocol header 149 | --accept-proxy-timeout accept proxy protocol timeout 150 | 151 | TIMEOUT OPTIONS: 152 | --tcp-timeout override tcp timeout 153 | --udp-timeout override udp timeout 154 | 155 | SUBCOMMANDS: 156 | convert convert your legacy configuration into an advanced one 157 | ``` 158 | 159 | Start from command line arguments: 160 | 161 | ```shell 162 | realm -l 0.0.0.0:5000 -r 1.1.1.1:443 163 | ``` 164 | 165 | Start with a config file: 166 | 167 | ```shell 168 | # use toml 169 | realm -c config.toml 170 | 171 | # use json 172 | realm -c config.json 173 | ``` 174 | 175 | Start with environment variables: 176 | 177 | ```shell 178 | REALM_CONF='{"endpoints":[{"local":"127.0.0.1:5000","remote":"1.1.1.1:443"}]}' realm 179 | 180 | # or 181 | export REALM_CONF=`cat config.json | jq -c ` 182 | realm 183 | ``` 184 | 185 | Convert a legacy config file: 186 | 187 | ```shell 188 | realm convert old.json 189 | ``` 190 | 191 | ## Configuration 192 | 193 | TOML Example 194 | 195 | ```toml 196 | [log] 197 | level = "warn" 198 | output = "/var/log/realm.log" 199 | 200 | [network] 201 | no_tcp = false 202 | use_udp = true 203 | 204 | [[endpoints]] 205 | listen = "0.0.0.0:5000" 206 | remote = "1.1.1.1:443" 207 | 208 | [[endpoints]] 209 | listen = "0.0.0.0:10000" 210 | remote = "www.google.com:443" 211 | 212 | ``` 213 | 214 |
215 | JSON Example 216 |

217 | 218 | ```json 219 | { 220 | "log": { 221 | "level": "warn", 222 | "output": "/var/log/realm.log" 223 | }, 224 | "network": { 225 | "no_tcp": false, 226 | "use_udp": true 227 | }, 228 | "endpoints": [ 229 | { 230 | "listen": "0.0.0.0:5000", 231 | "remote": "1.1.1.1:443" 232 | }, 233 | { 234 | "listen": "0.0.0.0:10000", 235 | "remote": "www.google.com:443" 236 | } 237 | ] 238 | } 239 | 240 | ``` 241 | 242 |

243 |
244 | 245 | [See more examples here](./examples). 246 | 247 | ## Overview 248 | 249 | ```shell 250 | ├── log 251 | │ ├── level 252 | │ └── output 253 | ├── dns 254 | │ ├── mode 255 | │ ├── protocol 256 | │ ├── nameservers 257 | │ ├── min_ttl 258 | │ ├── max_ttl 259 | │ └── cache_size 260 | ├── network 261 | │ ├── no_tcp 262 | │ ├── use_udp 263 | │ ├── tcp_timeout 264 | │ ├── udp_timeout 265 | │ ├── send_proxy 266 | │ ├── send_proxy_version 267 | │ ├── accept_proxy 268 | │ └── accept_proxy_timeout 269 | └── endpoints 270 | ├── listen 271 | ├── remote 272 | ├── extra_remotes 273 | ├── balance 274 | ├── through 275 | ├── interface 276 | ├── listen_transport 277 | ├── remote_transport 278 | └── network-> 279 | ``` 280 | 281 | You should provide at least [endpoint.listen](#endpointlisten-string) and [endpoint.remote](#endpointremote-string), the left fields will take their default values. 282 | 283 | Option priority: cmd override > endpoint config > global config. 284 | 285 | ### endpoint 286 | 287 | #### endpoint.listen: string 288 | 289 | Local address, supported formats: 290 | 291 | - ipv4:port 292 | - ipv6:port 293 | 294 | #### endpoint.remote: string 295 | 296 | Remote address, supported formats: 297 | 298 | - ipv4:port 299 | - ipv6:port 300 | - example.com:port 301 | 302 | #### endpoint.extra_remotes: string array 303 | 304 | Extra remote address, same as endpoint.remote above. 305 | 306 | #### endpoint.balance: string 307 | 308 | Require `balance` feature. 309 | 310 | Load balance strategy and weights of remote peers. 311 | 312 | Format: 313 | 314 | ```bash 315 | $strategy: $weight1, $weight2, ... 316 | ``` 317 | 318 | Where `remote` is used as default backend server, and `extra_remotes` are used as backups. 319 | 320 | Available algorithms (provided by [realm_lb](./realm_lb/)): 321 | 322 | - iphash 323 | 324 | - roundrobin 325 | 326 | Example: 327 | 328 | ```toml 329 | [[endpoints]] 330 | remote = "a:443" 331 | extra_remotes = ["b:443", "c:443"] 332 | balance = "roundrobin: 4, 2, 1" 333 | ``` 334 | 335 | The weight of [a, b, c] is [4, 2, 1] in turn. 336 | 337 | #### endpoint.through: string 338 | 339 | TCP: Bind a specific `ip` before opening a connection. 340 | 341 | UDP: Bind a specific `ip` or `address` before sending packet. 342 | 343 | Supported formats: 344 | 345 | - ipv4/ipv6 (tcp/udp) 346 | - ipv4/ipv6:port (udp) 347 | 348 | #### endpoint.interface: string 349 | 350 | Bind to a specific interface. 351 | 352 | #### endpoint.listen_transport: string 353 | 354 | Require `transport` feature. 355 | 356 | See [Kaminari Options](https://github.com/zephyrchien/kaminari#options). 357 | 358 | #### endpoint.remote_transport: string 359 | 360 | Require `transport` feature. 361 | 362 | See [Kaminari Options](https://github.com/zephyrchien/kaminari#options). 363 | 364 | #### endpoint.network 365 | 366 | The same as [network](#network), override global options. 367 | 368 | ### log 369 | 370 | #### log.level: string 371 | 372 | values: 373 | 374 | - off 375 | - error 376 | - warn 377 | - info 378 | - debug 379 | - trace 380 | 381 | default: off 382 | 383 | #### log.output: string 384 | 385 | values: 386 | 387 | - stdout 388 | - stderr 389 | - path (e.g. `/var/log/realm.log`) 390 | 391 | default: stdout 392 | 393 | ### dns 394 | 395 | Require `trust-dns` feature. 396 | 397 | #### dns.mode: string 398 | 399 | Dns resolve strategy. 400 | 401 | values: 402 | 403 | - ipv4_only 404 | - ipv6_only 405 | - ipv4_then_ipv6 406 | - ipv6_then_ipv4 407 | - ipv4_and_ipv6 408 | 409 | default: ipv4_and_ipv6 410 | 411 | #### dns.protocol: string 412 | 413 | Dns transport protocol. 414 | 415 | values: 416 | 417 | - tcp 418 | - udp 419 | - tcp_and_udp 420 | 421 | default: tcp_and_udp 422 | 423 | #### dns.nameservers: string array 424 | 425 | Custom upstream servers. 426 | 427 | format: ["server1", "server2" ...] 428 | 429 | default: 430 | 431 | If on **unix/windows**, read from the default location.(e.g. `/etc/resolv.conf`). 432 | 433 | Otherwise, use google's public dns(`8.8.8.8:53`, `8.8.4.4:53` and `2001:4860:4860::8888:53`, `2001:4860:4860::8844:53`). 434 | 435 | #### dns.min_ttl: unsigned int 436 | 437 | The minimum lifetime of a positive dns cache. 438 | 439 | default: 0 440 | 441 | #### dns.max_ttl: unsigned int 442 | 443 | The maximum lifetime of a positive dns cache. 444 | 445 | default: 86400 (1 day) 446 | 447 | #### dns.cache_size: unsigned int 448 | 449 | The maximum count of dns cache. 450 | 451 | default: 32 452 | 453 | ### network 454 | 455 | #### network.no_tcp: bool 456 | 457 | Do not start a tcp relay. 458 | 459 | default: false 460 | 461 | #### network.use_udp: bool 462 | 463 | ~~Require `udp` feature~~ 464 | 465 | Start listening on a udp endpoint and forward packets to the remote peer. 466 | 467 | It will dynamically allocate local endpoints and establish udp associations. Once timeout, the endpoints will be deallocated and the association will be terminated. See also: [network.udp_timeout](#networkudp_timeout-unsigned-int). 468 | 469 | Due to the receiver side not limiting access to the association, the relay works like a full-cone NAT. 470 | 471 | default: false 472 | 473 | #### ~~network.zero_copy: bool~~ deprecated 474 | 475 | ~~Require `zero-copy` feature.~~ 476 | 477 | ~~Use `splice` instead of `send/recv` while handing tcp connection. This will save a lot of memory copies and context switches.~~ 478 | 479 | ~~default: false~~ 480 | 481 | #### ~~network.fast_open: bool~~ deprecated 482 | 483 | ~~Require `fast-open` feature.~~ 484 | 485 | ~~It is not recommended to enable this option, see [The Sad Story of TCP Fast Open](https://squeeze.isobar.com/2019/04/11/the-sad-story-of-tcp-fast-open/).~~ 486 | 487 | ~~default: false~~ 488 | 489 | #### network.tcp_timeout: unsigned int 490 | 491 | This is **connect** timeout. An attempt to connect to a remote peer fails after waiting for a period of time. 492 | 493 | To disable timeout, you need to explicitly set timeout value to 0. 494 | 495 | default: 5 496 | 497 | #### network.udp_timeout: unsigned int 498 | 499 | Terminate udp association after `timeout`. 500 | 501 | The timeout value must be properly configured in case of memory leak. Do not use a large `timeout`! 502 | 503 | default: 30 504 | 505 | #### network.send_proxy: bool 506 | 507 | Require `proxy` feature. 508 | 509 | Send haproxy PROXY header once the connection established. Both `v1` and `v2` are supported, see [send_proxy_version](#networksend_proxy_version-unsigned-int). 510 | 511 | You should make sure the remote peer also speaks proxy-protocol. 512 | 513 | default: false 514 | 515 | #### network.send_proxy_version: unsigned int 516 | 517 | Require `proxy` feature. 518 | 519 | This option has no effect unless [send_proxy](#networksend_proxy-bool) is enabled. 520 | 521 | value: 522 | 523 | - 1 524 | - 2 525 | 526 | default: 2 527 | 528 | #### network.accept_proxy: bool 529 | 530 | Require `proxy` feature. 531 | 532 | Wait for a PROXY header once the connection established. 533 | 534 | If the remote sender does not send a `v1` or `v2` header before other contents, the connection will be closed. 535 | 536 | default: false 537 | 538 | #### network.accept_timeout: unsigned int 539 | 540 | Require `proxy` feature. 541 | 542 | Wait for a PROXY header within a period of time, otherwise close the connection. 543 | 544 | default: 5. 545 | --------------------------------------------------------------------------------