VPN
本文最后更新于:3 年前
操作系统大作业
一、基于UDP/TCP的VPN
实现原理
- tun 接口
- VPN搭建
- 隧道转发数据包
tun 设备
简介
tun(/tap) 是 Linux 内核 2.4.x 版本之后实现的虚拟网络设备,不同于物理网卡靠硬件网卡实现,tap/tun 虚拟网卡完全由软件来实现,功能和硬件实现完全没有差别,它们都属于网络设备,都可以配置 IP,都归 Linux 网络设备管理模块统一管理。
TUN 工作机制
TUN 设备是一种虚拟网络设备,通过此设备,程序可以方便得模拟网络行为。其工作方式如图
Linux Tun/Tap驱动程序为应用程序提供了两种交互方式:虚拟网络接口和字符设备/dev/net/tun。写入字符设备/dev/net/tun的数据会发送到虚拟网络接口中;发送到虚拟网络接口中的数据也会出现在该字符设备上。
应用程序可以通过标准的Socket API向Tun/Tap接口发送IP数据包,就好像对一个真实的网卡进行操作一样。除了应用程序以外,操作系统也会根据TCP/IP协议栈的处理向Tun/Tap接口发送IP数据包或者以太网数据包,例如ARP或者ICMP数据包。Tun/Tap驱动程序会将Tun/Tap接口收到的数据包原样写入到/dev/net/tun字符设备上,处理Tun/Tap数据的应用程序如VPN程序可以从该设备上读取到数据包,以进行相应处理。
应用程序也可以通过/dev/net/tun字符设备写入数据包,这种情况下该字符设备上写入的数据包会被发送到Tun/Tap虚拟接口上,进入操作系统的TCP/IP协议栈进行相应处理,就像从物理网卡进入操作系统的数据一样。
搭建 VPN
其工作流程为:
通过程序可以从/dev/net/tun
字符设备中读取(read)
或者写入(write)
数据,再通过将Tun结合物理网络设备使用,我们可以创建一个点对点的隧道。如下图所示,左边主机上应用程序发送到Tun虚拟设备上的IP数据包被VPN程序通过字符设备接收,然后再通过一个UDP隧道发送到右端的VPN服务器上,VPN服务器将隧道负载中的原始IP数据包写入字符设备,这些IP包就会出现在右侧的Tun虚拟设备上,最后通过操作系统协议栈和socket接口发送到右侧的应用程序上。
通过隧道发送/接收包
当隧道建立后,如何通过隧道发送/接收数据包是需要解决的问题
通过隧道发送
通过TUN接口获得一个IP包—>加密(或者验证)—>把它作为载荷发送到隧道另一端
通过隧道接收
通过隧道接收载荷—>解密并验证数据—>获得真实的包数据—>把包数据写到TUN接口。
如下图所示
监听socket和tun0网卡,然后转发数据包
每一个隧道应用都有两种接口: socket接口、TUN接口,两种接口都需要监听,需要在两种接口间转发数据。
程序实现
在编写程序之前需要做一些准备,程序流程图如下
从图中可知程序中主要包含四个部分
- 创建tun0网卡
- 客户端和服务器socket连接
- 转发来自tunnel和tun0数据
- 主程序,监听接口阻塞进程
vpnclient
和 vpnserver
程序是 VPN 隧道的两端。它们通过套接字使用 UDP 相互通信。客户端和服务器之间的虚线描绘了 VPN 隧道的路径。 VPN 客户端和服务器程序通过 TUN 接口连接到主机系统,通过它他们做两件事:
(1)从主机系统获取 IP 数据包,因此数据包可以通过隧道发送
(2)从隧道获取 IP 数据包,然后将其转发到托管系统,该系统会将数据包转发到其最终目的地。
创建tun网卡
在上图中可以看到客户端和服务器都需要一个tun网卡,所以需要在两台主机上都创建一个tun0网卡,从而形成tunnel
,实现通信
使用命令创建tun网卡
ip tuntap add dev tun0 mode tun
当上面的命令执行完再使用ifconfig -a
就可以看到刚刚创建的tun0
网卡,再使用如下的命令对其配置
ifconfig tun0 192.168.53.5/24 up
但是这里为了程序执行方便,直接在程序中创建虚拟网卡
int createTunDevice() { // 创建 tun0 网卡
int tunfd;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI; // tun设备不包含以太网头部
tunfd = open("/dev/net/tun", O_RDWR); // 打开文件
ioctl(tunfd, TUNSETIFF, &ifr); // 打开设备
return tunfd;
// 该函数执行完后 执行命令ifconfig tunX 192.168.53.5/24 up 为tun网卡设置IP并开启
}
socket连接
VPN server
UDP连接
int initUDPServer() {
int sockfd;
struct sockaddr_in server;
char buff[100];
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET; // 服务器地址类型为IPV4
server.sin_addr.s_addr = htonl(INADDR_ANY); //服务器IP
server.sin_port = htons(PORT_NUMBER); // 端口号
sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP的套接字
bind(sockfd, (struct sockaddr*) &server, sizeof(server)); // socket绑定地址和端口
// 等待连接
bzero(buff, 100); //清空缓冲区
int peerAddrLen = sizeof(struct sockaddr_in); //初始化结构体
int len = recvfrom(sockfd, buff, 100, 0,(struct sockaddr *) &peerAddr, &peerAddrLen); // 等待接收数据
printf("Connected with the client: %s\n", buff);
return sockfd;
}
VPN client
int connectToUDPServer(){ //创建socket连接 连接到vpn服务器
int sockfd;
char *hello="Hello";
//指定服务器的地址结构
memset(&peerAddr, 0, sizeof(peerAddr));
peerAddr.sin_family = AF_INET; // 服务器地址类型为IPV4
peerAddr.sin_port = htons(PORT_NUMBER);
peerAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto(sockfd, hello, strlen(hello), 0, // 连接服务器后发送一个消息 hello
(struct sockaddr *) &peerAddr, sizeof(peerAddr));
return sockfd;
}
TCP连接
VPN server
int initTCPServer()
{
struct sockaddr_in sa_server;
int listen_sock;
listen_sock= socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
CHK_ERR(listen_sock, "socket");
memset (&sa_server, '\0', sizeof(sa_server));
sa_server.sin_family = AF_INET;
sa_server.sin_addr.s_addr = INADDR_ANY;
sa_server.sin_port = htons (4433);
int err = bind(listen_sock, (struct sockaddr*)&sa_server, sizeof(sa_server));
CHK_ERR(err, "bind");
err = listen(listen_sock, 5);
CHK_ERR(err, "listen");
return listen_sock;
}
VPN client
int initTCPClient(const char* hostname, int port)
{
struct sockaddr_in server_addr;
struct hostent* hp = gethostbyname(hostname);
// 创建TCP socket
int sockfd= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 填写ip 端口 和协议信息
memset (&server_addr, '\0', sizeof(server_addr));
memcpy(&(server_addr.sin_addr.s_addr), hp->h_addr, hp->h_length);
server_addr.sin_port = htons (port);
server_addr.sin_family = AF_INET;
// 连接目的地址
connect(sockfd, (struct sockaddr*) &server_addr,
sizeof(server_addr));
return sockfd;
}
转发端口数据
客户端程序和服务器端程序都需要以下两个库函数
int sendto(int s, const void * msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);
函数说明:sendto() 用来将数据由指定的socket 传给对方主机. 参数s 为已建好连线的socket, 如果利用UDP协议则不需经过连线操作. 参数msg 指向欲连线的数据内容, 参数flags 一般设0, 参数to 用来指定欲传送的网络地址, 结构sockaddr 请参考bind(). 参数tolen 为sockaddr 的结果长度.
int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from,int *fromlen);
函数说明:recv()用来接收远程主机经指定的socket 传来的数据, 并把数据存到由参数buf 指向的内存空间, 参数len 为可接收数据的最大长度. 参数flags 一般设0, 参数from 用来指定欲传送的网络地址, 结构sockaddr 请参考bind(). 参数fromlen 为sockaddr 的结构长度.
从tun0接收数据转发到tunnel
void tunSelected(int tunfd, int sockfd){
int len;
char buff[BUFF_SIZE]; //定义接收数的缓冲区
printf("Got a packet from TUN\n");
bzero(buff, BUFF_SIZE); // 清空缓冲区
len = read(tunfd, buff, BUFF_SIZE); // 读取来自tun0网卡的数据,保存在缓冲区中
sendto(sockfd, buff, len, 0, (struct sockaddr *) &peerAddr, sizeof(peerAddr));
// 通过socket发送数据
}
从tunnel接收数据转发到tun0
void socketSelected (int tunfd, int sockfd){
int len;
char buff[BUFF_SIZE];
printf("Got a packet from the tunnel\n");
bzero(buff, BUFF_SIZE);
len = recvfrom(sockfd, buff, BUFF_SIZE, 0, NULL, NULL); // 接收数据至buff,保存数据的长度
write(tunfd, buff, len); // 通过write发送到tun0
}
主函数
主要作用,调用之前的函数,创建tun0网卡;建立基于udp的socket连接;循环接收数据
用到的库函数
int select(int n, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout);
函数说明:select()用来等待文件描述词状态的改变,会阻塞进程. 参数n 代表最大的文件描述词加1, 参数readfds、writefds 和exceptfds 称为描述词组, 是用来回传该描述词的读, 写或例外的状况. 底下的宏提供了处理这三种描述词组的方式:
FD_CLR(inr fd, fd_set* set); 用来清除描述词组set 中相关fd 的位
FD_ISSET(int fd, fd_set * set); 用来测试描述词组set 中相关fd 的位是否为真
FD_SET(int fd, fd_set* set); 用来设置描述词组set 中相关fd 的位
FD_ZERO(fd_set * set); 用来清除描述词组set 的全部位FD_ISSET
检测fd在fdset集合中的状态是否变化,当检测到fd状态发生变化时返回真,否则,返回假(也可以认为集合中指定的文件描述符是否可以读写)。
fd_set
可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,它用一位来表示一个fd(下面会仔细介绍)
int main (int argc, char * argv[]) {
int tunfd, sockfd;
tunfd = createTunDevice(); // 创建tun0网卡
sockfd = connectToUDPServer(); // 建立socket连接
// 进入主循环
while (1) {
fd_set readFDSet;
FD_ZERO(&readFDSet); // 将set清零使集合中不含任何fd
FD_SET(sockfd, &readFDSet); // 将socketfd加入set集合
FD_SET(tunfd, &readFDSet); // 将tunfd加入set集合
select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL); // 监听两个端口 阻塞进程,知道有一个端口收到数据
// 当收到数据程序继续执行 对数据进行转发
if (FD_ISSET(tunfd, &readFDSet)) // 当tun的文件fd可以读写
tunSelected(tunfd, sockfd);
if (FD_ISSET(sockfd, &readFDSet)) // 当socket的文件fd可以读写
socketSelected(tunfd, sockfd);
}
}
完整代码
VPN server
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <sys/ioctl.h>
#define PORT_NUMBER 55555
#define BUFF_SIZE 2000
struct sockaddr_in peerAddr;
int createTunDevice() { // 创建 tun0 网卡
int tunfd;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI; // tun设备不包含以太网头部
tunfd = open("/dev/net/tun", O_RDWR); // 打开文件
ioctl(tunfd, TUNSETIFF, &ifr); // 打开设备
return tunfd;
// 该函数执行完后 执行命令ifconfig tunX 192.168.53.5/24 up 为tun网卡设置IP并开启
}
int initUDPServer() {
int sockfd;
struct sockaddr_in server;
char buff[100];
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET; // 服务器地址类型为IPV4
server.sin_addr.s_addr = htonl(INADDR_ANY); //服务器IP
server.sin_port = htons(PORT_NUMBER); // 端口号
sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP的套接字
bind(sockfd, (struct sockaddr*) &server, sizeof(server)); // socket绑定地址和端口
// 等待连接
bzero(buff, 100); //清空缓冲区
int peerAddrLen = sizeof(struct sockaddr_in); //初始化结构体
int len = recvfrom(sockfd, buff, 100, 0,(struct sockaddr *) &peerAddr, &peerAddrLen); // 等待接收数据
printf("Connected with the client: %s\n", buff);
return sockfd;
}
void tunSelected(int tunfd, int sockfd){
int len;
char buff[BUFF_SIZE]; //定义接收数的缓冲区
printf("Got a packet from TUN\n");
bzero(buff, BUFF_SIZE); // 清空缓冲区
len = read(tunfd, buff, BUFF_SIZE); // 读取来自tun0网卡的数据,保存在缓冲区中
sendto(sockfd, buff, len, 0, (struct sockaddr *) &peerAddr, sizeof(peerAddr));
// 通过socket发送数据
}
void socketSelected (int tunfd, int sockfd){
int len;
char buff[BUFF_SIZE];
printf("Got a packet from the tunnel\n");
bzero(buff, BUFF_SIZE);
len = recvfrom(sockfd, buff, BUFF_SIZE, 0, NULL, NULL); // 接收数据至buff,保存数据的长度
write(tunfd, buff, len); // 通过write发送到tun0
}
int main (int argc, char * argv[]) {
int tunfd, sockfd;
tunfd = createTunDevice(); // 创建tun0网卡
sockfd = connectToUDPServer(); // 建立socket连接
// 进入主循环
while (1) {
fd_set readFDSet;
FD_ZERO(&readFDSet); // 将set清零使集合中不含任何fd
FD_SET(sockfd, &readFDSet); // 将socketfd加入set集合
FD_SET(tunfd, &readFDSet); // 将tunfd加入set集合
select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL); // 监听两个端口 阻塞进程,知道有一个端口收到数据
// 当收到数据程序继续执行 对数据进行转发
if (FD_ISSET(tunfd, &readFDSet)) // 当tun的文件fd可以读写
tunSelected(tunfd, sockfd);
if (FD_ISSET(sockfd, &readFDSet)) // 当socket的文件fd可以读写
socketSelected(tunfd, sockfd);
}
}
VPN client
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <sys/ioctl.h>
#define BUFF_SIZE 2000
#define PORT_NUMBER 55555
#define SERVER_IP "10.0.2.8" // VPN 服务器ip
struct sockaddr_in peerAddr;
int createTunDevice() { // 创建 tun0 网卡
int tunfd;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI; // tun设备不包含以太网头部
tunfd = open("/dev/net/tun", O_RDWR); // 打开文件
ioctl(tunfd, TUNSETIFF, &ifr); // 打开设备
return tunfd;
// 该函数执行完后 执行命令ifconfig tunX 192.168.53.5/24 up 为tun网卡设置IP并开启
}
int connectToUDPServer(){ //创建socket连接 连接到vpn服务器
int sockfd;
char *hello="Hello";
//指定服务器的地址结构
memset(&peerAddr, 0, sizeof(peerAddr));
peerAddr.sin_family = AF_INET; // 服务器地址类型为IPV4
peerAddr.sin_port = htons(PORT_NUMBER);
peerAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto(sockfd, hello, strlen(hello), 0, // 连接服务器后发送一个消息 hello
(struct sockaddr *) &peerAddr, sizeof(peerAddr));
return sockfd;
}
void tunSelected(int tunfd, int sockfd){
int len;
char buff[BUFF_SIZE]; //定义接收数的缓冲区
printf("Got a packet from TUN\n");
bzero(buff, BUFF_SIZE); // 清空缓冲区
len = read(tunfd, buff, BUFF_SIZE); // 读取来自tun0网卡的数据,保存在缓冲区中
sendto(sockfd, buff, len, 0, (struct sockaddr *) &peerAddr, sizeof(peerAddr));
// 通过socket发送数据
}
void socketSelected (int tunfd, int sockfd){
int len;
char buff[BUFF_SIZE];
printf("Got a packet from the tunnel\n");
bzero(buff, BUFF_SIZE);
len = recvfrom(sockfd, buff, BUFF_SIZE, 0, NULL, NULL); // 接收数据至buff,保存数据的长度
write(tunfd, buff, len); // 通过write发送到tun0
}
int main (int argc, char * argv[]) {
int tunfd, sockfd;
tunfd = createTunDevice(); // 创建tun0网卡
sockfd = connectToUDPServer(); // 建立socket连接
// 进入主循环
while (1) {
fd_set readFDSet;
FD_ZERO(&readFDSet); // 将set清零使集合中不含任何fd
FD_SET(sockfd, &readFDSet); // 将socketfd加入set集合
FD_SET(tunfd, &readFDSet); // 将tunfd加入set集合
select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL); // 监听两个端口 阻塞进程,知道有一个端口收到数据
// 当收到数据程序继续执行 对数据进行转发
if (FD_ISSET(tunfd, &readFDSet)) // 当tun的文件fd可以读写
tunSelected(tunfd, sockfd);
if (FD_ISSET(sockfd, &readFDSet)) // 当socket的文件fd可以读写
socketSelected(tunfd, sockfd);
}
}
实验环境
- 实验装置
- 拓扑图
- 主机物理网卡配置
实验装置
host u:ubuntu 18.04
VPNserver:ubuntu 18.04
host V:ubuntu 18.04
网络拓扑图
本次实验需要三台主机,分别作为host U(VPN 用户),VPN服务, host V(内网主机)。
host V处于192.168.60.0/24网段,属于内网。
host U处于10.0.2.0/24网段,属于外网,host V 和 host U在没有VPN的情况下无法通信。
VPN server有两张网卡,一张网卡的IP地址为192.168.60.1处于192.168.60.0/24网段,作为host V的网关,并可以与其通信;而另一张网卡地址为10.0.2.8属于外网网段,可以与host U通信,同时其作为host U的VPN服务器,实现与host V通信。
主机物理网卡配置
主机 | 角色 | IP地址 |
---|---|---|
客户机 | VPN客户机/host U | 10.0.2.7/24 |
服务器 | VPN服务器/网关 | 10.0.2.8/24 192.168.60.1/24 |
内网主机 | host V | 192.168.60.101/24 |
tun0 虚拟网卡配置
主机 | 虚拟接口 | IP地址 |
---|---|---|
服务器 | tun0 | 192.168.53.1/24 |
客户机 | tun0 | 192.168.53.5/24 |
环境搭建
运行了三台ubuntu的虚拟机,关系如上面描述
服务器主机
服务器主机需要两块网卡,这里通过VM再添加一块网卡即可,配置如图
运行vpnserver
程序,运行后会出现一个tun0
网卡
使用命令配置tun0
ifconfig tun0 192.168.53.1/24 up
由于 VPN Server 需要在私网和隧道之间转发数据包,因此它也需要充当网关,这是通过在 VPN Server 上启用 IP 转发来实现的。
sysctl net.ipv4.ip_forward=1
host v
客户端运行./vpnclient
,之后会出现一个tun0
接口,然后对其配置
ifconfig tun0 192.168.53.5/24 up
配置路由
经过以上两步,隧道就建立起来了,然后我们设置路由路径以将预期流量引导到客户端和服务器机器上的隧道。在 host v上,我们将所有进入专用网络 (192.168.60.0/24) 的数据包定向到 tun0 接口,数据包将从该接口通过 VPN 隧道转发。我们使用 route 命令在 host v 上添加以下路由条目:
route add -net 192.168.60.0/24 tun0
在服务器主机上,将流向 192.168.53.0/24 网络的流量定向到 tun0 接口
内网主机上为了通过 VPN 隧道将 Host V (内网主机)回复发送到 Host U,我们在 Host V 上添加了一个路由条目,它将去往 Host U 网络的数据包路由到 VPN Serve。从 VPN Server,这个数据包将通过 VPN 隧道到达 VPN Client,最终到达 Host U。 下面显示了 Host V 上的路由条目
route add -net 192.168.53.0/24 gw 192.168.60.1 ens33
验证通信
ping 测试
cliet ping 内网主机
内网主机ping client
内网主机追踪路由,可以看到经过网关192.168.60.1
到达了目的地址
查看vpnclient和vpnserver程序的运行情况
VPN client
在发送 ping 请求时,vpnclient反映已收到 TUN 接口上的数据包(ping 请求)并连续收到隧道中的包(ping 回复)
VPN server
在 vpnserver上,程序反映从隧道接收数据包(ping 请求),然后从 TUN 接收数据包(ping 回复)
ssh测试
在host V执行
ssh root@192.168.60.101
在 host U上执行
ssh root@10.0.2.7
通过上面的测试可以确定两台主机可以通过VPN程序进行远程控制。
二、TSL VPN
有了上面基于TCP的VPN,再做TSL就会简单很多。
程序实现
ssl初始化
创建SSL数据结构,用于建立TLS连接
VPN server
SSL* SSLLibInit() {
SSL_METHOD *meth;
SSL_CTX* ctx;
SSL *ssl;
int err;
// OpenSSL 初始化
SSL_library_init();
SSL_load_error_strings();
SSLeay_add_ssl_algorithms();
// SSL 初始化
meth = (SSL_METHOD *)TLSv1_2_method();
ctx = SSL_CTX_new(meth);
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
// 加载服务器证书和私钥文件
SSL_CTX_use_certificate_file(ctx, "./cert_server/server.pem", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "./cert_server/server-key.pem", SSL_FILETYPE_PEM);
ssl = SSL_new (ctx);
return ssl;
}
VPN client
SSL* setupTLSClient(const char* hostname)
{
// OpenSSL 初始化
SSL_library_init();
SSL_load_error_strings();
SSLeay_add_ssl_algorithms();
SSL_METHOD *meth;
SSL_CTX* ctx;
SSL* ssl;
meth = (SSL_METHOD *)TLSv1_2_method();
ctx = SSL_CTX_new(meth);
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback); //指明是否要求提供证书
if (SSL_CTX_load_verify_locations(ctx,NULL, CA_DIR) < 1) { //加载可信CA证书,进行证书验证
printf("Error setting the verify locations. \n");
exit(0);
}
ssl = SSL_new (ctx);
X509_VERIFY_PARAM *vpm = SSL_get0_param(ssl);
X509_VERIFY_PARAM_set1_host(vpm, hostname, 0);
return ssl;
}
TCP连接
TCP连接与上面的连接一样。
TLS握手
SSL_set_fd()将SSL绑定到一个TCP连接
调用SSL_connec()启动与服务器的TLS握手协议)
/*----------------TLS initialization ----------------*/
ssl = setupTLSClient(hostname);
printf("TLSClientsetup initialisation is successful\n");
/*----------------Create a TCP connection ---------------*/
int sockfd = setupTCPClient(hostname, port);
printf("TCPClientsetup is successful\n");
/*----------------TLS handshake ---------------------*/
SSL_set_fd(ssl, sockfd); //绑定TCP连接
printf("SSL_set_fd() is successful\n");
int err = SSL_connect(ssl); CHK_SSL(err);
printf("SSL connection is successful\n");
printf("SSL connection using %s\n", SSL_get_cipher(ssl));
数据转发
数据转发的原理与基于UDP/TCP的VPN 的原理
void processRequest(int tunfd, SSL* ssl, int sockfd)
{
while(1) {
fd_set readFDSet;
FD_ZERO(&readFDSet);
FD_SET(sockfd, &readFDSet);
FD_SET(tunfd, &readFDSet);
select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL);
if (FD_ISSET(tunfd, &readFDSet)) tunSelected(tunfd, sockfd, ssl);
if (FD_ISSET(sockfd, &readFDSet)) socketSelected(tunfd, sockfd, ssl);
}
}
实验环境
由于在上ubuntu 18.04上运行时,程序出现错误,所以使用了seedLab 提供的 ubuntu 16.04重新搭建了实验环境。各个网卡信息与上面的实验一样。
host U:ubuntu 16.04
VPNserver:ubuntu 16.04
host V:ubuntu 16.04
主机物理网卡配置
主机 | 角色 | IP地址 |
---|---|---|
客户机 | VPN客户机/host U | 10.0.2.7/24 |
服务器 | VPN服务器/网关 | 10.0.2.8/24 192.168.60.1/24 |
内网主机 | host V | 192.168.60.101/24 |
连通性测试
配置完各个网卡的IP地址后
host U 可以 ping 通 VPNserver (10.0.2.0/24)
VPN server 可以ping 通两台主机
host V可以ping 通VPNserver (192.168.60.0/24)
CA和证书设置
(1)、为 CA 生成一个自签名证书,它将作为根证书,如下所示:
openssl req -new -x509 -keyout ca.key -out ca.crt -config openssl.cnf
//文件 ca.key 包含 CA 的私钥,而 ca.crt 包含公钥证书。
(2)、接下来,我们使用以下命令为服务器创建一个 RSA 公私钥对
openssl genrsa -aes128 -out server.key 1024
(3)、创建一个包含服务器公钥的证书签名请求 (CSR)。 CSR 具有以下详细信息,服务器的通用名称为 sunzy.com
:
openssl req -new -key server.key -out server.csr -config openssl.cnf
(4)、然后将上述 CSR 发送到 CA 以生成密钥和通用名称的证书。
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
(5)、将服务器的证书和密钥存储为 pem 文件,这里是
cp server.crt server-cert.pem
cp server.key server-key.pem
VPN Server 程序使用这些文件来加载证书和私钥,如下所示
(6)、使用名称作为主题字段的哈希存储服务器的证书。这是因为在接收服务器的证书时,TLS 会根据颁发者的身份信息生成一个哈希值,并使用此哈希值在“./cert”文件夹中查找颁发者的证书,命令如下:(以下命令在ca_client 文件下执行)
openssl x509 -in ca.crt -noout -subject_hash
ln -s ca.crt b4386d70.0
建立TLS通信
VPN server
首先在VPN服务器上执行以下命令,将c程序编译为可执行程序
gcc -o vpnserver_tls vpnserver_tls.c -lssl -lcrypto -lcrypt
执行编译好的程序,此时该程序处于监听状态等待客户端的连接
此时该程序将创建一个tun0
虚拟网卡,使用以下命令配置网卡信息
sudo ifconfig tun0 192.168.53.1/24 up
配置完后的网卡信息如图:
编辑路由表并将设置为转发模式
sudo route add –net 192.168.53.0/24 tun0
sudo sysctl net.ipv4.ip_forward=1
VPN client
与服务器上一样,首先编译c程序
gcc -o vpnclient_tls vpnclient_tls.c -lssl -lcrypto
执行该程序(该域名与上面创建的证书中的通用名称一样)
sudo ./vpnclient_tls "sunzy.com" 4433
创建连接后,服务器端确认后,开始身份认证
服务器端收到的消息如下
此时TLS连接已经建立。
配置tun0和路由表
sudo ifconfig tun0 192.168.53.5/24 up
sudo route add -net 192.168.60.0/24
Host V
要实现内外网主机之间的通信,还需要在内网主机上添加路由表信息
sudo route add -net 192.168.53.0/24 gw 192.168.60.1 enp0s3
VPN server和VPN client 的连接信息
VPN server
VPN client
通信验证
ping 测试
- 客户端(10.0.2.7)ping 内网主机(192.168.60.101)
- 内网主机(192.168.60.101) ping 10.0.2.7
查看客户端上的 Wireshark 数据时,我们看到 ping 请求从 tun0 发送到内部网络 IP,并且此数据包从 VPN 客户端发送到 VPN tunnel
在服务器端,我们看到类似的流量——主机 U 和主机 V 之间的 ping 请求和回复通信,中间有 VPN 服务器。
telnet 测试
- 首先在在Host U上使用telnet控制Host V
- 在内网主机Host V 上telnet 客户端HostU
wireshark抓取的telnet的通信数据包
VPN server上的数据包信息
命令集合
VPN server
//编译程序
gcc -o vpnserver_tls vpnserver_tls.c -lssl -lcrypto -lcrypt
//启动程序
sudo ./vpnserver_tls 4433
//设置tun0信息 添加转发功能 添加路由
sudo ifconfig tun0 192.168.53.1/24 up
sudo sysctl net.ipv4.ip_forward=1
sudo route add -net 192.168.53.0/24 tun0
sudo route add -net 192.168.60.0/24 tun0
VPN client
//编译程序
gcc -o vpnclient_tls vpnclient_tls.c -lssl -lcrypto
//启动程序
sudo ./vpnclient_tls sunzy.com 4433
//设置tun0信息 添加路由
sudo ifconfig tun0 192.168.53.5/24 up
sudo route add -net 192.168.60.0/24
内网主机
route add -net 192.168.53.0/24 gw 192.168.60.1 enp0s3
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!