2. 从零用Rust编写正反向代理, 动工,基本理论准备

wmproxy

wmproxy是由Rust编写,已实现http/https代理,socks5代理, 反向代理,静态文件服务器,内网穿透,配置热更新等, 后续将实现websocket代理等,同时会将实现过程分享出来, 感兴趣的可以一起造个轮子法

项目 ++wmproxy++

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

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

设计流程图

图片.png

代理端和代理服务端之间可用自有格式来实现多路复用以减少连接的建立断开的开销,目前暂未实现代理服务端。

类结构

  • proxy.rs 负责代理结构的存储,监听类型,监听地址,是否有父级地址,认证账号密码等。
  • flag.rs 监听类型的二进制结构,可同时支持多结构比较http/https/socks5,如果解析http失败则尝试socks5格式,从而实现多种代理方式的同时支持
  • http.rs http及https代理的实现,如果解析失败则返回ProxyError::Continue,并把已经读取的数据带回,以便后续解析
  • socks5.rs socks5的代理实现,如果数据正确,则均在此处进行转发,解析失败返回Continue

命令行解析

使用Commander对命令行的的数据处理,如-p 8090,-b 127.0.0.1,完整的命令行如wmproxy -p 8090,则可在8090端口上实现http及https的转发,代码示例

let command = Commander::new()
            .version(&env!("CARGO_PKG_VERSION").to_string())
            .usage("-b 127.0.0.1 -p 8090")
            .usage_desc("use http proxy")
            .option_list(
                "-f, --flag [value]",
                "可兼容的方法, 如http https socks5",
                None,
            )
            .option_int("-p, --port [value]", "listen port", Some(8090))
            .option_str(
                "-b, --bind [value]",
                "bind addr",
                Some("0.0.0.0".to_string()),
            )
            .parse_env_or_exit();

let listen_port: u16 = command.get_int("p").unwrap() as u16;
let listen_host = command.get_str("b").unwrap();

启动入口

启动通过tokio的异步协议进行数据的处理,逻辑均在tokio::spawn的异步函数中,所有针对句柄数据的读取写入均由异步完成,从而实现高效率的处理。

while let Ok((mut inbound, _)) = listener.accept().await {
    tokio::spawn(async move {
        // tcp的连接被移动到该协程中,我们只要专注的处理该stream即可
    })
}

HTTP代理

如果该代理信息配置支持http/https则会尝试进行http解析,代码实现在proxy.rs中的process方法,

pub async fn process(mut inbound: TcpStream) -> ProxyResult<()> {
    let request = webparse::Request::new();
    // 通过该方法解析标头是否合法, 若是partial(部分)则继续读数据
    // 若解析失败, 则表示非http协议能处理, 则抛出错误
    match request.parse_buffer(&mut buffer.clone()) {
    }
}

该方法会循环的读取客户端的内容,如果内容为

GET / HTTP/1.1\r\nHost: wwww.baidu.com\r\n\r\n

这表示该请求为普通的http代理,我们解析完HTTP的头文件信息,得出包含的头信息,如果无法解析完整的地址(域名加端口或者ip加端口),则返回错误,无法处理该http信息。


图片.png

注意:客户端和服务端之前可能会存在大数据上传下载的情况,超过百兆数据的上传下载,所以我们为了减少序列化带来的性能损失和保证在低内存能正确运行,不做http的完整解析,仅仅只处理http头信息。

curl测试

export http_proxy=http://127.0.0.1:8090
curl http://www.baidu.com -I

可以正常的返回

HTTP/1.1 200 OK...

HTTPS代理

https处理是在http的基础在在额外解析connect协议来实现, 代理是客户端优先给代理发送connect协议,比如访问https://www.baidu.com那么先优先发如下消息。

CONNECT www.baidu.com:443 HTTP/1.1\r\n
Host: www.baidu.com:443\r\n\r\n

如果收到HTTP的CONNECT的方法则表示他是https的代理协议,那么此时对PATH提示的地址进行连接,连接成功后只需对该连接和客户端做双向绑定即可实现HTTPS代理协议。

curl测试

export https_proxy=http://127.0.0.1:8090
curl https://www.baidu.com -I

可以正常的返回两次,因为在connect的时候要求代理返回一次数据,另一次是https服务器返回,故而显示g

HTTP/1.1 200 OK
HTTP/1.1 200 OK
...

socks5协议

socks5由rfc1928进行定义
代码实现在socks5.rs中的process方法实现
因为在处理socks5之前可能进行过http的尝试,所以socket中的内容已经被读出了一部分,在处理时则带上了Option<BinaryMut>,表示预读的内容。
在socks5中通常需要预读一个字节来获取后续的长度,比如NMethod,或者用户名长度等,所以我们定义了函数

/// 读取至少长度为size的大小的字节数, 如果足够则返回Ok(())
pub async fn read_len<T>(stream: &mut T, buffer: &mut BinaryMut, size: usize) -> ProxyResult<()>
where
    T: AsyncRead + Unpin {
        
    }

这里的stream用的是泛型,只要具有异步读的类型都可以

保证已读内容须不少于多少字节数,然后再进行数据的预处理。
根据我们是否传用用户密码信息来确定socks5的验证方式,如果我们传入了用户密码,如果客户端不支持2的验证方式,则返回(0xFF)表示无验证方法。

curl http://www.baidu.com --socks5 127.0.0.1:8090
## curl: (97) No authentication method was acceptable.

验证成功或者无需验证后


图片.png

双向通道建立后,客户端已和服务器能正常的TCP操作,包括Http/Https/Websocket/自定义tcp信息,代理直到一方关闭则正常后续关闭。

错误处理方法

这里主要说明如何多协议兼容处理代理协议。以下定义的Continue协议包含了一个已读的字节表和当前的Tcp连接。

pub enum ProxyError {
    /// 该错误发生协议不可被解析,则尝试下一个协议
    Continue((Option<BinaryMut>, TcpStream)),
}

例如在http里协议解决头失败,

// 此处clone为浅拷贝,不确定是否一定能解析成功,不能影响偏移
match request.parse_buffer(&mut buffer.clone()) {
    Err(_) => {
        return Err(ProxyError::Continue((Some(buffer), inbound)));
    }
}

则返回当前已读的buffer和tcp连接,且游标为初始位置,buffer并未被读取过。下个解析器可以拿到完整的数据进行解析。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容