Add upstream scheme support

Need to implement TCP and UDP upstream support.
This commit is contained in:
KernelErr 2021-10-31 19:21:32 +08:00
parent 5944beb6a2
commit 47be2568ba
9 changed files with 254 additions and 89 deletions

78
Cargo.lock generated
View File

@ -96,9 +96,19 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]] [[package]]
name = "fourth" name = "fourth"
version = "0.1.3" version = "0.1.4"
dependencies = [ dependencies = [
"byte_string", "byte_string",
"bytes 1.1.0", "bytes 1.1.0",
@ -110,6 +120,7 @@ dependencies = [
"serde_yaml", "serde_yaml",
"tls-parser", "tls-parser",
"tokio", "tokio",
"url",
] ]
[[package]] [[package]]
@ -241,6 +252,17 @@ dependencies = [
"quick-error", "quick-error",
] ]
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.7.0" version = "1.7.0"
@ -318,6 +340,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.1" version = "2.4.1"
@ -453,6 +481,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.10.0" version = "0.10.0"
@ -721,6 +755,21 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "tinyvec"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tls-parser" name = "tls-parser"
version = "0.11.0" version = "0.11.0"
@ -766,12 +815,39 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "unicode-bidi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.3"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "fourth" name = "fourth"
version = "0.1.3" version = "0.1.4"
edition = "2021" edition = "2021"
authors = ["LI Rui <lr_cn@outlook.com>"] authors = ["LI Rui <lr_cn@outlook.com>"]
license = "Apache-2.0" license = "Apache-2.0"
@ -22,6 +22,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8" serde_yaml = "0.8"
futures = "0.3" futures = "0.3"
tls-parser = "0.11" tls-parser = "0.11"
url = "2.2.2"
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }

View File

@ -35,39 +35,31 @@ $ cargo install fourth
## 配置 ## 配置
Fourth使用yaml格式的配置文件默认情况下会读取`/etc/fourth/config.yaml`,如下是一个示例配置。 Fourth使用yaml格式的配置文件默认情况下会读取`/etc/fourth/config.yaml`,如下是一个最小有效配置:
```yaml ```yaml
version: 1 version: 1
log: info log: info
servers: servers:
example_server:
listen:
- "0.0.0.0:443"
- "[::]:443"
tls: true # Enable TLS features like SNI filtering
sni:
proxy.example.com: proxy
www.example.com: nginx
default: ban
proxy_server: proxy_server:
listen: listen:
- "127.0.0.1:8081" - "127.0.0.1:8081"
default: remote default: remote
kcp_server:
protocol: kcp # default TCP
listen:
- "127.0.0.1:8082"
default: echo
upstream: upstream:
nginx: "127.0.0.1:8080" remote: "tcp://www.remote.example.com:8082" # proxy to remote address
proxy: "127.0.0.1:1024"
remote: "www.remote.example.com:8082" # proxy to remote address
``` ```
内置两个的upstreamban立即中断连接、echo返回读到的数据 内置两个的upstreamban立即中断连接、echo返回读到的数据。更详细的配置可以参考[示例配置](./example-config.yaml)。
## 性能测试
在4C2G的服务器上测试
使用Fourth代理到Nginx直连QPS 120000: ~70000req/s (测试命令:`wrk -t200 -c1000 -d120s --latency http://proxy-server:8081 `
使用Fourth代理到本地iperf38Gbps
## io_uring? ## io_uring?

View File

@ -22,6 +22,6 @@ servers:
default: echo default: echo
upstream: upstream:
nginx: "127.0.0.1:8080" nginx: "tcp://127.0.0.1:8080"
proxy: "127.0.0.1:1024" proxy: "tcp://127.0.0.1:1024"
remote: "www.remote.example.com:8082" # proxy to remote address remote: "tcp://www.remote.example.com:8082" # proxy to remote address

View File

@ -3,10 +3,19 @@ use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io::{Error as IOError, Read}; use std::io::{Error as IOError, Read};
use url::Url;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub base: BaseConfig, pub base: ParsedConfig,
}
#[derive(Debug, Default, Deserialize, Clone)]
pub struct ParsedConfig {
pub version: i32,
pub log: Option<String>,
pub servers: HashMap<String, ServerConfig>,
pub upstream: HashMap<String, Upstream>,
} }
#[derive(Debug, Default, Deserialize, Clone)] #[derive(Debug, Default, Deserialize, Clone)]
@ -26,6 +35,20 @@ pub struct ServerConfig {
pub default: Option<String>, pub default: Option<String>,
} }
#[derive(Debug, Clone, Deserialize)]
pub enum Upstream {
Ban,
Echo,
Custom(CustomUpstream),
}
#[derive(Debug, Clone, Deserialize)]
pub struct CustomUpstream {
pub name: String,
pub addr: String,
pub protocol: String,
}
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum ConfigError {
IO(IOError), IO(IOError),
@ -41,27 +64,89 @@ impl Config {
} }
} }
fn load_config(path: &str) -> Result<BaseConfig, ConfigError> { fn load_config(path: &str) -> Result<ParsedConfig, ConfigError> {
let mut contents = String::new(); let mut contents = String::new();
let mut file = (File::open(path))?; let mut file = (File::open(path))?;
(file.read_to_string(&mut contents))?; (file.read_to_string(&mut contents))?;
let parsed: BaseConfig = serde_yaml::from_str(&contents)?; let base: BaseConfig = serde_yaml::from_str(&contents)?;
if parsed.version != 1 { if base.version != 1 {
return Err(ConfigError::Custom( return Err(ConfigError::Custom(
"Unsupported config version".to_string(), "Unsupported config version".to_string(),
)); ));
} }
let log_level = parsed.log.clone().unwrap_or_else(|| "info".to_string()); let log_level = base.log.clone().unwrap_or_else(|| "info".to_string());
if !log_level.eq("disable") { if !log_level.eq("disable") {
std::env::set_var("FOURTH_LOG", log_level.clone()); std::env::set_var("FOURTH_LOG", log_level.clone());
pretty_env_logger::init_custom_env("FOURTH_LOG"); pretty_env_logger::init_custom_env("FOURTH_LOG");
debug!("Set log level to {}", log_level); debug!("Set log level to {}", log_level);
} }
debug!("Config version {}", parsed.version); debug!("Config version {}", base.version);
let mut parsed_upstream: HashMap<String, Upstream> = HashMap::new();
for (name, upstream) in base.upstream.iter() {
let upstream_url = match Url::parse(upstream) {
Ok(url) => url,
Err(_) => {
return Err(ConfigError::Custom(format!(
"Invalid upstream url \"{}\"",
upstream
)))
}
};
let upstream_host = match upstream_url.host_str() {
Some(host) => host,
None => {
return Err(ConfigError::Custom(format!(
"Invalid upstream url \"{}\"",
upstream
)))
}
};
let upsteam_port = match upstream_url.port_or_known_default() {
Some(port) => port,
None => {
return Err(ConfigError::Custom(format!(
"Invalid upstream url \"{}\"",
upstream
)))
}
};
parsed_upstream.insert(
name.to_string(),
Upstream::Custom(CustomUpstream {
name: name.to_string(),
addr: format!("{}:{}", upstream_host, upsteam_port),
protocol: upstream_url.scheme().to_string(),
}),
);
}
parsed_upstream.insert(
"ban".to_string(),
Upstream::Ban,
);
parsed_upstream.insert(
"echo".to_string(),
Upstream::Echo,
);
let parsed = ParsedConfig {
version: base.version,
log: base.log,
servers: base.servers,
upstream: parsed_upstream,
};
// ToDo: validate config
Ok(parsed) Ok(parsed)
} }
@ -88,6 +173,6 @@ mod tests {
assert_eq!(config.base.version, 1); assert_eq!(config.base.version, 1);
assert_eq!(config.base.log.unwrap(), "disable"); assert_eq!(config.base.log.unwrap(), "disable");
assert_eq!(config.base.servers.len(), 5); assert_eq!(config.base.servers.len(), 5);
assert_eq!(config.base.upstream.len(), 3); assert_eq!(config.base.upstream.len(), 3 + 2); // Add ban and echo upstreams
} }
} }

View File

@ -5,13 +5,13 @@ use std::sync::Arc;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
mod protocol; mod protocol;
use crate::config::BaseConfig; use crate::config::{ParsedConfig, Upstream};
use protocol::{kcp, tcp}; use protocol::{kcp, tcp};
#[derive(Debug)] #[derive(Debug)]
pub struct Server { pub struct Server {
pub proxies: Vec<Arc<Proxy>>, pub proxies: Vec<Arc<Proxy>>,
pub config: BaseConfig, pub config: ParsedConfig,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -22,11 +22,11 @@ pub struct Proxy {
pub tls: bool, pub tls: bool,
pub sni: Option<HashMap<String, String>>, pub sni: Option<HashMap<String, String>>,
pub default: String, pub default: String,
pub upstream: HashMap<String, String>, pub upstream: HashMap<String, Upstream>,
} }
impl Server { impl Server {
pub fn new(config: BaseConfig) -> Self { pub fn new(config: ParsedConfig) -> Self {
let mut new_server = Server { let mut new_server = Server {
proxies: Vec::new(), proxies: Vec::new(),
config: config.clone(), config: config.clone(),
@ -53,6 +53,7 @@ impl Server {
continue; continue;
} }
}; };
let proxy = Proxy { let proxy = Proxy {
name: name.clone(), name: name.clone(),
listen: listen_addr, listen: listen_addr,
@ -103,7 +104,7 @@ impl Server {
} }
#[cfg(test)] #[cfg(test)]
mod test { mod tests {
use crate::plugins::kcp::{KcpConfig, KcpStream}; use crate::plugins::kcp::{KcpConfig, KcpStream};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::thread::{self, sleep}; use std::thread::{self, sleep};

View File

@ -1,3 +1,4 @@
use crate::config::Upstream;
use crate::plugins::kcp::{KcpConfig, KcpListener, KcpStream}; use crate::plugins::kcp::{KcpConfig, KcpListener, KcpStream};
use crate::servers::Proxy; use crate::servers::Proxy;
use futures::future::try_join; use futures::future::try_join;
@ -52,25 +53,27 @@ async fn accept(
"No upstream named {:?} on server {:?}", "No upstream named {:?} on server {:?}",
proxy.default, proxy.name proxy.default, proxy.name
); );
return process(inbound, &proxy.default).await; return process(inbound, proxy.upstream.get(&proxy.default).unwrap()).await; // ToDo: Remove unwrap and check default option
} }
}; };
return process(inbound, upstream).await; return process(inbound, upstream).await;
} }
async fn process(mut inbound: KcpStream, upstream: &str) -> Result<(), Box<dyn std::error::Error>> { async fn process(mut inbound: KcpStream, upstream: &Upstream) -> Result<(), Box<dyn std::error::Error>> {
if upstream == "ban" { match upstream {
Upstream::Ban => {
let _ = inbound.shutdown(); let _ = inbound.shutdown();
return Ok(()); Ok(())
} else if upstream == "echo" { }
Upstream::Echo => {
let (mut ri, mut wi) = io::split(inbound); let (mut ri, mut wi) = io::split(inbound);
let inbound_to_inbound = copy(&mut ri, &mut wi); let inbound_to_inbound = copy(&mut ri, &mut wi);
let bytes_tx = inbound_to_inbound.await; let bytes_tx = inbound_to_inbound.await;
debug!("Bytes read: {:?}", bytes_tx); debug!("Bytes read: {:?}", bytes_tx);
return Ok(()); Ok(())
} }
Upstream::Custom(custom) => {
let outbound = TcpStream::connect(upstream).await?; let outbound = TcpStream::connect(custom.addr.clone()).await?;
let (mut ri, mut wi) = io::split(inbound); let (mut ri, mut wi) = io::split(inbound);
let (mut ro, mut wo) = io::split(outbound); let (mut ro, mut wo) = io::split(outbound);
@ -83,6 +86,8 @@ async fn process(mut inbound: KcpStream, upstream: &str) -> Result<(), Box<dyn s
debug!("Bytes read: {:?} write: {:?}", bytes_tx, bytes_rx); debug!("Bytes read: {:?} write: {:?}", bytes_tx, bytes_rx);
Ok(()) Ok(())
}
}
} }
async fn copy<'a, R, W>(reader: &'a mut R, writer: &'a mut W) -> io::Result<u64> async fn copy<'a, R, W>(reader: &'a mut R, writer: &'a mut W) -> io::Result<u64>

View File

@ -1,3 +1,4 @@
use crate::config::Upstream;
use crate::servers::protocol::tls::get_sni; use crate::servers::protocol::tls::get_sni;
use crate::servers::Proxy; use crate::servers::Proxy;
use futures::future::try_join; use futures::future::try_join;
@ -71,25 +72,27 @@ async fn accept(inbound: TcpStream, proxy: Arc<Proxy>) -> Result<(), Box<dyn std
"No upstream named {:?} on server {:?}", "No upstream named {:?} on server {:?}",
proxy.default, proxy.name proxy.default, proxy.name
); );
return process(inbound, &proxy.default).await; return process(inbound, proxy.upstream.get(&proxy.default).unwrap()).await; // ToDo: Remove unwrap and check default option
} }
}; };
return process(inbound, upstream).await; return process(inbound, upstream).await;
} }
async fn process(mut inbound: TcpStream, upstream: &str) -> Result<(), Box<dyn std::error::Error>> { async fn process(mut inbound: TcpStream, upstream: &Upstream) -> Result<(), Box<dyn std::error::Error>> {
if upstream == "ban" { match upstream {
Upstream::Ban => {
let _ = inbound.shutdown(); let _ = inbound.shutdown();
return Ok(()); Ok(())
} else if upstream == "echo" { }
Upstream::Echo => {
let (mut ri, mut wi) = io::split(inbound); let (mut ri, mut wi) = io::split(inbound);
let inbound_to_inbound = copy(&mut ri, &mut wi); let inbound_to_inbound = copy(&mut ri, &mut wi);
let bytes_tx = inbound_to_inbound.await; let bytes_tx = inbound_to_inbound.await;
debug!("Bytes read: {:?}", bytes_tx); debug!("Bytes read: {:?}", bytes_tx);
return Ok(()); Ok(())
} }
Upstream::Custom(custom) => {
let outbound = TcpStream::connect(upstream).await?; let outbound = TcpStream::connect(custom.addr.clone()).await?;
let (mut ri, mut wi) = io::split(inbound); let (mut ri, mut wi) = io::split(inbound);
let (mut ro, mut wo) = io::split(outbound); let (mut ro, mut wo) = io::split(outbound);
@ -102,6 +105,8 @@ async fn process(mut inbound: TcpStream, upstream: &str) -> Result<(), Box<dyn s
debug!("Bytes read: {:?} write: {:?}", bytes_tx, bytes_rx); debug!("Bytes read: {:?} write: {:?}", bytes_tx, bytes_rx);
Ok(()) Ok(())
}
}
} }
async fn copy<'a, R, W>(reader: &'a mut R, writer: &'a mut W) -> io::Result<u64> async fn copy<'a, R, W>(reader: &'a mut R, writer: &'a mut W) -> io::Result<u64>

View File

@ -31,6 +31,6 @@ servers:
default: echo default: echo
upstream: upstream:
web: "127.0.0.1:8080" web: "tcp://127.0.0.1:8080"
proxy: "www.example.com:1024" proxy: "tcp://www.example.com:1024"
tester: "127.0.0.1:54599" tester: "tcp://127.0.0.1:54599"