use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, bail, Context, Result};
use nfrp_common::frp::stream::{BoxStream, PrefixedStream};
use nfrp_common::frp::tls::{client_config, server_config_from_pem};
use nfrp_common::http_relay::{
body_length, connection_tokens, has_no_body, is_chunked, relay_body, relay_request_body,
wants_keep_alive, wants_upgrade, HeadParts, HttpIo, HOP_BY_HOP,
};
use nfrp_common::util;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_rustls::{TlsAcceptor, TlsConnector};
use tracing::{debug, warn};
use crate::plugin::Ctx;
#[cfg(not(test))]
const UPSTREAM_DIAL_TIMEOUT: Duration = Duration::from_secs(10);
#[cfg(test)]
const UPSTREAM_DIAL_TIMEOUT: Duration = Duration::from_millis(800);
pub struct Bridge {
in_tls: Option<Arc<rustls::ServerConfig>>,
out_tls: bool,
raw: bool,
local_addr: String,
host_rewrite: String,
extra_headers: BTreeMap<String, String>,
set_x_forwarded: bool,
}
pub struct BridgeConfig {
pub local_addr: String,
pub host_rewrite: String,
pub extra_headers: BTreeMap<String, String>,
pub crt_path: String,
pub key_path: String,
}
impl Bridge {
pub fn http2http(c: BridgeConfig) -> Result<Self> {
Ok(Self::plain(c, false, false))
}
pub fn http2https(c: BridgeConfig) -> Result<Self> {
Ok(Self::plain(c, false, true))
}
pub fn https2http(c: BridgeConfig) -> Result<Self> {
let tls = load_inbound_tls("https2http", &c)?;
Ok(Self {
in_tls: Some(tls),
..Self::plain(c, true, false)
})
}
pub fn https2https(c: BridgeConfig) -> Result<Self> {
let tls = load_inbound_tls("https2https", &c)?;
Ok(Self {
in_tls: Some(tls),
..Self::plain(c, true, true)
})
}
pub fn tls2raw(c: BridgeConfig) -> Result<Self> {
let tls = load_inbound_tls("tls2raw", &c)?;
Ok(Self {
in_tls: Some(tls),
raw: true,
set_x_forwarded: false,
..Self::plain(c, true, false)
})
}
fn plain(c: BridgeConfig, x_forwarded: bool, out_tls: bool) -> Self {
Self {
in_tls: None,
out_tls,
raw: false,
local_addr: c.local_addr,
host_rewrite: c.host_rewrite,
extra_headers: c.extra_headers,
set_x_forwarded: x_forwarded,
}
}
pub async fn serve(self, stream: BoxStream, leftover: Vec<u8>, ctx: &Ctx<'_>) -> Result<()> {
if self.local_addr.trim().is_empty() {
bail!("插件必须配置 localAddr(要转发的上游地址)");
}
let stream: BoxStream = if leftover.is_empty() {
stream
} else {
Box::pin(PrefixedStream::new(leftover, stream))
};
match self.in_tls.clone() {
None => self.run(stream, ctx).await,
Some(cfg) => {
let accepted = TlsAcceptor::from(cfg)
.accept(stream)
.await
.with_context(|| {
format!(
"代理 [{}] 的入站 TLS 握手失败(客户端是不是没按 TLS 连?)",
ctx.proxy_name
)
})?;
self.run(accepted, ctx).await
}
}
}
async fn run<S>(self, mut stream: S, ctx: &Ctx<'_>) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let r = if self.raw {
self.serve_raw(&mut stream, ctx).await
} else {
self.serve_http(&mut stream, ctx).await
};
let _ = stream.shutdown().await;
r
}
async fn wait_upstream<T, S>(
io: &mut HttpIo<&mut S>,
fut: impl std::future::Future<Output = Result<T>>,
) -> Result<Option<T>>
where
S: AsyncRead + Unpin,
{
let mut fut = std::pin::pin!(fut);
loop {
tokio::select! {
r = &mut fut => return Ok(Some(r?)),
closed = io.watch_closed() => {
if closed? {
return Ok(None);
}
}
}
}
}
async fn serve_raw<S>(&self, stream: &mut S, ctx: &Ctx<'_>) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut up = self.dial_upstream().await?;
debug!(proxy = %ctx.proxy_name, upstream = %self.local_addr, "tls2raw:TLS 已终止,开始裸对流拷");
util::relay_between(stream, &mut up).await?;
Ok(())
}
async fn serve_http<S>(&self, stream: &mut S, ctx: &Ctx<'_>) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut io: HttpIo<&mut S> = HttpIo::new(stream);
loop {
let Some(lines) = io.read_head().await? else {
return Ok(());
};
let mut req = HeadParts::parse(&lines)?;
let client_keep = wants_keep_alive(&req);
let upgrading = wants_upgrade(&req);
if let Err(e) = nfrp_common::http_relay::validate_request_framing(&req) {
debug!(proxy = %ctx.proxy_name, "请求 framing 非法,按走私风险直接拒绝:{e}");
let body = format!("{e}\n");
io.stream
.write_all(
format!(
"HTTP/1.1 400 Bad Request\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\r\n{body}",
body.len()
)
.as_bytes(),
)
.await?;
io.stream.flush().await?;
return Ok(());
}
if req
.get("expect")
.is_some_and(|v| v.eq_ignore_ascii_case("100-continue"))
{
io.stream
.write_all(b"HTTP/1.1 100 Continue\r\n\r\n")
.await?;
io.stream.flush().await?;
req.remove("expect");
}
let head = self.rewrite_request(&mut req, ctx, upgrading);
let Some(up) = Self::wait_upstream(&mut io, self.dial_upstream()).await? else {
debug!(proxy = %ctx.proxy_name, "访问方已撤离,放弃连接上游");
return Ok(());
};
let mut up = HttpIo::new(up);
up.stream.write_all(&head).await?;
relay_request_body(&mut io, &mut up.stream, &req).await?;
up.stream.flush().await?;
let Some(resp_head) = Self::wait_upstream(&mut io, up.read_head()).await? else {
debug!(
proxy = %ctx.proxy_name,
"访问方已撤离,释放上游连接(不再替它等响应)"
);
return Ok(());
};
let Some(resp_lines) = resp_head else {
bail!("上游 {} 在返回任何响应之前就关闭了连接", self.local_addr);
};
let mut resp = HeadParts::parse(&resp_lines)?;
let switched = upgrading && resp.start_token(1) == Some("101");
let framed =
has_no_body(&resp) || is_chunked(&resp) || body_length(&resp).is_some() || switched;
let keep = client_keep && wants_keep_alive(&resp) && framed;
nfrp_common::http_relay::strip_hop_by_hop(&mut resp);
if !keep {
resp.set("Connection", "close");
}
io.stream.write_all(&resp.to_bytes()).await?;
io.stream.flush().await?;
if switched {
debug!(proxy = %ctx.proxy_name, "协议升级成功,转入裸流对拷");
util::relay_between(&mut io.stream, &mut up.stream).await?;
return Ok(());
}
relay_body(&mut up, &mut io.stream, &resp).await?;
io.stream.flush().await?;
if !keep {
return Ok(());
}
}
}
async fn dial_upstream(&self) -> Result<BoxStream> {
let tcp = tokio::time::timeout(
UPSTREAM_DIAL_TIMEOUT,
TcpStream::connect(self.local_addr.trim()),
)
.await
.map_err(|_| {
anyhow!(
"连接上游 {} 超时({:?})—— 对端不可达或未响应",
self.local_addr,
UPSTREAM_DIAL_TIMEOUT
)
})?
.with_context(|| format!("连接上游 {} 失败(localAddr 配错了?)", self.local_addr))?;
tcp.set_nodelay(true).ok();
if !self.out_tls {
return Ok(Box::pin(tcp));
}
let host = host_part(&self.local_addr);
let name = rustls::pki_types::ServerName::try_from(host.to_string())
.map_err(|e| anyhow!("上游地址 {host} 不能用作 TLS 名字:{e}"))?;
let conn = tokio::time::timeout(
UPSTREAM_DIAL_TIMEOUT,
TlsConnector::from(client_config()?).connect(name, tcp),
)
.await
.map_err(|_| {
anyhow!(
"与上游 {} 做 TLS 握手超时({:?})—— 对端 accept 后没有完成握手",
self.local_addr,
UPSTREAM_DIAL_TIMEOUT
)
})?
.with_context(|| format!("与上游 {} 做 TLS 握手失败", self.local_addr))?;
Ok(Box::pin(conn))
}
fn rewrite_request(&self, req: &mut HeadParts, ctx: &Ctx<'_>, upgrading: bool) -> Vec<u8> {
if let Some(target) = req.start_token(1).map(str::to_string) {
if let Some(path) = origin_form(&target) {
let method = req.start_token(0).unwrap_or("GET").to_string();
let ver = req.start_token(2).unwrap_or("HTTP/1.1").to_string();
req.start_line = format!("{method} {path} {ver}");
}
}
let client_ip = ctx.src.map(|s| s.ip().to_string());
if self.set_x_forwarded {
let prior: Vec<String> = req
.get("x-forwarded-for")
.map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_default();
match client_ip {
Some(ip) => {
let mut chain = prior;
chain.push(ip);
req.set("X-Forwarded-For", &chain.join(", "));
}
None => req.remove("x-forwarded-for"),
}
let inbound_host = req.get("host").unwrap_or("").to_string();
req.set("X-Forwarded-Host", &inbound_host);
req.set("X-Forwarded-Proto", "https");
} else if self.in_tls.is_some() {
req.set("X-Forwarded-Proto", "https");
} else if self.out_tls {
} else {
req.remove("forwarded");
req.remove("x-forwarded-for");
req.remove("x-forwarded-host");
req.remove("x-forwarded-proto");
}
let extra = connection_tokens(req);
let drop_it = |k: &str| {
HOP_BY_HOP.iter().any(|h| h.eq_ignore_ascii_case(k))
|| extra.iter().any(|t| t.eq_ignore_ascii_case(k))
};
if !upgrading {
req.headers.retain(|(k, _)| !drop_it(k));
req.set("Connection", "close");
}
if !self.host_rewrite.trim().is_empty() {
req.set("Host", self.host_rewrite.trim());
}
for (k, v) in &self.extra_headers {
req.set(k, v);
}
let _ = ctx;
req.to_bytes()
}
}
pub fn build(kind: &str, p: &nfrp_common::config::ProxyConfig) -> Result<Bridge> {
let c = BridgeConfig {
local_addr: p.plugin_local_addr.clone(),
host_rewrite: p.plugin_host_header_rewrite.clone(),
extra_headers: p.plugin_request_headers.clone(),
crt_path: p.plugin_crt_path.clone(),
key_path: p.plugin_key_path.clone(),
};
match kind {
"http2http" => Bridge::http2http(c),
"http2https" => Bridge::http2https(c),
"https2http" => Bridge::https2http(c),
"https2https" => Bridge::https2https(c),
"tls2raw" => Bridge::tls2raw(c),
other => bail!("不是 HTTP 桥接插件:{other}"),
}
}
fn load_inbound_tls(kind: &str, c: &BridgeConfig) -> Result<Arc<rustls::ServerConfig>> {
if c.crt_path.trim().is_empty() || c.key_path.trim().is_empty() {
bail!(
"插件 {kind} 要在 frpc 这一侧终止 TLS,必须同时配置 crtPath 和 keyPath\
(证书与私钥是本地服务用的,不是 frps 的)"
);
}
server_config_from_pem(c.crt_path.trim(), c.key_path.trim())
}
fn host_part(addr: &str) -> &str {
let a = addr.trim();
if let Some(rest) = a.strip_prefix('[') {
return rest.split(']').next().unwrap_or(rest);
}
a.rsplit_once(':').map(|(h, _)| h).unwrap_or(a)
}
fn origin_form(target: &str) -> Option<String> {
let rest = target
.strip_prefix("http://")
.or_else(|| target.strip_prefix("https://"))?;
match rest.find('/') {
Some(i) => Some(rest[i..].to_string()),
None => Some("/".to_string()),
}
}
pub fn log_failure(proxy_name: &str, e: &anyhow::Error) {
warn!(proxy = %proxy_name, "插件处理工作连接失败:{e:#}");
}
#[cfg(test)]
mod tests {
use super::*;
fn parts(v: &[&str]) -> HeadParts {
HeadParts::parse(&v.iter().map(|s| s.to_string()).collect::<Vec<_>>()).unwrap()
}
fn bridge(kind: &str) -> Bridge {
let c = BridgeConfig {
local_addr: "127.0.0.1:8080".into(),
host_rewrite: String::new(),
extra_headers: BTreeMap::new(),
crt_path: String::new(),
key_path: String::new(),
};
match kind {
"http2http" => Bridge::http2http(c).unwrap(),
"http2https" => Bridge::http2https(c).unwrap(),
_ => unreachable!(),
}
}
fn ctx() -> Ctx<'static> {
Ctx {
proxy_name: "p",
src: Some("203.0.113.7:1234".parse().unwrap()),
}
}
#[test]
fn 上游主机名解析() {
assert_eq!(host_part("127.0.0.1:8080"), "127.0.0.1");
assert_eq!(host_part("example.com:443"), "example.com");
assert_eq!(host_part("example.com"), "example.com");
assert_eq!(host_part("[::1]:8080"), "::1");
assert_eq!(host_part(" a.b:1 "), "a.b");
}
#[test]
fn 绝对形式还原成原始形式() {
assert_eq!(origin_form("http://a.b/c?d=1"), Some("/c?d=1".to_string()));
assert_eq!(origin_form("https://a.b"), Some("/".to_string()));
assert_eq!(origin_form("/c?d=1"), None);
assert_eq!(origin_form("*"), None);
}
#[test]
fn 客户端插件路径必须做请求_framing_校验() {
use nfrp_common::http_relay::validate_request_framing as check;
for ok in [
vec!["POST / HTTP/1.1", "Host: x", "Content-Length: 5"],
vec!["POST / HTTP/1.1", "Host: x", "Transfer-Encoding: chunked"],
vec![
"POST / HTTP/1.1",
"Host: x",
"Transfer-Encoding: identity, chunked",
],
vec!["GET / HTTP/1.1", "Host: x"],
] {
let p = parts(&ok);
assert!(
check(&p).is_ok(),
"合法请求被误拒:{ok:?} -> {:?}",
check(&p)
);
}
let p = parts(&[
"POST / HTTP/1.1",
"Host: x",
"Content-Length: 5",
"Transfer-Encoding: chunked",
]);
assert!(check(&p).is_err(), "TE 与 CL 并存必须拒绝");
let p = parts(&[
"POST / HTTP/1.1",
"Host: x",
"Content-Length: 5",
"Content-Length: 6",
]);
assert!(check(&p).is_err(), "重复的 Content-Length 必须拒绝");
for bad_te in ["chunked, identity", "xchunked", "chunked, gzip"] {
let p = parts(&[
"POST / HTTP/1.1",
"Host: x",
&format!("Transfer-Encoding: {bad_te}"),
]);
assert!(check(&p).is_err(), "TE={bad_te} 必须拒绝");
}
}
#[test]
fn 客户端响应侧必须剥掉全部逐跳头() {
use nfrp_common::http_relay::strip_hop_by_hop;
let mut resp = parts(&[
"HTTP/1.1 200 OK",
"Content-Type: text/html",
"Connection: keep-alive, x-secret",
"X-Secret: internal-token-abc",
"Upgrade: h2c",
"Keep-Alive: timeout=5",
"Proxy-Authenticate: Basic",
"Trailer: X-Foo",
"Te: trailers",
]);
strip_hop_by_hop(&mut resp);
assert_eq!(resp.get("content-type"), Some("text/html"));
for gone in [
"connection",
"x-secret", "upgrade",
"keep-alive",
"proxy-authenticate",
"trailer",
"te",
] {
assert!(
resp.get(gone).is_none(),
"{gone} 必须被剥掉,实际仍在:{:?}",
resp.get(gone)
);
}
let raw = String::from_utf8(resp.to_bytes())
.unwrap()
.to_ascii_lowercase();
assert!(
!raw.contains("internal-token-abc"),
"内网令牌绝不能出现在转发给客户的响应里:{raw}"
);
}
#[test]
fn http2http_要删掉客户端的_x_forwarded() {
let b = bridge("http2http");
let mut req = parts(&[
"GET / HTTP/1.1",
"Host: vhost.example.com",
"X-Forwarded-For: 1.2.3.4",
"Connection: keep-alive",
]);
let raw = String::from_utf8(b.rewrite_request(&mut req, &ctx(), false)).unwrap();
assert!(
!raw.to_ascii_lowercase().contains("x-forwarded-for"),
"http2http 不该透传伪造的 XFF:{raw}"
);
assert!(
raw.contains("Host: vhost.example.com"),
"Host 默认透传:{raw}"
);
assert!(raw.contains("Connection: close"), "{raw}");
}
#[test]
fn http2http_要删掉官方清单里的全部四个头() {
let b = bridge("http2http");
let mut req = parts(&[
"GET / HTTP/1.1",
"Host: h",
"Forwarded: for=6.6.6.6",
"X-Forwarded-For: 6.6.6.6",
"X-Forwarded-Host: evil.example.com",
"X-Forwarded-Proto: https",
"X-Real-IP: 6.6.6.6",
"CF-Connecting-IP: 6.6.6.6",
]);
let raw = String::from_utf8(b.rewrite_request(&mut req, &ctx(), false)).unwrap();
let low = raw.to_ascii_lowercase();
for h in [
"forwarded",
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
] {
assert!(
!low.contains(&format!("{h}:")),
"★ `{h}` 必须被删除(Go Rewrite 在调用前就移除它)—— \
原先漏了 `Forwarded`,与官方语义不一致(E-3)。实际报文:{raw}"
);
}
for h in ["x-real-ip", "cf-connecting-ip"] {
assert!(
low.contains(&format!("{h}:")),
"`{h}` **不该**被删 —— 官方也不删它(不在 Go 那份清单里)。\
删掉会偏离官方语义、破坏依赖它的后端。实际报文:{raw}"
);
}
}
#[test]
fn http2https_要原样透传_x_forwarded() {
let b = bridge("http2https");
let mut req = parts(&["GET / HTTP/1.1", "Host: h", "X-Forwarded-For: 1.2.3.4"]);
let raw = String::from_utf8(b.rewrite_request(&mut req, &ctx(), false)).unwrap();
assert!(raw.contains("X-Forwarded-For: 1.2.3.4"), "{raw}");
assert!(!raw.contains("X-Forwarded-Proto"), "{raw}");
}
#[test]
fn https2http_要补_x_forwarded_并追加真实客户端_ip() {
let b = Bridge {
in_tls: Some(Arc::new(
rustls::ServerConfig::builder_with_provider(
nfrp_common::frp::tls::crypto_provider(),
)
.with_safe_default_protocol_versions()
.unwrap()
.with_no_client_auth()
.with_cert_resolver(Arc::new(NoCerts)),
)),
out_tls: false,
raw: false,
local_addr: "127.0.0.1:8080".into(),
host_rewrite: String::new(),
extra_headers: BTreeMap::new(),
set_x_forwarded: true,
};
let mut req = parts(&[
"GET / HTTP/1.1",
"Host: vhost.example.com",
"X-Forwarded-For: 10.0.0.1",
]);
let raw = String::from_utf8(b.rewrite_request(&mut req, &ctx(), false)).unwrap();
assert!(
raw.contains("X-Forwarded-For: 10.0.0.1, 203.0.113.7"),
"必须追加**真实客户端** IP:{raw}"
);
assert!(raw.contains("X-Forwarded-Host: vhost.example.com"), "{raw}");
assert!(raw.contains("X-Forwarded-Proto: https"), "{raw}");
}
#[test]
fn host_rewrite_与自定义头生效() {
let mut b = bridge("http2http");
b.host_rewrite = "backend.internal".into();
b.extra_headers.insert("X-From-Where".into(), "frp".into());
let mut req = parts(&["GET /a?b=1 HTTP/1.1", "Host: old"]);
let raw = String::from_utf8(b.rewrite_request(&mut req, &ctx(), false)).unwrap();
assert!(raw.starts_with("GET /a?b=1 HTTP/1.1\r\n"), "{raw}");
assert!(raw.contains("Host: backend.internal"), "{raw}");
assert!(raw.contains("X-From-Where: frp"), "{raw}");
assert!(!raw.contains("Host: old"), "{raw}");
}
#[test]
fn 绝对形式请求行会被还原() {
let b = bridge("http2http");
let mut req = parts(&["GET http://vhost.example.com/a?b=1 HTTP/1.1", "Host: x"]);
let raw = String::from_utf8(b.rewrite_request(&mut req, &ctx(), false)).unwrap();
assert!(raw.starts_with("GET /a?b=1 HTTP/1.1\r\n"), "{raw}");
}
#[test]
fn 升级请求必须留着_connection_和_upgrade() {
let b = bridge("http2http");
let mut req = parts(&[
"GET /ws HTTP/1.1",
"Host: h",
"Connection: Upgrade",
"Upgrade: websocket",
]);
assert!(wants_upgrade(&req));
let raw = String::from_utf8(b.rewrite_request(&mut req, &ctx(), true)).unwrap();
assert!(
raw.to_ascii_lowercase().contains("upgrade: websocket"),
"{raw}"
);
assert!(
raw.to_ascii_lowercase().contains("connection: upgrade"),
"{raw}"
);
assert!(
!raw.contains("Connection: close"),
"升级时不能改成 close:{raw}"
);
}
#[test]
fn 没有_upgrade_头的_connection_upgrade_不算升级() {
assert!(!wants_upgrade(&parts(&[
"GET / HTTP/1.1",
"Connection: upgrade"
])));
assert!(!wants_upgrade(&parts(&["GET / HTTP/1.1", "Upgrade: h2c"])));
assert!(wants_upgrade(&parts(&[
"GET / HTTP/1.1",
"Connection: TE, Upgrade",
"Upgrade: h2c"
])));
}
use std::net::SocketAddr;
use std::time::Duration;
use nfrp_common::http_relay::find_subslice;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpListener;
const TIMEOUT: Duration = Duration::from_secs(5);
async fn read_request<S: AsyncRead + Unpin>(s: &mut S) -> Vec<u8> {
let mut got = Vec::new();
let mut b = [0u8; 4096];
let head_end = loop {
let n = tokio::time::timeout(TIMEOUT, s.read(&mut b))
.await
.expect("上游读请求头超时")
.expect("上游读请求头出错");
assert!(
n > 0,
"头没读完客户端就断了:{:?}",
String::from_utf8_lossy(&got)
);
got.extend_from_slice(&b[..n]);
if let Some(i) = find_subslice(&got, b"\r\n\r\n") {
break i + 4;
}
};
let head = String::from_utf8_lossy(&got[..head_end]).to_ascii_lowercase();
let want: usize = head
.lines()
.find_map(|l| l.strip_prefix("content-length:"))
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0);
while got.len() < head_end + want {
let n = tokio::time::timeout(TIMEOUT, s.read(&mut b))
.await
.expect("上游读请求体超时")
.expect("上游读请求体出错");
if n == 0 {
break;
}
got.extend_from_slice(&b[..n]);
}
got
}
async fn serve_one<S: AsyncRead + AsyncWrite + Unpin>(mut s: S, reply: &[u8]) -> Vec<u8> {
let got = read_request(&mut s).await;
s.write_all(reply).await.expect("上游写响应失败");
s.flush().await.expect("上游 flush 失败");
got
}
async fn fake_upstream(reply: &'static [u8]) -> (SocketAddr, tokio::task::JoinHandle<Vec<u8>>) {
let l = TcpListener::bind("127.0.0.1:0")
.await
.expect("绑定假上游失败");
let addr = l.local_addr().unwrap();
let h = tokio::spawn(async move {
let (s, _) = l.accept().await.expect("假上游 accept 失败");
serve_one(s, reply).await
});
(addr, h)
}
#[allow(clippy::type_complexity)]
async fn fake_tls_upstream(
reply: &'static [u8],
) -> (SocketAddr, tokio::task::JoinHandle<Vec<u8>>) {
let l = TcpListener::bind("127.0.0.1:0")
.await
.expect("绑定假上游失败");
let addr = l.local_addr().unwrap();
let h = tokio::spawn(async move {
let (s, _) = l.accept().await.expect("假上游 accept 失败");
let s = TlsAcceptor::from(nfrp_common::frp::tls::server_config().unwrap())
.accept(s)
.await
.expect("假上游 TLS 握手失败");
serve_one(s, reply).await
});
(addr, h)
}
#[allow(clippy::type_complexity)]
async fn silent_upstream() -> (
SocketAddr,
tokio::sync::oneshot::Receiver<()>,
tokio::sync::oneshot::Receiver<()>,
) {
let l = TcpListener::bind("127.0.0.1:0")
.await
.expect("绑定假上游失败");
let addr = l.local_addr().unwrap();
let (got_tx, got_rx) = tokio::sync::oneshot::channel();
let (cls_tx, cls_rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let (mut s, _) = l.accept().await.expect("假上游 accept 失败");
let mut b = [0u8; 4096];
if let Ok(Ok(_)) = tokio::time::timeout(Duration::from_secs(3), s.read(&mut b)).await {
let _ = got_tx.send(());
}
let mut idle = 0u32;
loop {
match tokio::time::timeout(Duration::from_millis(300), s.read(&mut b)).await {
Ok(Ok(0)) | Ok(Err(_)) => break,
Ok(Ok(_)) => {}
Err(_) => {
idle += 1;
if idle > 20 {
break;
}
}
}
}
let _ = cls_tx.send(());
});
(addr, got_rx, cls_rx)
}
async fn slow_upstream(reply: &'static [u8], delay: Duration) -> SocketAddr {
let l = TcpListener::bind("127.0.0.1:0")
.await
.expect("绑定假上游失败");
let addr = l.local_addr().unwrap();
tokio::spawn(async move {
let (mut s, _) = l.accept().await.expect("假上游 accept 失败");
let _ = read_request(&mut s).await;
tokio::time::sleep(delay).await;
let _ = s.write_all(reply).await;
let _ = s.flush().await;
});
addr
}
fn spawn_drive(b: Bridge) -> (tokio::io::DuplexStream, tokio::task::JoinHandle<Result<()>>) {
let (cli, srv) = tokio::io::duplex(64 * 1024);
let task = tokio::spawn(async move {
let ctx = real_ctx();
b.serve(Box::pin(srv), Vec::new(), &ctx).await
});
(cli, task)
}
#[tokio::test]
async fn 上游悬挂时访问方撤离必须释放上游连接() {
let (addr, got, closed) = silent_upstream().await;
let (mut cli, task) = spawn_drive(Bridge::plain(cfg_to(addr), false, false));
write_req(&mut cli, b"GET / HTTP/1.1\r\nHost: x\r\n\r\n").await;
tokio::time::timeout(Duration::from_secs(2), got)
.await
.expect("上游 2s 内应当收到请求")
.expect("通道不该丢");
drop(cli); tokio::time::timeout(Duration::from_secs(2), closed)
.await
.expect("访问方撤离后 2s 内上游连接必须被释放")
.expect("通道不该丢");
let r = tokio::time::timeout(TIMEOUT, task)
.await
.expect("插件应当立即收尾")
.expect("插件任务不 panic");
assert!(r.is_ok(), "访问方撤离应当安静收尾:{r:?}");
}
#[tokio::test]
async fn 上游tls握手悬挂时访问方撤离必须释放连接() {
let (addr, got, closed) = silent_upstream().await; let (mut cli, task) = spawn_drive(Bridge::plain(cfg_to(addr), false, true)); write_req(&mut cli, b"GET / HTTP/1.1\r\nHost: x\r\n\r\n").await;
tokio::time::timeout(Duration::from_secs(2), got)
.await
.expect("上游 2s 内应当收到 ClientHello")
.expect("通道不该丢");
drop(cli);
tokio::time::timeout(Duration::from_secs(2), closed)
.await
.expect("访问方撤离后 2s 内上游连接必须被释放")
.expect("通道不该丢");
let r = tokio::time::timeout(TIMEOUT, task)
.await
.expect("插件应当立即收尾")
.expect("插件任务不 panic");
assert!(r.is_ok(), "访问方撤离应当安静收尾:{r:?}");
}
#[tokio::test]
async fn 上游tls握手沉默时有界失败() {
let (addr, _got, _closed) = silent_upstream().await;
let (mut cli, task) = spawn_drive(Bridge::plain(cfg_to(addr), false, true));
let t0 = std::time::Instant::now();
write_req(&mut cli, b"GET / HTTP/1.1\r\nHost: x\r\n\r\n").await;
let r = tokio::time::timeout(TIMEOUT, task)
.await
.expect("上游 TLS 握手必须**有界**失败,不能永久挂住")
.expect("插件任务不 panic");
let e = r.expect_err("沉默上游 + 在线访问方:应当报握手超时");
assert!(format!("{e:#}").contains("超时"), "错误要说清是超时:{e:#}");
assert!(
t0.elapsed() < Duration::from_secs(3),
"应当快速失败(test 超时 800ms),实际 {:?}",
t0.elapsed()
);
}
#[tokio::test]
async fn 慢响应上游照常送达不误伤() {
let addr = slow_upstream(
b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok",
Duration::from_millis(2000),
)
.await;
let (mut cli, task) = spawn_drive(Bridge::plain(cfg_to(addr), false, false));
write_req(&mut cli, b"GET / HTTP/1.1\r\nHost: x\r\n\r\n").await;
let out = read_resp(&mut cli).await; assert!(
out.starts_with(b"HTTP/1.1 200"),
"慢上游的响应必须完整送达:{:?}",
String::from_utf8_lossy(&out)
);
assert!(out.ends_with(b"ok"));
let r = tokio::time::timeout(TIMEOUT, task)
.await
.expect("插件应正常收尾")
.expect("插件任务不 panic");
assert!(r.is_ok(), "{r:?}");
}
fn tmp_cert(tag: &str) -> (String, String) {
let k = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
let dir = std::env::temp_dir().join(format!("nfrp-plugin-{tag}"));
std::fs::create_dir_all(&dir).unwrap();
let crt = dir.join("crt.pem");
let key = dir.join("key.pem");
std::fs::write(&crt, k.cert.pem()).unwrap();
std::fs::write(&key, k.key_pair.serialize_pem()).unwrap();
(
crt.to_string_lossy().into_owned(),
key.to_string_lossy().into_owned(),
)
}
fn cfg_to(addr: SocketAddr) -> BridgeConfig {
BridgeConfig {
local_addr: addr.to_string(),
host_rewrite: String::new(),
extra_headers: BTreeMap::new(),
crt_path: String::new(),
key_path: String::new(),
}
}
fn real_ctx() -> Ctx<'static> {
Ctx {
proxy_name: "p",
src: Some("203.0.113.7:1234".parse().unwrap()),
}
}
async fn write_req<S: AsyncWrite + Unpin>(s: &mut S, b: &[u8]) {
tokio::time::timeout(TIMEOUT, s.write_all(b))
.await
.expect("写请求超时")
.expect("写请求失败");
}
async fn read_resp<S: AsyncRead + Unpin>(s: &mut S) -> Vec<u8> {
let mut out = Vec::new();
let mut b = [0u8; 4096];
loop {
match tokio::time::timeout(TIMEOUT, s.read(&mut b)).await {
Err(_) => panic!("读响应超时,已读到 {:?}", String::from_utf8_lossy(&out)),
Ok(Ok(0)) => break,
Ok(Ok(n)) => out.extend_from_slice(&b[..n]),
Ok(Err(e)) => panic!("读响应出错:{e}"),
}
}
out
}
async fn drive(b: Bridge, req: &[u8]) -> (Vec<u8>, Result<()>) {
let (mut cli, srv) = tokio::io::duplex(64 * 1024);
let task = tokio::spawn(async move {
let ctx = real_ctx();
b.serve(Box::pin(srv), Vec::new(), &ctx).await
});
write_req(&mut cli, req).await;
let out = read_resp(&mut cli).await;
let r = tokio::time::timeout(TIMEOUT, task)
.await
.expect("插件收尾超时")
.expect("插件任务 panic");
(out, r)
}
#[tokio::test]
async fn 端到端_http2http_把请求体一起送给上游并把响应搬回来() {
let (up, got) = fake_upstream(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok").await;
let b = Bridge::http2http(cfg_to(up)).unwrap();
let (out, r) = drive(
b,
b"POST /a?b=1 HTTP/1.1\r\nHost: vhost.example.com\r\n\
X-Forwarded-For: 1.2.3.4\r\nContent-Length: 5\r\n\
Connection: close\r\n\r\nhello",
)
.await;
r.expect("插件应当正常收尾");
let up_req = String::from_utf8(got.await.expect("假上游任务 panic")).unwrap();
assert!(up_req.starts_with("POST /a?b=1 HTTP/1.1\r\n"), "{up_req}");
assert!(
up_req.ends_with("\r\n\r\nhello"),
"请求体必须完整交给上游(少一个字节上游就会一直等):{up_req}"
);
assert!(
!up_req.to_ascii_lowercase().contains("x-forwarded-for"),
"http2http 要删掉伪造的 XFF:{up_req}"
);
assert!(up_req.contains("Connection: close"), "{up_req}");
let resp = String::from_utf8(out).unwrap();
assert!(resp.starts_with("HTTP/1.1 200 OK\r\n"), "{resp}");
assert!(resp.ends_with("ok"), "响应体要原样搬回来:{resp}");
}
#[tokio::test]
async fn 端到端_http2https_上游握手与响应都通() {
let (up, got) = fake_tls_upstream(b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\ntls").await;
let b = Bridge::http2https(cfg_to(up)).unwrap();
let (out, r) = drive(
b,
b"GET / HTTP/1.1\r\nHost: vhost\r\nX-Forwarded-For: 1.2.3.4\r\nConnection: close\r\n\r\n",
)
.await;
r.expect("插件应当正常收尾");
assert!(
String::from_utf8(got.await.unwrap())
.unwrap()
.contains("X-Forwarded-For: 1.2.3.4"),
"http2https 要原样透传 XFF"
);
let resp = String::from_utf8(out).unwrap();
assert!(resp.ends_with("tls"), "{resp}");
}
#[tokio::test]
async fn 端到端_https2http_终止tls并补真实客户端ip() {
let (crt, key) = tmp_cert("h2h");
let (up, got) = fake_upstream(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n").await;
let mut c = cfg_to(up);
c.crt_path = crt;
c.key_path = key;
let b = Bridge::https2http(c).unwrap();
let (cli, srv) = tokio::io::duplex(64 * 1024);
let task = tokio::spawn(async move {
let ctx = real_ctx();
b.serve(Box::pin(srv), Vec::new(), &ctx).await
});
let name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
let tls = TlsConnector::from(nfrp_common::frp::tls::client_config().unwrap())
.connect(name, cli)
.await
.expect("与插件做 TLS 握手失败");
let mut tls = Box::pin(tls);
write_req(
&mut tls,
b"GET / HTTP/1.1\r\nHost: vhost.example.com\r\nX-Forwarded-For: 10.0.0.1\r\nConnection: close\r\n\r\n",
)
.await;
let out = read_resp(&mut tls).await;
task.await.unwrap().unwrap();
let up_req = String::from_utf8(got.await.unwrap()).unwrap();
assert!(
up_req.contains("X-Forwarded-For: 10.0.0.1, 203.0.113.7"),
"要追加真实客户端 IP:{up_req}"
);
assert!(up_req.contains("X-Forwarded-Proto: https"), "{up_req}");
assert!(
up_req.contains("X-Forwarded-Host: vhost.example.com"),
"{up_req}"
);
assert!(String::from_utf8(out)
.unwrap()
.starts_with("HTTP/1.1 200 OK"));
}
#[tokio::test]
async fn 端到端_https2https_两端都是tls() {
let (crt, key) = tmp_cert("h2hs");
let (up, got) = fake_tls_upstream(b"HTTP/1.1 204 No Content\r\n\r\n").await;
let mut c = cfg_to(up);
c.crt_path = crt;
c.key_path = key;
let b = Bridge::https2https(c).unwrap();
let (cli, srv) = tokio::io::duplex(64 * 1024);
let task = tokio::spawn(async move {
let ctx = real_ctx();
b.serve(Box::pin(srv), Vec::new(), &ctx).await
});
let name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
let tls = TlsConnector::from(nfrp_common::frp::tls::client_config().unwrap())
.connect(name, cli)
.await
.expect("与插件做 TLS 握手失败");
let mut tls = Box::pin(tls);
write_req(
&mut tls,
b"GET /x HTTP/1.1\r\nHost: h\r\nConnection: close\r\n\r\n",
)
.await;
let out = read_resp(&mut tls).await;
task.await.unwrap().unwrap();
assert!(String::from_utf8(got.await.unwrap())
.unwrap()
.starts_with("GET /x HTTP/1.1"));
assert!(String::from_utf8(out).unwrap().contains("204 No Content"));
}
#[tokio::test]
async fn 端到端_tls2raw_不解析http直接裸对流拷() {
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
let up = l.local_addr().unwrap();
let got = tokio::spawn(async move {
let (mut s, _) = l.accept().await.unwrap();
let mut buf = Vec::new();
s.read_to_end(&mut buf).await.unwrap();
s.write_all(&buf).await.unwrap();
s.flush().await.unwrap();
buf
});
let (crt, key) = tmp_cert("t2r");
let mut c = cfg_to(up);
c.crt_path = crt;
c.key_path = key;
let b = Bridge::tls2raw(c).unwrap();
let (cli, srv) = tokio::io::duplex(64 * 1024);
let task = tokio::spawn(async move {
let ctx = real_ctx();
b.serve(Box::pin(srv), Vec::new(), &ctx).await
});
let name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
let tls = TlsConnector::from(nfrp_common::frp::tls::client_config().unwrap())
.connect(name, cli)
.await
.expect("与插件做 TLS 握手失败");
let mut tls = Box::pin(tls);
write_req(&mut tls, b"raw-bytes-not-http").await;
tls.shutdown().await.expect("关掉 TLS 写方向失败");
let out = read_resp(&mut tls).await;
task.await.unwrap().unwrap();
assert_eq!(
got.await.unwrap(),
b"raw-bytes-not-http",
"上游该收到明文原样"
);
assert_eq!(out, b"raw-bytes-not-http", "回程也要原样");
}
#[derive(Debug)]
struct NoCerts;
impl rustls::server::ResolvesServerCert for NoCerts {
fn resolve(
&self,
_hello: rustls::server::ClientHello<'_>,
) -> Option<Arc<rustls::sign::CertifiedKey>> {
None
}
}
}