wmproxy

wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

敏感的时间

  现实生活中大家都对时间有着概念,比如“快上班了,要不然要迟到了。”、“这班怎么这么久,怎么还没下班?”、“啊?已经晚上12点啦,等我这把游戏玩完。”、“叮叮叮,起床闹钟一直在催着你起床了。”

  闹钟、自然变化、生物钟为我们提供着时间的保证。而计算机的世界里,就靠着硬件定时器,控制着时间的流逝,如果哪一天本地时间和别人的时间不一致了,此时需要找别人对时,这也就是经典的网络时间协议(NTP)

  现实的生活中,通常以分钟或者小时乃至天去和别人约定时间,而在计算机的世界里,在我们看来那就是朝生暮死的蚍蜉一般,他们生命较短,所以对他们来说,通常用s或者ms来乃至μs做通知,所以需要严格的遵守时间约定,绝对不允许有赖床的行为。

HTTP行为中的定时器

HTTP/HTTPS/WebSocket的访问撑起了互联网总流量的半臂江山,日常接触中APP或者游戏这种对外服务的基本上均是通过HTTP及HTTPS进行服务,长链接由于兼容小程序这类的大部分游戏直接也从普通的Socket转为WebSocket直接做全网的兼容。WebSocket的基础协议是由HTTP升级而来,所以也归于HTTP协议。

主要有以下列行为定时器

  1. 连接超时定时器
  2. 读操作超时定时器
  3. 写操作超时定时器
  4. 读/写操作超时定时器(在规则的时间内同时完成读和写)
  5. keep-alive超时定时器(连接保持的最长时间)

定时器类的定义

相关的超时数据均存放在该类里,接下来类型进行相应的处理

#[derive(Debug)]
pub struct TimeoutLayer {
pub connect_timeout: Option<Duration>,
pub read_timeout: Option<Duration>,
pub write_timeout: Option<Duration>,
pub timeout: Option<Duration>,
/// keep alive 超时时长
pub ka_timeout: Option<Duration>, read_timeout_sleep: Option<Pin<Box<Sleep>>>,
write_timeout_sleep: Option<Pin<Box<Sleep>>>,
timeout_sleep: Option<Pin<Box<Sleep>>>,
ka_timeout_sleep: Option<Pin<Box<Sleep>>>,
}

连接超时定时器

  此时约定的是客户端向服务端请求TCP连接建立的最长时间,如果没有约定,将由系统连接超时的或者确认不可达的时候才返回失败。

  如果没有连接超时,那么以下我们的型业务场景模拟:

  1. 在弱网的环境下,比如手机信号不好的地方,一次连接请求建立可能会花费10秒左右才能返回,那么如果我们没有连接超时,用户在等待了8秒,客户端的界面都没有办法给出任何的响应,就会认为服务端出现了问题或者频繁的重启客户端应用不断的进行重试而得不到预期的反馈。在App设计中,对这种情况的用户体验极差,需要在指定的时间内给用户反馈出当前网络无法访问。

  2. 客户端的设计模型中,如果有定时请求或者埋点数据之类,如果没有超时机制,容易出现短时间内打开的socket过多出现资源耗尽的情况,其次客户端不知道是否将数据已经进行发送,不确认是否需要将该条数据进行缓存以便下一次推送。

在设计模型中,连接超时必不可少,因为各种业务场景的不同,需要要不同的时间内得到预期的反馈。以下是连接超时在Rust中的实现,因为只存在于客户端主动连接服务端,所以连接超时只在客户端实现。

async fn inner_connect<A: ToSocketAddrs>(&self, addr: A) -> ProtResult<TcpStream> {
if self.inner.timeout.is_some() {
// 获取是否配置了连接超时, 如果有连接超时那么指定timeout
if let Some(connect) = &self.inner.timeout.as_ref().unwrap().connect_timeout {
match tokio::time::timeout(*connect, TcpStream::connect(addr)).await {
Ok(v) => {
return Ok(v?)
}
Err(_) => return Err(ProtError::Extension("connect timeout")),
}
}
}
let tcp = TcpStream::connect(addr).await?;
Ok(tcp)
}

通过指定超时时间来对连接的建立监听。

读操作超时定时器

大部分HTTP请求,只有得到完整的数据才能进行处理,少部分如文件上传这种可以边上传边操作,而是否能开始操作关系到请求的响应时间。

读超时大概有以下的可能:

  1. 服务器处理请求的时间太长,导致客户端等待超时。
  2. 服务器返回的数据量过大,导致客户端读取数据的时间超过了规定的时间。
  3. 网络延迟或网络不稳定,导致客户端无法在规定的时间内读取完数据。

这造成客户端无法及时处理数据,可以报错好让客户端换备用线路或者备用服务器等以便及时的处理数据。

读操作我们不管服务端或者客户端,不管http/1.1或者http/2均由is_read_end字段来判定,在未读完当前请求的数据前,判断是否超时。

pub fn poll_ready(
&mut self,
cx: &mut Context<'_>,
ready_time: Instant,
is_read_end: bool,
is_write_end: bool,
is_idle: bool,
) -> ProtResult<()> {
let now = Instant::now();
if !is_read_end {
if let Some(read) = &self.read_timeout {
let next = ready_time + *read;
if now >= next {
return Err(crate::ProtError::Extension("read timeout"));
}
if self.read_timeout_sleep.is_some() {
self.read_timeout_sleep.as_mut().unwrap().as_mut().set(tokio::time::sleep_until(next.into()));
} else {
self.read_timeout_sleep = Some(Box::pin(tokio::time::sleep_until(next.into())));
}
let _ = Pin::new(self.read_timeout_sleep.as_mut().unwrap()).poll(cx);
}
} Ok(())
}

其中比较复杂的是如何判断is_read_end,因为需要区分服务端客户端或者是http/1.1及http/2,这个源码实现 http2核心http1.1核心

写操作超时定时器

对于客户端的写,就是将请求发送到服务端,而服务端的写,刚好是将返回发送给客户端。这是数据处理的重要的一环。

  在异步的处理socket中,都会将socket设置成非阻塞,也就是nonblocking,此时我们会得到一个默认的内核缓冲区大小。如果在该缓冲区未满前,我们写入数据将是0等待,该缓冲区的数据将由系统进行数据发送给另一端。

  如果对方不将我们的缓冲区数据读走,那么我们此时是无法在写入到该缓冲区的,如果远程端不读,我们的传输速度将会无限的接近于0KB/S。

  此时如果是服务端,有数千上万个这种该连接,每分钟只读走几个字节的数据,那么没有写入操作,我们将要保持上万的空闲连接,而默认的端口连接数为65535,那么客户端将会耗尽我们的服务资源。此种为 [慢速攻击]

该操作是通过is_write_end来判断是否写入完成。监听方式和读的一致,核心代码在 timeout,此处不再赘述

读/写操作超时定时器

该定时器是由读和写需要共同来完成的,在很多场景中,我们只关心该请求需要耗时多少时间来完成,此时我们不关心是读的时间或者写的时间,所以此时表示请求完成的需要在这个时间下完成

就比如HTTP/1.1中的Slow headers,也就是慢速头攻击。正常来说,我们的http/1.1的头类似如下:

GET / HTTP/1.1\r\n
Host : wm-proxy.com\r\n
Connection: keep-alive\r\n
Keep-Alive: 900\r\n
Content-Length: 100000000\r\n
Content_Type: application/x-www-form-urlencoded\r\n
Accept: *.*\r\n
\r\n

整个报文的结束将有一个空白的\r\n,如果没有收到该标记,服务端无法得到完整的头信息,也无法正确的把数据转成Request,那么此时客户端可以占用了大量的连接,从而使服务端拒绝服务。

此定时器判断由is_read_endis_write_end有一方为false,监听方法略。

keep-alive操作超时定时器

在http/1.1中,端口是可以复用的,从而减少socket的反复建立关闭,并可以一定程度上快速的响应,这是端口请求完成后,又没有后续的请求,此时当前socket线路为空闲,即当前空闲线路的保持时间。

  keep-alive为保持连接,此参数用的好可以极大的加速服务的访问,此参数设置不好的时候,如设置成9999s,那么客户端将会保持当前的空闲socket很久,从而另一个程度上造成了拒绝服务了。

  此判定由is_idle来判定,判断当前是否空闲,也就是上一个请求已经读写均已完成,后一个请求还未进来,此时就为空闲时间。监听方法雷同,略。

测试客户端

let url = "http://www.baidu.com";
let req = Request::builder().method("GET").header(HeaderName::ACCEPT_ENCODING, "gzip").url(url).body("").unwrap(); Instant::now());
let client = Client::builder()
// 是否支持HTTP2
// .http2(false)
// 是否仅使用HTTP2
.http2_only(true)
// 连接使时时间3秒
.connect_timeout(Duration::new(5, 0))
// 设置keep-alive时间10秒
.ka_timeout(Duration::new(10, 0))
// 设置读超时5秒
// .read_timeout(Duration::new(5, 0))
// 设置写超时5秒
.write_timeout(Duration::new(5, 0))
.connect(url).await.unwrap();
//发起请求
let (mut recv, sender) = client.send2(req.into_type()).await?;
//接收请求
let mut res = recv.recv().await.unwrap();
//接收所有body数据
res.body_mut().wait_all().await;

测试服务端

let mut server = Server::new(stream, Some(addr));
// 设置读操作5秒
server.set_read_timeout(Some(Duration::new(5, 0)));
// 设置写超时5秒
server.set_write_timeout(Some(Duration::new(5, 0)));
// 设置读写超时5秒
server.set_timeout(Some(Duration::new(5, 0)));
async fn operate(req: Request<RecvStream>) -> ProtResult<Response<String>> {
let response = Response::builder()
.version(req.version().clone())
.body("Hello World\r\n".to_string())?;
Ok(response)
}
let _ = server.incoming(operate).await;

结语

  时间的尺度越小,那么对时间的敏感度越高,定时器是约束,也是保护,保护不受攻击。感谢定时器给我们构建一个更加稳定的互联网世界。

点击 [关注][在看][点赞] 是对作者最大的支持

24. 从零用Rust编写正反向代理,细说HTTP行为中的几种定时器的更多相关文章

  1. bloom-server 基于 rust 编写的 rest api cache 中间件

    bloom-server 基于 rust 编写的 rest api cache 中间件,他位于lb 与api worker 之间,使用redis 作为缓存内容存储, 我们需要做的就是配置proxy,同 ...

  2. 正反向代理、负载均衡、Nginx配置实现

    一.正反向代理 1.前提 我们曾经使用翻墙软件,访问google:使用了代理软件时,需要在浏览器选项中配置代理的地址,我们仅仅有代理这个概念,并不清楚代理还有正向和反向之分. 2.正向代理(代替客户端 ...

  3. SSM-Spring-11:Spring中使用代理工厂Bean实现aop的四种增强

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 说说那四种增强:前置增强,后置增强,环绕增强,异常增强 那什么是代理工厂bean呢? org.springfr ...

  4. 程序一 用记事本建立文件src.dat,其中存放若干字符。编写程序,从文件src.dat中读取数据,统计其中的大写字母、小写字母、数字、其它字符的个数,并将这些数据写入到文件test.dat中。

    用记事本建立文件src.dat,其中存放若干字符.编写程序,从文件src.dat中读取数据,统计其中的大写字母.小写字母.数字.其它字符的个数,并将这些数据写入到文件test.dat中. #inclu ...

  5. 详解 Java 中的三种代理模式

    代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 这里使用 ...

  6. 【Linux】windows下编写的脚本文件,放到Linux中无法识别格式

    注意:我启动的时候遇到脚本错误 » sh startup.sh -m standalone tanghuang@bogon : command not found : command not foun ...

  7. 23、有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串

    /* 有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串 */ #include <stdio.h> #include <stdlib ...

  8. 使用Squid部署代理缓存服务(标准正向、透明正反向代理)

    正向代理让用户可以通过Squid服务程序获取网站页面等数据,具体工作形式又分为标准代理模式与透明代理模式.标准正向代理模式: 将网站的数据缓存在服务器本地,提高数据资源被再次访问时的效率,但用户必需在 ...

  9. Nginx正反向代理、负载均衡等功能实现配置

    版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[+]   系统环境: VirtualBox Manager Centos6.4 nginx1.10.0 IP对应的机器名: IP ...

  10. UA池和代理池在scrapy中的应用

    一.下载中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系 ...

随机推荐

  1. STA学习笔记-0

    如今的逻辑设计复杂度和工作频率要求越来越高.为了保证设计稳定可靠,必须对设计附加时序约束,对综合实现结果进行时序分析. 导言 时序约束:主要用于规范设计的时序行为,表达设计者期望满足的时序条件,指导综 ...

  2. python数据处理:获取Dataframe中的一列或一行

    解决方案 df['w'] #选择表格中的'w'列,使用类字典属性,返回的是Series类型 df.w #选择表格中的'w'列,使用点属性,返回的是Series类型 df[['w']] #选择表格中的' ...

  3. 用 Tensorflow.js 做了一个动漫分类的功能(二)

    前言: 前面已经通过采集拿到了图片,并且也手动对图片做了标注.接下来就要通过 Tensorflow.js 基于 mobileNet 训练模型,最后就可以实现在采集中对图片进行自动分类了. 这种功能在应 ...

  4. Hi3798MV200 恩兔N2 NS-1 (二): HiNAS海纳思使用和修改

    目录 Hi3798MV200 恩兔N2 NS-1 (一): 设备介绍和刷机说明 Hi3798MV200 恩兔N2 NS-1 (二): HiNAS海纳思使用和修改 Hi3798MV200 恩兔N2 NS ...

  5. CVE-2022-42475-FortiGate-SSLVPN HeapOverflow 学习记录

    前言 之前就想复现这个洞,不过因为环境的问题迟迟没有开工.巧在前一阵子有个师傅来找我讨论劫持 ssl结构体中函数指针时如何确定堆溢出的偏移,同时还他把搭建好了的环境发给了我,因此才有了此文. 如何劫持 ...

  6. Git-入门使用资料

    一.Git入门教程 Git入门视频,针对于小白快速入门,时常约2~3小时 Git入门视频 相关课件资料: https://pan.baidu.com/s/1U-s4OmkToXJ5Y7BbJ7w2Ww ...

  7. 【路由器】小米 WR30U 解锁并刷机

    本文主要记录个人对小米 WR30U 路由器的解锁和刷机过程,整体步骤与 一般安装流程 类似,但是由于 WR30U 的解锁 ssh 和刷机的过程中有一些细节需要注意,因此记录一下 解锁 ssh 环境准备 ...

  8. CentOS 内网YUM源配置使用

    YUM介绍 yum,是Yellow dog Updater, Modified 的简称,是杜克大学为了提高RPM 软件包安装性而开发的一种软件包管理器.起初是由yellow dog 这一发行版的开发者 ...

  9. (洛谷P4213)杜教筛

    https://www.cnblogs.com/Mychael/p/8744633.html #pragma GCC optimize(3, "Ofast", "inli ...

  10. [htmlayout] csss! 改变值/文本

    <input type="text" value="123" /> <div class="test">内容内容&l ...