TLS/SSL VPN设计基础
- VPN
- TUN/TAP接口
- 路由
- PKI相关
- TLS/SSL加密
Tasks
task 1:设置虚拟机
VPN Client/Host U | 10.0.2.4 |
---|---|
Gateway | 10.0.2.5、192.168.60.1 |
Host V | 192.168.60.101 |
在建立VPN隧道之前,可以看到U是无法ping V的
task 2:通过TUN/TAP建立VPN隧道
step 1:运行VPN server
在Gateway上面按照步骤运行相关命令,之后可以看到对于tun0接口的配置如下,已经有了IP地址192.168.53.1
step 2:运行VPN client
和上面基本一样
step 3:在client和server端建立路由
配置好的路由表应该如下
- client端
- server端
step 4:设置V的路由
根据对于整个发包流程的认识,其实在这一步我们所需要添加的就是如果包的目的IP是192.168.53.0/24的主机,需要通过enp0s8
来发往192.168.60.1
,这样当Gateway
收到来自host V
的回复之后,会通过192.168.53.1
端口来进行转发数据包。
host V
路由表设置完之后如下
step 5:测试VPN
首先是terminal上面的显示
可以看到是能够ping通的,之后来分析wireshark
其中,ICMP类型的包不是隧道流量,而在10.0.2.4
和10.0.2.5
之间流动的是隧道流量
同理,telnet的命令也是如此
)
step 6:破洞实验
当我们在保持telnet连接的时候停止运行vpnclient,此时输入的命令不会显示出来,连接断开
但是当我们重新建立连接的时候,会显示之前输入的字符串
task 3:隧道加密
要保护隧道的完整性以及机密性,其中机密性是通过加密来实现的,完整性可以通过MAC来确保,参考:消息认证码MAC
思路
首先,直接用是不行的,因为server-key.pem
证书过期,但是又不知道cacert.pem
的密码,所以不能用原来的cacert.pem
来为服务端重新签名一个证书,所以只能是自己生成一个根证书,之后再用自己的根证书给服务器签名,在这里,我们将服务端的域名命名为jhlvpn.com
具体实现
在cert_server
文件夹中
- 首先生成自签名的根证书
cacert.pem
,运行命令
openssl req -new -x509 -keyout cakey.pem -out cacert.pem -config openssl.cnf -days 3650
之后需要设定文件密码,为123456
,然后填写相关内容
会生成cacert.pem
文件和cakey.pem
,即CA的证书文件和私钥
- 之后,服务器产生一对私钥,采用des3加密
openssl genrsa -des3 -out server-key.pem 1024
文件密码还是123456
- 然后根据证书生成证书请求文件
server-csr.pem
openssl req -new -key server-key.pem -out server-csr.pem -config openssl.cnf
- 生成服务端签名证书
openssl ca -in server-csr.pem -out server-cert.pem -cert cacert.pem -keyfile cakey.pem
-config openssl.cnf -days 3650
之后把
cacert.pem
文件复制到ca_client
文件夹下面之后生成散列值并且利用散列值创建符号链接
openssl x509 -in cacert.pem -noout -subject_hash
ln -s cacert.pem 3de75e64.0
之后运行程序,成功
- server端
- client端
同时,对wireshark抓包结果进行分析
首先,能明显看到TCP连接建立的握手过程(SYN、ACK包)、数据传输的过程以及断开连接的过程(FIN、ACK包),同时随便选择一个数据包,分析Data字段,可以看到是加密传输,不是明文传输
task 4:VPN服务器验证
思路
在建立VPN之前,要对VPN服务器进行验证,是通过使用公钥证书的方式来实现的
具体分为三部:1.首先要验证服务器证书有效 2.验证服务器是证书的所有者 3.验证服务器是目标服务器
具体实现
指出执行上述验证的代码行
首先,验证部分包括服务端发送服务器证书以及客户端对于证书的验证过程
server
// Step 0: OpenSSL library initialization
// This step is no longer needed as of version 1.1.0.
SSL_library_init();//进行协议初始化工作
SSL_load_error_strings();//加载错误信息
SSLeay_add_ssl_algorithms();//添加SSL加密算法
// Step 1: SSL context initialization
meth = (SSL_METHOD *)TLSv1_2_method();
ctx = SSL_CTX_new(meth);//创建会话环境
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);//指定握手阶段的证书验证方式,SSL_VERIFY_NONE表示完全忽略验证证书的结果
// Step 2: Set up the server certificate and private key
SSL_CTX_use_certificate_file(ctx, "./cert_server/server-cert.pem", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "./cert_server/server-key.pem", SSL_FILETYPE_PEM);
//加载服务端证书和私钥
// Step 3: Create a new SSL structure for a connection
ssl = SSL_new (ctx);
client
SSL* setupTLSClient(const char* hostname)
{
// Step 0: OpenSSL library initialization
// This step is no longer needed as of version 1.1.0.
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, NULL);//SSL_VERIFY_PEER表示希望验证对方证书
if(SSL_CTX_load_verify_locations(ctx,NULL, CA_DIR) < 1){//SSL_CTX_load_verify_locations为CA证书所在目录,这里实现了上面所说的步骤一对于服务器证书的验证:利用CA_DIR目录下面的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);//实现步骤三对于hostname的验证,检查服务器hostname
return ssl;
}
而第二步的检查在验证证书合法性的时候就已经验证了
task 5:VPN客户端验证
在这一个task当中,我们需要对VPN客户端进行验证,client会向server端发送username和password,之后server端通过shadow文件匹配来验证对方身份
思路
在server端加入对用户信息的请求,用户在终端上输入username以及password,参考文档的3.3节
具体实现
- 在server端添加
loginrequest()
函数,以及通过login()
来进行信息的验证
void loginRequest(SSL* ssl,int sock){
char buf[1024];
char username[1024];
char password[1024];
//将请求用户名的语句SSL_write到Client,从client端读取到的后面的输入字符串SSL_read到username
char* requsr = "Please enter username:";
SSL_write(ssl,requsr,strlen(requsr));
int usrlen = SSL_read(ssl,username,sizeof(username)-1);
username[usrlen] = '\0';
//将请求用户口令的语句SSL_write到Client,从client端读取到的输入字符串SSL_read到 password
char* reqpsd = "Please enter password:";
SSL_write(ssl,reqpsd,strlen(reqpsd));
int psdlen = SSL_read(ssl,password,sizeof(password)-1);
password[psdlen] = '\0';
login(username,password); //检查shadow文件中是否有该用户的信息
}
void login(char *user, char *passwd)
{
struct spwd *pw;
char *epasswd;
pw = getspnam(user);
if (pw == NULL) {
exit(0);
}
printf("Login name: %s\n", pw->sp_namp);
printf("Passwd : %s\n", pw->sp_pwdp);
epasswd = crypt(passwd, pw->sp_pwdp);
if (strcmp(epasswd, pw->sp_pwdp)) {
exit(0);
}
}
对main()
函数中只需要在处理client的GET请求的processRequest()
函数之前加上loginrequest()
即可
- client端在收到来自于server端的用户信息的请求之后,就直接
scanf()
输入即可,但是要求用户密码不可见,就用getpass()
函数即可
printf ("SSL connection using %s\n", SSL_get_cipher(ssl));
/*----------------Send username & password-------------*/
int len1;
char username[20];
char* password;
char usrbuf[1000];
char pwdbuf[1000];
len1 = SSL_read (ssl, usrbuf, sizeof(usrbuf)-1);//SSL_read来获取Server的请求用户名的信息
usrbuf[len1] = '\0';
printf("%s", usrbuf);
scanf("%s:",username);
SSL_write (ssl,username,strlen(username));//将username通过SSL_write到Server
len1 = SSL_read (ssl, pwdbuf, sizeof(pwdbuf)-1);//SSL_read到Server的请求用户口令的信息
pwdbuf[len1] = '\0';
printf("%s", pwdbuf);
password = getpass("");
SSL_write(ssl,password,strlen(password));//将password SSL_write到Server
/*----------------Send/Receive data --------------------*/
运行结果如下:
client端
server端
而当我们没有正确的输入用户名和密码的时候,用户端会直接退出
task 6:支持多进程
在之前的task当中,我们只是实现了数据的加密传输(SSL),但是还没有完全的实现VPN,因为没用通过使用TUN接口来构建VPN隧道,所以接下来我们实际要实现的就是在多进程的条件下来实现我们对于TUN接口的使用
- client端
int createTunDevice()
{//该函数的作用就是新建一个tun接口并且返回对应的文件描述符,无需修改,直接加到tlsclient即可
int tunfd;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
tunfd = open("/dev/net/tun", O_RDWR);
ioctl(tunfd, TUNSETIFF, &ifr);
return tunfd;
}
void tunSelected(int tunfd, SSL* ssl){
int len;
char buff[BUFF_SIZE];
printf("Got a packet from TUN\n");
bzero(buff, BUFF_SIZE);
len = read(tunfd, buff, BUFF_SIZE);//read函数将从tunfd当中的数据读取到buff当中
//sendto(sockfd, buff, len, 0, (struct sockaddr *) &peerAddr,sizeof(peerAddr));
SSL_write(ssl,buff,len);//写给ssl套接字
}
void socketSelected (int tunfd, SSL* ssl){
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);
len = SSL_read(ssl,buff,sizeof(buff)-1);
write(tunfd, buff, len);//写给tunfd来进行外层的解包
}
- server端,所需要的函数和上面基本一样,在main里面的修改如下
while(1){
int sock = accept(listen_sock, (struct sockaddr*)&sa_client, &client_len);
if (fork() == 0) { // The child process
close (listen_sock);
SSL_set_fd (ssl, sock);
int err = SSL_accept (ssl);
CHK_SSL(err);
printf ("SSL connection established!\n");
loginrequest(ssl, sock);
processRequest(ssl, sock);
while(1){
fd_set readFDSet;
FD_ZERO(&readFDSet);
FD_SET(sock, &readFDSet);
FD_SET(tunfd, &readFDSet);
select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL);
if (FD_ISSET(tunfd, &readFDSet)) tunSelected(tunfd, ssl);
if (FD_ISSET(sock, &readFDSet)) socketSelected(tunfd, ssl);
}
return 0;
} else { // The parent process
close(sock);
}
}
之后就按照task 2的一些步骤来弄就行
pipe实现
在pipe的是相当中,会区分父子进程,父进程负责将从tun接口收到的数据发给子进程,而子进程有两种情况要处理,第一种是将来自父进程的数据通过通过ssl/tls发给客户端进程,第二种是将来自于客户端的程序传递给tun接口
具体实现
void tunPipeSelected(int tunfd, int pipefd) {
//该函数的作用是实现父进程的作用,从tun接口通过read来把数据读取到pipe的输出端口上
int len;
char buff[BUFF_SIZE];
printf("Got a packet from TUN Interface\n");
bzero(buff, BUFF_SIZE);
len = read(tunfd, buff, BUFF_SIZE);
buff[len] = '\0';
write(pipefd, buff, len);
}
void pipeSelected(int pipefd, int sockfd, SSL *ssl) {
//该函数的作用是实现子进程从父进程通过pipe来得到的数据写入到ssl的socket上
int len;
char buff[BUFF_SIZE];
printf("Got a packet from TUN Interface\n");
bzero(buff, BUFF_SIZE);
len = read(pipefd, buff, BUFF_SIZE);
buff[len] = '\0';
SSL_write(ssl, buff, len);
}
void socketSelected(int pipefd, int sockfd, SSL *ssl, int tunfd) {
//该函数的作用是将来自客户端的程序发给了
int len;
char buff[BUFF_SIZE];
char *ptr = buff;
printf("Got a packet from the tunnel established \n");
bzero(buff, BUFF_SIZE);
len = SSL_read(ssl, buff, BUFF_SIZE);
buff[len] = '\0';
write(tunfd, buff, len);
}
对于main()
函数的修改
int main(){
SSL_METHOD *meth;
SSL_CTX* ctx;
SSL *ssl;
int err;
// Step 0: OpenSSL library initialization
// This step is no longer needed as of version 1.1.0.
SSL_library_init();
SSL_load_error_strings();
SSLeay_add_ssl_algorithms();
// Step 1: SSL context initialization
meth = (SSL_METHOD *)TLSv1_2_method();
ctx = SSL_CTX_new(meth);
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
// Step 2: Set up the server certificate and private key
SSL_CTX_use_certificate_file(ctx, "./cert_server/server-cert.pem",SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "./cert_server/server-key.pem",SSL_FILETYPE_PEM);
// Step 3: Create a new SSL structure for a connection
ssl = SSL_new (ctx);
struct sockaddr_in sa_client;
size_t client_len;
int tunfd = createTunDevice();
int listen_sock = setupTCPServer();
int fd[2];//pipe输入端和pipe输出端
pid_t pid;
pipe(fd);
pid = fork();//fork子进程,pid为子进程id
if(pid==-1){
perror("fork");
exit(1);
}
if(pid>0){
close(fd[0]);
while(1){
fd_set readFDSet;
FD_ZERO(&readFDSet);
FD_SET(tunfd, &readFDSet);
select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL);
if (FD_ISSET(tunfd, &readFDSet)) tunPipeSelected(tunfd, fd[1]);
}
exit(0);
}
else{
close(fd[1]);
while(1){
int sock = accept(listen_sock, (struct sockaddr*)&sa_client, &client_len);
if (fork() == 0) { // The child process
close (listen_sock);
SSL_set_fd (ssl, sock);
int err = SSL_accept (ssl);
CHK_SSL(err);
printf ("SSL connection established in child process!\n");
loginRequest(ssl,sock);
processRequest(ssl, sock);
while(1){
fd_set readFDSet;
FD_ZERO(&readFDSet);
FD_SET(sock, &readFDSet);
FD_SET(fd[0], &readFDSet);
select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL);
if (FD_ISSET(fd[0],&readFDSet)) pipeSelected(fd[0], sock, ssl);
if (FD_ISSET(sock, &readFDSet)) socketSelected(fd[0],sock,ssl,tunfd);
}
} else { // The parent process
close(sock);
}
}
}
}
并且此时并不需要在单线程的时候所写的socketSelecct()
和tunSelect()
,
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!