20. 从零用Rust编写正反向代理,四层反向代理stream(tcp与udp)实现

news2024/9/28 1:18:54

wmproxy

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

项目地址

gite: https://gitee.com/tickbh/wmproxy

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

四层代理

四层代理,也称为网络层代理,是基于IP地址和端口号的代理方式。它只关心数据包的源IP地址、目的IP地址、源端口号和目的端口号,不关心数据包的具体内容。四层代理主要通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。

因为四层代理不用处理任何相关的包信息,只需将包数据传递给正确的服务器即可,所以实现相对比较简单。

以下是OSI七层模型的示意图,来源于网上

图片.png

实现方式

双端建立连接,也就是收到客户端的连接的时候,同时建立一条通往服务端的连接,然后做双向绑定即可完成服务。

四层代理还有udp的转发需求,需要同步将udp的数据进行转发,udp的处理方式处理会相对复杂一些,因为当前地址只有绑定一份,但是可能来自各种不同的地址,不同的客户端的(remote_ip, remote_port)我们需要当成一个全新的客户端。

而且有时候无法主动感知是否已经被断开了,所以也必须有超时机制,好在超时的时候能及时释放掉连接,好让系统及时的socket资源。

TCP实现

tcp找到相应的地址,连接,并双向绑定即可

pub async fn process<T>(
    data: Arc<Mutex<StreamConfig>>,
    local_addr: SocketAddr,
    mut inbound: T,
    _addr: SocketAddr,
) -> ProxyResult<()>
where
    T: AsyncRead + AsyncWrite + Unpin + std::marker::Send + 'static,
{
    let value = data.lock().await;
    for (_, s) in value.server.iter().enumerate() {
        if s.bind_addr.port() == local_addr.port() {
            let addr = ReverseHelper::get_upstream_addr(&s.upstream, "")?;
            let mut connect = HealthCheck::connect(&addr).await?;
            copy_bidirectional(&mut inbound, &mut connect).await?;
            break;
        }
    }
    Ok(())
}
UDP实现

UDP相对比较复杂,下面我们先列举内部的流程图

根据地址连接发送数据到
将Receiver传到以接收数据
否,将数据Sender给
异步读取数据并发送
绑定反向udp端口
客户端
是否第一次
创建异步协程
异步协程中

在stream绑定的时候,要区分出TCP还是UDP的,做分别的绑定

/// stream的绑定,按bind_mode区分出udp或者是tcp,返回相应的列表
pub async fn bind(&mut self) -> ProxyResult<(Vec<TcpListener>, Vec<StreamUdp>)> {
    let mut listeners = vec![];
    let mut udp_listeners = vec![];
    let mut bind_port = HashSet::new();
    for value in &self.server.clone() {
        if bind_port.contains(&value.bind_addr.port()) {
            continue;
        }
        bind_port.insert(value.bind_addr.port());
        if value.bind_mode == "udp" {
            let listener = Helper::bind_upd(value.bind_addr).await?;
            udp_listeners.push(StreamUdp::new(listener, value.clone()));
        } else {
            let listener = Helper::bind(value.bind_addr).await?;
            listeners.push(listener);
        }
    }
    Ok((listeners, udp_listeners))
}

我们会对连接做分别的监听,下面是udp的获取是否有新数据:

async fn multi_udp_listen_work(
    listens: &mut Vec<StreamUdp>,
) -> (io::Result<(Vec<u8>, SocketAddr)>, usize) {
    if !listens.is_empty() {
        let (data, index, _) =
            select_all(listens.iter_mut().map(|listener| {
                listener.next().boxed()
            })).await;
        if data.is_none() {
            return (Err(io::Error::new(io::ErrorKind::InvalidInput, "read none data")), index)
        }
        (data.unwrap(), index)
    } else {
        let pend = std::future::pending();
        let () = pend.await;
        unreachable!()
    }
}

此处我们用next,也就是我们实现了 futures_core::Stream接口,用Poll的方式来注册实现有事件的时候来通知。

在tokio中,在read或者write的时候返回Poll::Pending,将会将socket的可读可写注册到底层,如果一旦系统可读可写就会通知该接口,将会重新执行一遍futures_core::Stream

我们将同时可以处理可读可写可发送事件,如果接口超时我们将关闭相应的接口。

impl Stream for StreamUdp {
    type Item = io::Result<(Vec<u8>, SocketAddr)>;
    fn poll_next(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Option<Self::Item>> {
        let _ = self.poll_write(cx)?;
        let _ = self.poll_sender(cx)?;
        self.poll_read(cx)
    }
}

下面是主要的StreamUdp

/// Udp转发的处理结构,缓存一些数值以做中转
pub struct StreamUdp {
    /// 读的缓冲类,避免每次都释放
    pub buf: BinaryMut,
    /// 核心的udp绑定端口
    pub socket: UdpSocket,
    pub server: ServerConfig,

    /// 如果接收该数据大小为0,那么则代表通知数据关闭
    pub receiver: Receiver<(Vec<u8>, SocketAddr)>,
    /// 将发送器传达给每个子协程
    pub sender: Sender<(Vec<u8>, SocketAddr)>,

    /// 接收的缓存数据,无法保证全部直接进行发送完毕
    pub cache_data: LinkedList<(Vec<u8>, SocketAddr)>,
    /// 发送的缓存数据,无法保证全部直接进行发送完毕
    pub send_cache_data: LinkedList<(Vec<u8>, SocketAddr)>,
    /// 每个地址绑定的对象,包含Sender,最后操作时间,超时时间
    remote_sockets: HashMap<SocketAddr, InnerUdp>,
}

结果测试

我们自己开一个udp服务端,绑定了本地的8089,我们将接收到的数据前面加上from server:并进行返回,代理端我们绑定了84的端口,并将udp数据转发给8089端:

use tokio::net::UdpSocket;
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    let sock = UdpSocket::bind("0.0.0.0:8089").await?;
    let mut buf = [0; 1024];
    loop {
        let (len, addr) = sock.recv_from(&mut buf).await?;
        let mut vec = "from server: ".as_bytes().to_vec();
        vec.extend(&buf[..len]);
        let _ = sock.send_to(&vec, addr).await?;
    }
}

客户端我们用nc运行:

图片.png

可以看出两个客户端互相独立,彼此返回的数据均符合预期,正常的接收及返回。

TCP我们绑定了83端口并转发到HTTP的本地端口8080,我们用curl进行测试,符合预期,如图:

图片.png

结语

至此四层的反向代理TCP/UDP均已完成,也符合预期。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1396683.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Acrel-1000DP分布式光伏系统在某重工企业18MW分布式光伏中应用

摘 要&#xff1a;分布式光伏发电特指在用户场地附近建设&#xff0c;运行方式以用户侧自发自用、余电上网&#xff0c;且在配电系统平衡调节为特征的光伏发电设施&#xff0c;是一种新型的、具有广阔发展前景的发电和能源综合利用方式&#xff0c;它倡导就近发电&#xff0c;就…

回溯算法篇-01:全排列

力扣46&#xff1a;全排列 排列和组合的区别在于&#xff0c;排列对“顺序”有要求。比如 [1,2] 和 [2,1] 是两个不同的结果。 这就导致了同一个元素 在同一条路径中不可重复使用&#xff0c;在不同的路径中可以重复使用。 比如数组 [1,2,3] &#xff0c;在第一条路径 [1,2,…

【jQuery入门】基础使用-入口函数、顶级对象$

文章目录 前言一、基础使用1.1 jQuery的下载1.2 简单的使用 二、顶级对象$总结 前言 jQuery是一款广泛应用于前端开发的JavaScript库&#xff0c;它简化了许多常见任务的操作&#xff0c;使得代码编写更加便捷。本文将介绍jQuery的基础使用&#xff0c;包括入口函数和顶级对象…

IPFoxy运营干货|谷歌广告Google Ads建立广告需要注意什么?

编辑投放谷歌广告需要多少个步骤和什么准备工作&#xff0c;本文将来讲述&#xff0c;主要分5个内容&#xff1a;一、投放前竞对研究&#xff1b;二、投放前广告账户设置&#xff1b;三、建立广告系列&#xff1b;四、建立广告组&#xff1b;五、广告长期策略。接下来我们来开始…

防火墙技术

防火墙&#xff08;英语&#xff1a;Firewall&#xff09;技术是通过有机结合各类用于安全管理与筛选的软件和硬件设备&#xff0c;帮助计算机网络于其内、外网之间构建一道相对隔绝的保护屏障&#xff0c;以保护用户资料与信息安全性的一种技术。 防火墙技术的功能主要在于及…

【安装VMware Tools】实现Vmware虚拟机和主机之间复制、粘贴内容、拖拽文件

https://www.bilibili.com/video/BV1rN411277B/?spm_id_from333.788.recommend_more_video.6&vd_sourcefb8dcae0aee3f1aab700c21099045395 https://blog.csdn.net/wxqian25/article/details/19406673 待解决方案&#xff1a; 重新下载ubuntu&#xff0c;然后按照 https://…

C#:接口中如何将某个值类型的字段传null?

在实际对接第三方接口时&#xff0c;偶尔会有一些字段在某些情况下是不需要传值的。那如何处理呢&#xff1f; 有两种方法&#xff1a; 1、将值类型改为可空类型&#xff1b; 2、定义基类&#xff0c;基类包含所有必须要传的字段&#xff0c;子类则加入偶尔需要传的字段。 下…

制造业企业数字化转型难点剖析及解决之法

导语 全球正在由工业经济向数字经济转型过渡&#xff0c;制造业正在且并将长期处于数字化转型发展阶段&#xff0c;并沿着数字化、网络化、智能化阶段不断跃升。但如何找准数字化转型的切入点&#xff0c;以低耗能、低成本、高效率的方式加快制造业转型升级的步伐&#xff0c;仍…

米粒图像预处理-图像背景均匀化

案例背景 食品检测在国家粮食安全中拥有举足轻重的地位。随着经济全球化程度的深入&#xff0c;中国巨大的市场消费力将吸引越来越多的国际米类品牌的进入&#xff0c;国内粮食市场的竞争将会日趋激烈&#xff0c;人们越来越注重粮食的安全和品质问题&#xff0c;粮食质量检测…

Sublime Text4 crack时替换的汇编指令

Sublime Text4 crack时替换的汇编指令 首先请支持正版&#xff0c;这里研究破解的步骤&#xff0c;仅做汇编代码学习。 破解步骤很简单&#xff1a; 打开二进制文件&#xff0c; 搜索 80 78 05 00 0F 94 C1&#xff0c; 替换为 C6 40 05 01 48 85 C9. (源: https://gist.git…

SQL Server中数据表的增删查改

文章目录 一、增二、查三、改四、删除 一、增 进行增删查改的前提需要在指定数据库中创建数据表&#xff0c;对这块不大理解的可以先看看前面几期文章&#xff1a; 创建数据库 创建数据表 use StudentManageDB go insert into Students (StudentName,Gender,Birthday,Age,Stu…

【图形学】直线光栅化算法(DDA算法,Bresenham算法和中点算法)

在数学上,直线就是由无穷多个点组成的, 在计算机屏幕显示的话, 需要做一些处理,对于光栅显示器&#xff0c;就是用有限多个点去逼近直线, 我们需要知道每一个像素点的坐标(都是整数) 数学上直线的方程如下 y k x b ykxb ykxb&#xff0c;给定直线的起点坐标 P 0 ( x 0 , y…

Docker部署的gitlab升级指南(15.11.X容器里升级PostgreSQL到13.8)

一、确定当前版本 #进入当前版本容器产看gitlab版本 docker exec -it gitlab cat /opt/gitlab/embedded/service/gitlab-rails/VERSION#显示版本如下 14.4.0二、备份数据&#xff0c;防止升级发生意外 #执行备份命令 docker exec -ti gitlab gitlab-rake gitlab:backup:creat…

MT6789(G99)性能参数/datasheet_MTK联发科4G处理器

联发科MT6789将4G智能手机带入下一代。基于高效率的台积电N6 (6nm级)芯片生产工艺&#xff0c;该芯片可全天候实现出色的游戏&#xff0c;加上大摄像头&#xff0c;快速显示&#xff0c;流畅的流媒体和可靠的全球连接。合作伙伴可以定制联发科Helio G99平台&#xff0c;以满足其…

Python高级编程之IO模型与协程

更多Python学习内容&#xff1a;ipengtao.com 在Python高级编程中&#xff0c;IO模型和协程是两个重要的概念&#xff0c;它们在处理输入输出以及异步编程方面发挥着关键作用。本文将介绍Python中的不同IO模型以及协程的概念、原理和用法&#xff0c;并提供丰富的示例代码来帮助…

go语言(七)----slice的声明方式

1、声明方式一 //声明一个slice1是一个切片&#xff0c;但是并没有给slice分配空间var slice1 []intslice1 make([]int,3)2、声明方式二 声明一个slice切片&#xff0c;同时给slice分配空间&#xff0c;3个空间&#xff0c;初始化值是0var slice1 []int make([]int,3)3、声…

ChatGPT 报错“Oops!We ran into an issue while signing you in…”如何解决?

ChatGPT报错&#xff1a;“Oops&#xff01;We ran into an issue while signing you in, please take abreak and try again soon.” 说明&#xff1a;哎呀&#xff01;我们在登录时遇到了一个问题&#xff0c;请稍作休息并尽快再试一次。 原因&#xff1a; 看到这个提示时&a…

杂记 | 在Linux上使用Docker-compose安装单机版Milvus向量数据库并配置访问控制和可视化面板(Attu)

文章目录 01 Milvus向量数据库简介02 安装前的准备03 安装3.1 创建milvus工作目录3.2 下载并编辑docker-compose.yml3.3 下载milvus.yml文件3.4 启动milvus 04 访问可视化面板并修改密码 01 Milvus向量数据库简介 Milvus是一款开源的向量数据库&#xff0c;它专为AI应用设计&a…