若要实现在 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 一下就行了。

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

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

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

简易测试

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

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

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

  1. sudo ip addr add 192.168.61.0/24 dev dummytun

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

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

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

  1. 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 的第三方库来做包的拼装和解析,下面则是对上面描述的步骤的一个简单的例子。

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

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

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

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

所以,服务端呢?

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

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

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

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

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

  1. 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. linux下权限问题思考

    今天遇到一些关于linux的权限问题,文件的所有者,文件的所属组等问题,文件对于所有者所属组是非常敏感的,同一个脚本所属者所属组不同,得到执行的结果也是差很多的.

  2. hdu5673 Robot 卡特兰数 / 默慈金数

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5673 分析: 这道题是一道裸的默慈金数,比较容易想到的是用卡特兰数来做.不了解的可以先学习一下. 卡特 ...

  3. 排序算法——选择排序(js语言实现)

    选择排序:顾名思义选择,选择排序属于O(N^2)的排序算法,意思就是选择数组中的最小的拿出来放在第一位,然后再剩下的数里面,选择最小的,排在第二位,以此类推. 例如:8  3  4  5  6  2  ...

  4. JVM内存结构和6大区域

    摘自 http://www.iteye.com/news/30350 对于我们一般理解的计算机内存,它算是CPU与计算机打交道最频繁的区域,所有数据都是先经过硬盘至内存,然后由CPU再从内存中获取数据 ...

  5. 可编辑的EditorGridPanel

    1.创建pannel是为可编辑的: new Ext.grid.EditorGridPanel 2.设置单击可以编辑属性: clickstoEdit: 1 3.在列设置添加文本编辑框 {header:& ...

  6. C++内存布局详解

    一个由C/C++编译的程序除了存放函数二进制代码的程序代码段(code段)外,数据占用的内存大致分为以下几个部分: 1.栈区(stack) 存放局部变量.函数参数.返回数据.返回地址等.系统自动分配释 ...

  7. 开源的API集成测试工具 v0.1.2 - 增强体验

    Hitchhiker 是一款开源的 Restful Api 集成测试工具,你可以在轻松部署到本地,和你的team成员一起管理Api. 详细介绍请看: http://www.cnblogs.com/br ...

  8. iOS block和代理的区别

      block和代理是iOS开发中实现回调的两种方式,大多数情况下是用哪个都可以,主要看个人喜好.本文主要是对两者做一下对比. 1.block简介   在 iOS中, block一共分三种.   (1 ...

  9. 开源项目Druid的提取SQL模板

    在数据库审计中,常常用到SQL模板,这样提取一次模板,下一次就不用对相同的模板的SQL进行相关操作.对此Druid提供相应的工具类进行SQL模板提取: package com.dbappsecurit ...

  10. Vue组件库的那些事儿,你都知道吗?

    前段时间一直在研究Vue组件库,终于在组内派上了用场.来给大家贡献一篇关于Vue组件库的相关知识.经验不多,如果有不合理的地方还请多多指出哦--- 回想一下,在你们公司或者你们小组是否有一个以上的项目 ...