简介
- 这篇文章讲的是如何开发一套VPN, 不是如何使用某个 VPN.
- 一套, 包括Linux服务端, 安卓客户端, Windows客户端.
- VPN 一般用于实现访问局域网, 当然也可以有其他用途.
- VPN 协议特征过于明显, 很容易被识别并封禁, 本文主要是介绍原理, 有兴趣的同学可以基于此优化.
- 咦? 为什么会有人要识别和封禁 VPN 呢? 我也布吉岛…
1. VPN 的原理
以下会描述得比较通俗, 不太专业, 请专业人士见谅, 也欢迎您指正.
1.1 Linux 服务端
- 如果Linux有两张网卡, K1 和 K2, 其中 K1 可以上网, K2 不可以.
- 我们可以设置 Linux 使它将 K2 收到的流量转向 K1, 让 K1 帮把 K2 的流量转发出去, 等到数据回来到 K1 时, K1 再转给 K2.
- 这就是路由器的工作原理.
- 我们购买到的 VPS 一般只有一张上网的网卡, 这时, 我们需要创建一张虚拟网卡.
- VPN 服务程序只需要将从客户端收到的数据写入 K2, 再从 K2 读取回包, 再发回给客户端就行.
1.2 安卓客户端
- 安卓提供了一个方法, 可以新建一个虚拟网卡, 叫做 tun, 然后让所有 app 的网络连接都发到 tun.
- 然后让某个 app 可以对发到这张虚拟网卡的流量进行管理.
- 这个可以管理虚拟网卡流量的 app 就是 VPN 客户端 app.
- VPN 客户端 app 只需要将从 tun 读取到的数据发向服务端, 等服务端回包时, 再将数据写入 tun 就行.
1.3 Windows 客户端
- Windows 可以安装第三方虚拟网卡.
- 我们可以让程序修改路由表, 使得流量转向虚拟.
- 客户端程序对虚拟网卡进行监听, 读取住转向虚拟网卡的流量.
- 客户端程序只需要将从虚拟网卡读取到的数据发向服务端, 等服务端回包时, 再将数据写入虚拟网卡就行.
2. 服务端开发
2.1 Linux
以下命令均需要root权限执行.
一. 首先我们要设置 Linux 的 ip_forward. 使它能在不同网卡间转发流量.
# 编辑这个文件:
vim /etc/sysctl.conf
# 将文件里的这行取消注释
# net.ipv4.ip_forward = 1
# 然后执行这条命令:
sysctl -p
# 如果有看到 net.ipv4.ip_forward = 1 说明成功了.
二. 需要新建一个虚拟网卡, 并设置它的ip, 以及设置将其流量转发到真实网卡. 一般命名为 tun0, tun1, tun2…
ip tuntap add tun0 mode tun
ip link set dev tun0 up
ifconfig tun0 192.168.194.224 netmask 255.255.255.0 promisc
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# 第一步有可能遇到如下错误:
# Object "tuntap" is unknown, try "ip help".
# 解决方法: 使用tunctl添加tun
# Unbuntu:
apt-get install uml-utilities bridge-utils
# CentOS:
yum install tunctl brctl
# 执行下面这条命令后继续第二步
tunctl -n -t tun0 -u root
三. 关键代码 (使用udp进行服务端和客户端间的数据传输)
// 打开虚拟网卡 tun0, 获取文件描述符 tun_fd
int tun_fd;
char *clonedev = "/dev/net/tun";
if ((tun_fd = open(clonedev, O_RDWR)) < 0) {
printf("error1");
return -1;
}
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strncpy(ifr.ifr_name, "tun0", IFNAMSIZ);
int err;
if ((err = ioctl(tun_fd, TUNSETIFF, (void *) &ifr)) < 0) {
close(tun_fd);
return err;
}
uint8_t buf[MAX_PKG_LEN] = { 0 };
struct sockaddr_in* clent_addr;
// 将收到的数据写入 tun0
int n = recvfrom(udp_fd, buf, MAX_PKG_LEN, 0, (struct sockaddr*)clent_addr, &len);
write(tun_fd, buf, n);
// 读取 tun0 收到的回包并发回客户端
int n = read(tun_fd, buf, MAX_PKG_LEN);
sendto(udp_fd, buf, n, 0, clent_addr, sizeof(struct sockaddr_in));
四. 完整代码
代码完全使用 C 语言编程.
使用 epoll 进行文件描述符管理.
为了方便演示没有对数据进行加密, 在实际使用中必须加密!
3. 客户端开发
3.1 安卓
一. 关键代码:
//请求打开vpn
Intent intent = VpnService.prepare(MainActivity.this);
if (intent != null) {
startActivityForResult(intent, 0);
} else {
onActivityResult(0, RESULT_OK, null);
}
//用户同意后,开启vpn服务
@SuppressLint("ResourceAsColor")
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (result == RESULT_OK) {
Intent intent = new Intent(this, MyVpnService.class);
startService(intent);
}
}
import android.net.VpnService;
public class MyVpnService extends VpnService { ... }
static String SERVER_ADDR = "192.168.1.169";
static int SERVER_PORT = 7194;
static int MAX_BKG_LEN = 65535;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Builder builder = new Builder();
builder.setSession("MyVPNService");
builder.addAddress("192.168.194.1", 24); //这个ip要和服务器的虚拟网卡ip在同一个网段
builder.addDnsServer("8.8.8.8");
builder.addRoute("0.0.0.0", 0);
static ParcelFileDescriptor mInterface = builder.establish();
FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());
InetAddress serverAddr = InetAddress.getByName(SERVER_ADDR);
DatagramSocket sock = new DatagramSocket();
sock.setSoTimeout(0); //超时为无穷大
protect(sock); //保护这个连接的数据不会进入虚拟网卡
//启动两个线程一收一发
return START_STICKY
}
//发线程
@Override
public void run() {
int length;
byte[] ip_pkg = new byte[MAX_BKG_LEN];
while ((length = in.read(ip_pkg)) >= 0) {
if (length == 0) {
continue;
}
DatagramPacket msg = new DatagramPacket(
ip_pkg, length, serverAddr, SERVER_PORT);
sock.send(msg);
}
in.close();
}
//收线程
@Override
public void run() {
byte[] ip_buf = new byte[MAX_BKG_LEN];
while (true) {
DatagramPacket msg_r = new DatagramPacket(
ip_buf, MAX_BKG_LEN, serverAddr, SERVER_PORT);
sock.receive(msg_r);
int pkg_len = msg_r.getLength();
if (pkg_len == 0) {
continue;
} else if (pkg_len < 0) {
break;
}
out.write(ip_buf, 0, pkg_len);
}
out.close();
}
// 注意: 代码里省略了很多 try catch.
二. 完整代码
这是一个完整的安卓项目
评论 (0)