vpn-ws一个开源的,承载协议使用Websocket的VPN实现。代码简单易读,其中对于tuntap/SSL/socket/event的C API使用,有三个平台的版本(Win、Linux和MacOSX1),具有参考价值。
来源:https://medium.com/@FWTO_O/vpn-ws%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-7f21809ceaa#.dtre36puj
参考
词汇
- peer,VPN中的节点,对于服务器而言包括自己的tuntap和每个客户端socket
- tuntap,Linux/Unix中创建的虚拟网卡,读取和写入数据和其它文件设备如socket、file差不多
实现
VPN的实现无非是客户端创建一个虚拟网卡tun/tap,从tun/tap中读取数据包,发送到服务端,服务端写入到它的tun/tap虚拟网卡中,从而让两端像是在同一个局域网内。发送数据无论是走UDP/TCP还是websocket,都只是为了转发虚拟网卡中接收到的数据包(以太包或者ip包)。
客户端与服务端的交互如下:
vpn-ws client <—HTTP/websocket—> Nginx <—uWSGI—> vpn-ws server
VPN-WS的客户端与Nginx Web交互,通过HTTP/HTTPS传送HTTP数据,而后Nginx通过uWSGI接口协议发送至应用服务即VPN-WS服务端。在完成了websocket的upgrade协商后,整个通路变成了websocket数据帧的转发通路。
用Nginx作前端的好处是,可以直接在现有的基于Nginx的服务上添加入口布置这个VPN服务器。客户端有重连机制,但是只有一条连接到服务端,如果拿来翻墙,可就太弱了。
服务端实现
创建tun/tap虚拟网卡
因为Linux视一切设备为File,所以其与file fd(文件句柄),与socket fd使用上没有区别。以此创建一个peer放到全局数组vpn_ws_conf.peers里。
tuntap_fd = vpn_ws_tuntap(vpn_ws_conf.tuntap_name); vpn_ws_peer_create(event_queue, tuntap_fd, vpn_ws_conf.tuntap_mac); void vpn_ws_peer_create(int queue, vpn_ws_fd client_fd, uint8_t *mac) { vpn_ws_event_add_read(queue, client_fd) vpn_ws_peer *peer = vpn_ws_calloc(sizeof(vpn_ws_peer)); peer->fd = client_fd; vpn_ws_conf.peers[client_fd] = peer; if (mac) { memcpy(peer->mac, mac, 6); peer->mac_collected = 1; //只有创建虚拟网卡时,peer的raw属性才置为1,这个值 //决定了websocket数据包是直接转发还是还原成原始数据包再转发 peer->handshake = 1; peer->raw = 1; }
创建服务端口,以接收客户端连接
server_fd = vpn_ws_bind(vpn_ws_conf.server_addr); vpn_ws_event_add_read(event_queue, server_fd);
接收新客户端连接并分配一个peer节点
为走web socket而来的客户端创建的peer,其raw属性为0,也就是说从这个peer读取出的数据包非原始包(而是websocket数据帧格式)。而handshake属性也为0,则需要与服务端作协议认证交互后才能让这个peer正常使用,即接收和转发数据包。
int ret = vpn_ws_event_wait(event_queue, events); for(int i=0;i<ret;i++) { int fd = vpn_ws_event_fd(events, i); if (fd == server_fd) { vpn_ws_peer_accept(event_queue, server_fd); continue; } if (vpn_ws_manage_fd(event_queue, fd)) break; }
服务端添加一个peer到peer数组里:
void vpn_ws_peer_accept(int queue, int fd) { int client_fd = accept(fd, (struct sockaddr *) &s_un, &s_len); vpn_ws_peer *peer = vpn_ws_calloc(sizeof(vpn_ws_peer)); peer->fd = client_fd; if (mac) { //只有虚拟网卡才会进这里 memcpy(peer->mac, mac, 6); peer->mac_collected = 1; peer->handshake = 1; peer->raw = 1; } vpn_ws_conf.peers[client_fd] = peer; }
处理每一个peer事件(发送数据或接收数据)
这里只解释读取的代码。读取部分,主要是做三件事:
1, 完成HTTP到websocket的协议升级协商。 2, 读取websocket的数据帧。忽略掉其中一些ping/pong等无用类型数据。并解出里面的以太网帧格式的数据包 3, 跟据包的目标MAC地址,进行数据包转发。转发的目标peer在已登录了的所有的peer中查找。
1, 如参考文档所示,The WebSocket Handshake是请求web服务进行HTTP->Websocket协议升级的过程(其实跟HTTP->HTTP2的升级协商几乎是一样的)。因为Nginx与server是通过uWSGI交互的,所以这里HTTP请求头通过uWSGI的api解释出来的。
请求头除了标准的升级要求的字段,还有自定义字段HTTP_X_VPN_WS_MAC/HTTP_X_VPN_WS_BRIDGE,用来传送客户端peer的MAC地址和是不是bridge的属性:
#define HTTP_RESPONSE "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " int64_t vpn_ws_handshake(int queue, vpn_ws_peer *peer) { ssize_t rlen = vpn_ws_uwsgi_parse(peer, &modifier1, &modifier2); char *ws_mac = vpn_ws_peer_get_var(peer, "HTTP_X_VPN_WS_MAC", 17, &ws_mac_len); char *ws_bridge = vpn_ws_peer_get_var(peer, "HTTP_X_VPN_WS_BRIDGE", 20, &ws_bridge_len); … int ret = vpn_ws_write( peer, http_response, sizeof(HTTP_RESPONSE)-1 + ws_accept_len + 4); }
2, 如果来源peer是raw的(即tuntap/本地虚拟网卡),直接将数据包读取出来就好了。如果这peer是raw的,那么就没有上面的第1步:
if (peer->raw) { data = peer->buf; data_len = peer->pos; mac = data; ws_ret = data_len; goto parsed; }
不然就是远端的peer,那么还要解码websocket的数据帧格式,得到原始的以太网数据包:
int64_t vpn_ws_websocket_parse(vpn_ws_peer *peer, uint16_t *ws_header) { uint8_t byte1 = peer->buf[0]; uint8_t opcode = byte1 & 0xf; uint64_t pktsize = byte2 & 0x7f; … switch(opcode) { case 0: case 1: case 2: return needed + pktsize; case 8: return -1; case 9: case 10: *ws_header = 0; return needed + pktsize; default: return -1; } }
这段代码主要是解释头部格式,找出Payload在websocket数据包中的范围。跟据RFC文档,websocket数据帧里的opcode取值如下,注意binary frame和ping/pong的处理就行了:
* %x0 denotes a continuation frame * %x1 denotes a text frame * %x2 denotes a binary frame * %x3-7 are reserved for further non-control frames * %x8 denotes a connection close * %x9 denotes a ping * %xA denotes a pong * %xB-F are reserved for further control frames
综上,第1和2步旨在解释出以太网数据包,代码简要如下:
int vpn_ws_manage_fd(int queue, vpn_ws_fd fd) { int ret = vpn_ws_read(peer, 8192); if (!peer->handshake) { int64_t hret = vpn_ws_handshake(queue, peer); } ws_ret = vpn_ws_websocket_parse(peer, &ws_header); uint8_t *ws = peer->buf + ws_header; uint64_t ws_len = ws_ret - ws_header; // 用以整个websocket包进行转发 data = peer->buf; data_len = ws_ret;
3, 转发非多播目标MAC地址的数据包
目标MAC地址是某个peer的MAC地址或者其bridge下的某个MAC:
if (b_peer->raw && !peer->raw) { //从远程节点的websocket中取数据包写入到虚拟网卡 wret = vpn_ws_write( b_peer, peer->buf+ws_header, ws_ret-ws_header); } else if (!b_peer->raw && peer->raw) { //从虚拟网卡写入到远程节点websocket wret = vpn_ws_write_websocket( b_peer, data, data_len); } else { //这里只可能有一种情况即 !b_peer->raw && !peer->raw成立 //也就是从远程节点转发到另一个远程节点,所以保持整个websocket包进行转发 wret = vpn_ws_write(b_peer, data, data_len); }
客户端实现
1, 创建tun/tap设备,
vpn_ws_fd tuntap_fd = vpn_ws_tuntap(vpn_ws_conf.tuntap_name);
2, 创建连接至Nginx Web端
int main(){ vpn_ws_fd tuntap_fd = vpn_ws_tuntap(vpn_ws_conf.tuntap_name); vpn_ws_nb(tuntap_fd); peer = vpn_ws_calloc(sizeof(vpn_ws_peer)); memcpy(peer->mac, vpn_ws_conf.tuntap_mac, 6); if (vpn_ws_connect(peer, vpn_ws_conf.server_addr)) { vpn_ws_client_destroy(peer); goto reconnect; } … }
发送HTTP请求进行websocket升级协商。如果使用HTTPS,那么在此前还有SSL的握手:
int vpn_ws_connect(vpn_ws_peer *peer, char *name) { if (!strncmp(cpy, "wss://", 6)) { ssl = 1; port = 443; } struct hostent *he = gethostbyname(domain); … if (connect(peer->fd, (struct sockaddr *) &sin, sizeof(struct sockaddr_in))) { vpn_ws_error("vpn_ws_connect()/connect()"); return -1; } int ret = snprintf(buf, 8192, "GET /%s HTTP/1.1\r\nHost: %s%s%s\r\n%sUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %.*s\r\nX-vpn-ws-MAC: %02x:%02x:%02x:%02x:%02x:%02x%s\r\n\r\n, … ) if (ssl) { vpn_ws_conf.ssl_ctx = vpn_ws_ssl_handshake( peer, domain, vpn_ws_conf.ssl_key, vpn_ws_conf.ssl_crt); if (!vpn_ws_conf.ssl_ctx) { return -1; } if (vpn_ws_ssl_write(vpn_ws_conf.ssl_ctx, (uint8_t *)buf, ret)) { return -1; } } …
等待websocket升级协商回应:
int http_code = vpn_ws_wait_101( peer->fd, vpn_ws_conf.ssl_ctx); if (http_code != 101) { vpn_ws_log("error, websocket handshake returned code: %d\n", http_code); return -1; }
3, 读事件的响应。
每个客户端要监听的是tuntap和peer到服务端的连接socket这两个fd,接收前者的以太网格式数据包封装成websocket包转发到后者,接收后者的websocket数据包解码成以太网数据包后转发到前者。
这里的17秒超时设置是为了超时后会发送一个ping包,即每17秒一个ping包的保证。ping包为\x89\x00,这里websocket的数据帧格式,表示FIN=1,opcode=9(PING类型),HAS_MASK=0,Payload length=0:
for(;;) { FD_ZERO(&rset); FD_SET(peer->fd, &rset); FD_SET(tuntap_fd, &rset); tv.tv_sec = 17; tv.tv_usec = 0; int ret = select(max_fd, &rset, NULL, NULL, &tv); if (ret == 0) { // 超时 if (vpn_ws_client_write(peer, (uint8_t *) "\x89\x00", 2)) { vpn_ws_client_destroy(peer); goto reconnect; } continue; } …
处理远端来的websocket数据包,即写入本地的tuntap设备:
if (FD_ISSET(peer->fd, &rset)) { if (vpn_ws_client_read(peer, 8192)) { vpn_ws_client_destroy(peer); goto reconnect; } int64_t rlen = vpn_ws_websocket_parse( peer, &ws_header); uint8_t *ws = peer->buf + ws_header; uint64_t ws_len = rlen - ws_header; if (peer->has_mask) { //XOR解密… } vpn_ws_full_write(tuntap_fd, ws, ws_len) }
转发来自tuntap的以太网数据包以websocket格式封装后转发到服务器:
if (FD_ISSET(tuntap_fd, &rset)) { vpn_ws_recv(tuntap_fd, mtu+8, 1500, rlen); … vpn_ws_client_write(peer, mtu, rlen + 8) }
代码不严谨/不妥的地方
C语言直是一门可怕的语言,拿C语言写出大工程更不容易。心疼C语言超弱的表达能力:字符串操作还要自己写,字符串拷贝还要自己写,还要小心什么时候应该释放掉。缺乏面向对象和结构体的权限限制。
本项目代码存在的一些问题:
- 低效的重分配内存。没有内存池复用,只使用了C的realloc,会有频繁的分配内存和内存拷贝(这里又没有对write_buf的收缩,网络状态不好时只能一直涨大下去)
uint64_t available = peer->write_len - peer->write_pos; if (available < amount) { peer->write_len += amount; void *tmp = realloc(peer->write_buf, peer->write_len);
- 可能会指针越界的字符串拷贝(几处地方,一下子找不到了)
- websocket的代码没有封装,客户端和服务端代码都把websocket的组包和解包的逻辑(尤其是XOR解密MASK的数据)全放入发包的地方,可读性差
- 没有客户端认证机制?看起来是是个客户端就能连上服务端
- 缺少合理的封装。比如MAC地址及相关方法。比如vpn_ws_peer这个结构太多成员而且很多不相关,可以封装更多子结构。
- 客户端与服务端的主循环代码太长,还使用了好多goto,每一个IO调用都要有不同返回值表示出了什么情况,也就是0,小于0,大于0的三种路经,整体逻辑复杂。
- Windows并未完全实现,不可用 ↩︎
没有评论:
发表评论