若要实现在 Linux 下的代理程序,方法有很多,比如看着 RFC 1928 来实现一个 socks5 代理并自行设置程序经过 socks5 代理等方式,下文是使用 Linux 提供的 tun/tap 设备来实现 UDP 代理隧道的大体思路和过程讲解。

TUN 设备

tun / tap 是由 Linux (可能还有其他 *NIX 系统提供支持)提供的,可以用来实现用户态的网络路由等处理的虚拟网络接口。也就是说,它们允许用户态的程序直接管理这个网络接口,而不是让内核协议栈来处理网络包。

那么很明显,如果我们要实现一个代理隧道程序,那么我们第一步就要解决包从哪里来的问题,这很好解决,我们知道我们可以通过路由表来指定包到底应该流向哪个网络设备,等数据包进入我们能够控制的网络接口后,我们自行处理网络包的转发就好了。而这里,tun / tap 就是我们所需要的,能够为所欲为的自行处理包状态的虚拟网络接口。

TUN 和 TAP 分别是虚拟的三层和二层网络设备,也就是说,我们可以从 TUN 拿到的就是 IP 层的网络数据包了,而 TAP 则是二层网络包,比如以太网包。因为我只打算对 IP 层的包进行处理(其实只打算处理 TCP 和 UDP),故接下来就只讨论 TUN 设备了。

要想使用 臀 TUN 设备,首先需要启用这个内核模块,在我的系统( Arch linux )上,只需 insmod 一下就行了。

find /lib/modules/ -iname 'tun.ko.gz' # 找到在哪儿
sudo insmod /lib/modules/4.12.12-1-ARCH/kernel/drivers/net/tun.ko.gz # 插入该模块
modprobe tun # 加载该内核模块,当然你也可以选择 the Windows way: 重启一下
modinfo tun # 可以检查一下了
lsmod | grep tun # 也可以这样检查

为了方便,我选择重启...

TUN 设备可以由程序创建和销毁(这种情况下即便程序没有主动的销毁创建的 TUN 设备,程序退出时 TUN 设备也会自行销毁),也可以使用 cli 工具创建和销毁,比如 ip tuntaptunctl ,或是 openvpn --mktun。在我们用程序实现之前,我们先使用 ip tuntap 来创建一个 TUN 设备来进行简易的测试。

简易测试

建立一个 tun 设备(网络接口),然后设置路由表把数据包路由到 tun 设备里。

sudo ip tuntap add dev dummytun mode tun # 添加一个叫 dummytun 的 tun 设备
sudo ip link set dummytun up # 把设备 dummytun 开起来
sudo route add 123.123.123.123 dummytun # 把 123.123.123.123 路由到 dummytun
ip route get 123.123.123.123 # 试试看是否搞成了

如上并没有给这个 tun 设备 IP (但 ioctl() 打开这个 tun 设备后就会有一个 IPv6 地址了),当然也可以给它一个 IP :

sudo ip addr add 192.168.61.0/24 dev dummytun

由于 tun 设备需要我们编写用户态的程序来操作数据包,所以需要写个东西来处理包数据的 IO 。tun 是三层设备,故能拿到的都是三层( IP 层)的包了。下面是一个非常简单的代码片段,仅仅简单的把东西从 dummytun 中读出来而已。

// 此处省略了 tun_open() 的原型。
// 作用仅仅是 `open()` 该设备,`ioctl()` 连接到该设备最终返回文件描述符
int fd = tun_open("dummytun");
// 我们假设是按照刚刚的步骤,在运行程序之前就已经创建并 `up` 了设备 dummytun
// 如果你希望通过代码创建 tun 设备,别忘了把创建的设备 `up` 起来
printf("Device dummytun opened\n"); while(1) {
int nbytes = read(fd, buf, sizeof(buf)); // FYI: char buf[1600];
printf("Read %d bytes from dummytun\n", nbytes);
}

另外额外需要注意的事是,在写过路由表规则以及给设备绑 IP 之后,最好还是刷一下缓存比较好,以免出现测了半天才发现压根没经过自己的 tun 设备的情况。

sudo ip route flush cache

当开始处理包时,我们就可以在 wireshark 或其他类似软件中看到流经该 tun 的网络包了。值得一提的是,即便事先没有给 tun 设备分配任何 IP 地址的情况下,在使用 ioctl() 打开 tun 设备后, tun 设备也将自动分得一个 IPv6 地址,所以在 wireshark 中是可以看到 ICMPv6 包存在的。

TUN 设备的使用

根据上面的简单测试,我们可以发现,实际上我们的程序本身就是完全接管所创建的虚拟网络设备的,我们程序所处于的职责就是,不断的 read 看看哪些网络包需要处理,然后程序进行处理并 write 就是了。

作为最简单的实践,我们可以写一个无脑的程序去伪装远程端响应我们网络设备中出现的 ICMP 包,首先本地拦截 123.123.123.123 到我们的 TUN 网络设备,然后我们在 ping 的时候,就可以从 TUN 中 read 到 ICMP 包了,那么接下来,我们只需要互换 IP 包的源地址和目的地址,并修改 ICMP 包中的标志位为 ECHO_REPLY ,(别忘了重算包的checksum)然后写回 TUN 设备,就可以让 ping 程序认为远程端服务器正确的做了响应了。

我在编写时使用了一个叫 libtins 的第三方库来做包的拼装和解析,下面则是对上面描述的步骤的一个简单的例子。

while(1) {
nbytes = read(fd, buf, sizeof(buf)); // 假设 fd 为所创建的 tun 设备的文件描述符
printf("Read %d bytes from dummytun\n", nbytes);
RawPDU p((uint8_t *)buf, nbytes);
try {
IP ip(p.to<IP>());
cout << "IP Packet: " << ip.src_addr() << " -> " << ip.dst_addr() << std::endl;
Tins::IPv4Address srcaddr = ip.src_addr();
ip.src_addr(ip.dst_addr());
ip.dst_addr(srcaddr);
ICMP &icmp = ip.rfind_pdu<ICMP>();
icmp.type(ICMP::ECHO_REPLY);
write(fd, ip.serialize().data(),ip.serialize().size()); // 其实不建议 serialize 调用两次,这里无所谓了
} catch (...) {
continue;
}
}

现在就可以看出我们的程序到底是干嘛的了吧?所以,当我们要实现隧道程序时,我们实际只是需要从我们创建的网络接口上读取数据包,并把数据包通过我们自己的方式发给代理隧道服务端,并等待代理隧道服务端的回复并写回我们创建的的网络接口就好了。也就是说,我们的客户端程序做的事情就是:

  • 从 TUN 设备读取数据包
  • 将数据包发送给代理隧道服务端(本篇所用的是 UDP 发送未做任何加密处理的数据包)
  • 读取代理隧道服务端发回的数据包
  • 把发回的数据包写回 TUN 设备

于是接下来我们可以试着把上面的程序改成两部分,客户端仅发送和接收,服务端则仅仅把源地址和目的地址交换并修改 ICMP 头的标志为 ECHO_REPLY。示例代码这里就不贴了,有兴趣的读者可以自己试着写一写,很简单的内容。

所以,服务端呢?

实际上,客户端的工作就是简单的监听 UDP socket 和 TUN 设备的文件描述符,然后读写就是了,那么服务端怎么把客户端发来的包写到网卡中,又怎么把远程端服务器返回的数据包捕获到程序中呢?

为了让我们的数据包发出去后远程端返回的数据包依然能回到我们的代理隧道服务端程序所处的服务器上,我们自然要对数据包进行一次 sNAT。而假如我们把源地址改成了服务器的 IP,返回的数据包就会进入服务器的默认网络接口。一旦数据包进入由内核控制的网络接口,内核协议栈就会处理 SYN 包并自动做出响应,如果我们隧道中的 TCP 数据包发出去,结果远程端服务器与我们的服务器建立的连接,这就很糟糕了,于是我们需要让我们的数据包不经过协议栈处理而由我们控制。

我们能控制什么?当然是 TUN 设备啦!还记得我们可以给 TUN 设备指定网络地址吗?我们可以在服务端也建立一个 TUN 设备,并指定一个内网网络地址(我假设指定的是192.168.61.123),我们把要发的数据包写入该 TUN ,数据包就发出去了。接下来呢?我们当然是写一条 iptables 规则来把数据包导到我们的 TUN 设备了。大概是这样的:

iptables -t nat -A POSTROUTING -s 192.168.61.0/24 -o eth0 -j MASQUERADE # 这样写
iptables -t nat -A POSTROUTING -s 192.168.61.0/24 -o eth0 -j SNAT --to-source xxx.xxx.xxx.xxx # 或这样,xx是服务器 IP

哦对了。别忘了打开服务器的路由转发功能:

echo "1" > /proc/sys/net/ipv4/ip_forward

这样的话,我们做 sNAT 的时候把源地址改为 tun 设备的地址就是了,而数据包回来时,网络数据包就会进入我们的能为所欲为控制的 TUN 设备啦!此时我们只需要做一次 dNAT 然后把数据包发回客户端,就大功告成了。

整个过程并不复杂,我的一份简单实现可以见 GitHub: BLumia/udptun 。上述内容可能有遗漏和错误,如果你发现了任何错误,都欢迎在下面评论指正,感激不尽!

使用 TUN 设备实现一个简单的 UDP 代理隧道的更多相关文章

  1. 一个简单的tcp代理实现

    There are a number of reasons to have a TCP proxy in your tool belt, bothfor forwarding traffic to b ...

  2. 一个简单JDK动态代理的实例

    动态代理的步骤: 创建一个实现了InvocationHandler接口的类,必须重写接口里的invoke()方法. 创建被代理的类和接口 通过Proxy的静态方法 newProxyInsatance( ...

  3. 一个简单 JDK 动态代理的实例

    动态代理的步骤: 创建一个实现了 InvocationHandler 接口的类,必须重写接口里的 invoke()方法. 创建被代理的类和接口 通过 Proxy 的静态方法 newProxyInsat ...

  4. 【实验 1-2】编写一个简单的 UDP 服务器和 UDPP 客户端程序。程序均为控制台程序窗口。

    1.服务器 #include<winsock2.h> //包含头文件#include<stdio.h>#include<windows.h>#pragma comm ...

  5. Go语言网络通信---一个简单的UDP编程

    Server端: package main import ( "fmt" "net" ) func main() { //创建udp地址 udpAddr, _ ...

  6. golang实现一个简单的http代理

    代理是网络中的一项重要的功能,其功能就是代理网络用户去取得网络信息.形象的说:它是网络信息的中转站,对于客户端来说,代理扮演的是服务器的角色,接收请求报文,返回响应报文:对于web服务器来说,代理扮演 ...

  7. python爬虫系列:做一个简单的动态代理池

    自动 1.设置动态的user agent import urllib.request as ure import urllib.parse as upa import random from bs4 ...

  8. 一个简单爬免费代理IP的脚本

  9. nginx一个简单的反向代理设置

    location /aaaaa/ { proxy_pass http://localhost:8080/aaaaa/; } 经过配置,现在访问 http://localhost/aaaaa/   就会 ...

随机推荐

  1. Java温故而知新-空心菱形

    空心菱形 今天做题练手,题目是空心菱形,一开始没什么思路,去网上找了找,发现很难找到讲的通透的,自己现在独立做出来了,记录一下,以备后用. * * * * * * * * * * * * * * * ...

  2. c#实现服务端webSocket

    现阶段socket通信使用TCP.UDP协议,其中TCP协议相对来说比较安全稳定!本文也是来讲解TCP为主(恕在下学艺不精). 下面是个人理解的tcp/ip进行通讯之间的三次握手! 1.客户端先发送报 ...

  3. js动态获取时间的方式

    列子的时间是这样实现的."2017/7/25 下午10:27:11 星期二" 列子中有一个div用来放时间. 每隔1s执行一次函数,秒就会变. function showTime( ...

  4. SVG裁切和蒙版

    前面的话 本文将详细介绍SVG裁切和蒙版 裁剪 SVG中的<clipPath>的元素,专门用来定义剪裁路径.必须设置的属性是id属性,被引用时使用 下面是一个圆形 <svg heig ...

  5. Linux下的I/O模型以及各自的优缺点

    其实关于这方面的知识,我阅读的是<UNIX网络编程:卷一>,书里是以UNIX为中心展开描述的,根据这部分知识,在网上参考了部分资料.以Linux为中心整理了这篇博客. Linux的I/O模 ...

  6. 【1414软工助教】团队作业10——复审与事后分析(Beta版本) 得分榜

    题目 团队作业10--复审与事后分析(Beta版本) 往期成绩 个人作业1:四则运算控制台 结对项目1:GUI 个人作业2:案例分析 结对项目2:单元测试 团队作业1:团队展示 团队作业2:需求分析& ...

  7. 【beta】阶段 第六次 Scrum Meeting

    每日任务 1.本次会议为第六次 Meeting会议: 2.本次会议在周六上午大课间,在陆大楼召开,召开本次会议为15分钟. 一.今日站立式会议照片 二.每个人的工作 (有work item 的ID) ...

  8. 通过修改my.ini配置文件来解决MySQL 5.6 内存占用过高的问题

    打开后台进程发现mysql占用的内存达到400+M. 修改一下my.ini这个配置文件的配置选项是可以限制MySQL5.6内存占用过高这一问题的,具体修改选项如下: performance_schem ...

  9. 团队作业4——第一次项目冲刺(Alpha版本)2nd day

    一.Daily Scrum Meeting照片 二.燃尽图 三.项目进展 界面 1.四个用户登录界面已经完成. 2.界面内的功能完成了一小部分. 登陆部分 1.QQ授权已经申请,还未通过. 2.通过好 ...

  10. 201521044091 《Java学习笔记》 第六周学习总结

    1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结.注1:关键词与内容不求多,但概念之间的联系要清晰,内容覆盖面 ...