wmproxy
wmproxy
已用Rust
实现http/https
代理, socks5
代理, 反向代理, 负载均衡, 静态文件服务器,websocket
代理,四层 TCP/UDP 转发,内网穿透等,会将实现过程分享出来,感兴趣的可以一起造个轮子
项目地址
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
设计目标
让系统拥有 acme 的能力,即可以领取Let's Encrypt
的证书签发,快速实现上线部署。
acme 是什么?
ACME(Automated Certificate Management Environment)
是一个用于自动化管理 SSL/TLS 证书的协议。它通过自动获取、自动更新和自动拒绝等功能,可以大大提高 SSL 证书的管理和更新效率,降低错误风险,提高网站的安全性和稳定性。
当 ACME 服务器发布不安全的 SSL 证书时,可以通过 ACME 协议自动拒绝证书,确保网站始终使用安全的 SSL 证书。此外,ACME 协议还支持自动续期功能,这意味着在证书到期之前,系统可以自动申请并获取新的证书,从而避免了因证书过期而导致的网站访问中断或安全风险。
acme 的定义
acme 是一个可以自动获取 TLS 证书的协议,acmev1 已经被正式弃用,现行的 acme 在rfc8555定义。其中定义了 SSL 如何获取的整个过程,包括其中最重要的权限鉴定。
以下是两种 acme 判定权限拥有者的鉴权方式,以下是wmproxy.net
做为域名来举例。
HTTP-01 方式鉴定
HTTP-01 的校验原理是访问给你域名指向的 HTTP 服务增加一个临时 location,Let’s Encrypt
会发送 http 请求到 http://wmproxy.net/.well-known/acme-challenge/
,wmproxy.net
就是被校验的域名,TOKEN 是 ACME 协议的客户端负责放置的文件,在这里 ACME 客户端就是 acme-lib
。Let’s Encrypt 会对比 TOKEN 是否符合预期,校验成功后就会颁发证书。不支持泛域名证书。成功后我们就可以拥有 TLS 证书了。
DNS-01 方式鉴定
在 ACME DNS 质询验证的自动化中,以下是一些关键步骤:
生成一个 DNS TXT 记录,如_acme-challenge
。
将 TXT 记录添加到 DNS 区域中。
通知 Let's Encrypt 验证 DNS 记录。
等待 Let's Encrypt 验证完成。
如果验证成功,则生成证书。
删除 DNS TXT 记录。
此方法不需要你的服务使用 Http 服务,并且支持泛域名证书。
优点不需要 HTTP 服务器支持泛域名
缺点各 DNS 服务商均不一致
acme 在保证安全的情况下缩短了 TLS 证书的申请流程,可以自动化的进行部署,极大的缓解因证书过期带来的麻烦。
代码实现
依赖:acme-lib改造:之前是确定配置证书及密钥后直接生成完整的 TLS 信息TlsAcceptor
,那么现在在未申请到证书前,不能确定完整的TlsAcceptor
,需要对初始化对象进行重新改造处理。源码:wrap_tls_accepter定义:
/// 为了适应acme, 重新改造Acceptor进行封装处理
#[derive(Clone)]
pub struct WrapTlsAccepter {
pub last: Instant,
pub domain: Option<String>,
pub accepter: Option<TlsAcceptor>,
}
复制代码
同样添加 accept 方法
#[inline]
pub fn accept<IO>(&self, stream: IO) -> io::Result<Accept<IO>>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
self.accept_with(stream, |_| ())
}
pub fn accept_with<IO, F>(&self, stream: IO, f: F) -> io::Result<Accept<IO>>
where
IO: AsyncRead + AsyncWrite + Unpin,
F: FnOnce(&mut ServerConnection),
{
if let Some(a) = &self.accepter {
Ok(a.accept_with(stream, f))
} else {
self.check_and_request_cert()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "load https error"))?;
Err(io::Error::new(io::ErrorKind::Other, "try next https error"))
}
}
复制代码
当accepter
未初始化时,我们将会试图检查证书,查看是否能签发证书。
此处我们为了避免并发中,重复多次请求导致请求数过多导致的服务不可用,我们此处定义了全局静态变量。
lazy_static! {
static ref CACHE_REQUEST: Mutex<HashMap<String, Instant>> = Mutex::new(HashMap::new());
}
复制代码
在检查的时候,我们只允许一段时间内仅有一个请求进入申请证书的流程,其它的请求全部返回错误:
let mut map = CACHE_REQUEST
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Fail get Lock"))?;
if let Some(last) = map.get(self.domain.as_ref().unwrap()) {
if last.elapsed() < Duration::from_secs(30) {
return Err(io::Error::new(io::ErrorKind::Other, "等待上次请求结束").into());
}
}
map.insert(self.domain.clone().unwrap(), Instant::now());
复制代码
然后我们对该域名发起证书签名请求,此处我们会循环卡住整个线程,而非异步的请求,所以我们这里用了thread::spawn
而非tokio::spawn
:
let obj = self.clone();
thread::spawn(move || {
let _ = obj.request_cert();
});
复制代码
以下是请求证书的函数:
fn request_cert(&self) -> Result<(), Error> {
// 使用let's encrypt签发证书
let url = DirectoryUrl::LetsEncrypt;
let path = Path::new(".well-known/acme-challenge");
if !path.exists() {
let _ = std::fs::create_dir_all(path);
}
// 使用内存的存储结构,存储自己做处理
let persist = MemoryPersist::new();
// 创建目录节点
let dir = Directory::from_url(persist, url)?;
// 设置请求的email信息
let acc = dir.account("wmproxy@wmproxy.net")?;
// 请求签发的域名
let mut ord_new = acc.new_order(&self.domain.clone().unwrap_or_default(), &[])?;
let start = Instant::now();
// 以下域名的鉴权,需要等待let's encrypt确认信息
let ord_csr = loop {
// 成功签发,跳出循环
if let Some(ord_csr) = ord_new.confirm_validations() {
break ord_csr;
}
// 超时30秒,认为失败了
if start.elapsed() > Duration::from_secs(30) {
println!("获取证书超时");
return Ok(());
}
// 获取鉴权方式
let auths = ord_new.authorizations()?;
// 以下是HTTP的请求方法,本质上是请求token的url,然后返回正确的值
// 此处我们用的是临时服务器
//
// /var/www/.well-known/acme-challenge/<token>
//
// http://mydomain.io/.well-known/acme-challenge/<token>
let chall = auths[0].http_challenge();
// 将token存储在目录下
let token = chall.http_token();
let path = format!(".well-known/acme-challenge/{}", token);
// 获取token的内容
let proof = chall.http_proof();
Helper::write_to_file(&path, proof.as_bytes())?;
// 等待acme检测时间,以ms计
chall.validate(5000)?;
// 再尝试刷新acme请求
ord_new.refresh()?;
};
// 创建rsa的密钥对
let pkey_pri = create_rsa_key(2048);
// 提交CSR获取最终的签名
let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?;
// 下载签名及证书,此时下载下来的为pkcs#8证书格式
let cert = ord_cert.download_and_save_cert()?;
Helper::write_to_file(
&self.get_cert_path().unwrap(),
cert.certificate().as_bytes(),
)?;
Helper::write_to_file(&self.get_key_path().unwrap(), cert.private_key().as_bytes())?;
Ok(())
}
复制代码
在其中,我们跟 acme 服务器的时候我们需要架设临时文件服务器以使 acme 访问我们 http 服务器的时候http://mydomain.io/.well-known/acme-challenge/<token>
能正确的返回正常的请求,我们将在绑定 tls 的时候,如果没有该证书信息时,我们将自动添加一个.well-known/acme-challenge
的 location 以启用 https 的验证:
pub async fn bind(
&mut self,
) -> ProxyResult<(Vec<Option<WrapTlsAccepter>>, Vec<bool>, Vec<TcpListener>)> {
// ...
for value in &mut self.server {
// ...
if has_acme {
let mut location = LocationConfig::new();
let file_server = FileServer::new(
".well-known/acme-challenge".to_string(),
"/.well-known/acme-challenge".to_string(),
);
location.rule = Matcher::from_str("/.well-known/acme-challenge/").expect("matcher error");
location.file_server = Some(file_server);
value.location.insert(0, location);
}
}
Ok((accepters, tlss, listeners))
}
复制代码
以启用远程 acme 能访问该链接的能力,也就意味着我们不能将敏感信息放置在".well-known/acme-challenge"
目录下面,也就是我们使用MemoryPersist
的原因。
测试是否可行
因为 http-01 的方式必须使 acme 能访问我们的服务器,所以此时测试需要公网环境下进行测试:我们配置如下文件,reverse.toml:
# 反向代理相关,七层协议为http及https
[http]
# 反向代理中的具体服务,可配置多个多组
[[http.server]]
bind_addr = "0.0.0.0:80"
bind_ssl = "0.0.0.0:443"
up_name = "auto1.wmproxy.net"
root = ""
[[http.server.location]]
rule = "/"
static_response = "I'm Ok {client_ip}"
复制代码
此时布置在我们的auto1.wmproxy.net
的服务器上,我们运行
wmproxy run -c reverse.toml
复制代码
此时当我们访问https://auto1.wmproxy.net
的请求的时候,将会触发证书申请,成功后证书将放置在".well-known"
下面,下次启动服务器的时候我们将自动加载已请求的 tls 证书以提供 https 服务。
频繁限制问题
在 let's encrypt 中,如果有早过 5 次成功后,需要 2 天后才能继续申请,他将无限返回 429,得注意控制申请证书的频率。
总结
TLS 证书在当今互联网中处于最重要的一环,他保护着我们的隐私数据的安全,也是最流行的加密方式之一。所以 TLS 证书的快速部署对于小而美的应用能让其快速的落地使用。
文章转载自:问蒙服务框架
原文链接:https://www.cnblogs.com/wmproxy/p/18033496/wmproxy50
体验地址:http://www.jnpfsoft.com/?from=001
评论