PPTP隧道應用詳解
本文內容基於PPTPD應用層程式pptpd-1.4.0版本。初始化時在pptp_manager函式中,新建一個TCP的套介面監聽PPTP客戶端的控制連線請求,由函式createHostSocket完成,此套介面監聽在PPTP的協議埠1723(PPTP_PORT)上:
static int createHostSocket(int *hostSocket) { if ((*hostSocket = vrf_socket(vrf, AF_INET, SOCK_STREAM, 0)) <= 0) return -1; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PPTP_PORT); if (bind(*hostSocket, (struct sockaddr *) &address, sizeof(address)) < 0) return -3; }
客戶端的開始控制連線請求(Start-Control-Connection-Request)報文如下:
由於pptpd監聽在TCP的1723埠,故此資料包會由核心送到pptpd處理。pptpd服務讀取pptp_header大小的資料,用於判斷其型別是否為開始空氣連線請求START_CTRL_CONN_RQST(1),如果為真pptpd將fork一個新的程序pptpctrl來處理與客戶端的控制報文互動,pptpd繼續監聽新的客戶端連線:
pptpctrl程序的函式pptp_handle_ctrl_connection具體處理控制報文互動。函式read_pptp_packet和send_pptp_packet接收和傳送pptp報文。互動完成之後,客戶端將進行PPP的鏈路協商,由pppd程序處理。pptpctrl程序繼續處理控制鏈路的保活與拆除等工作。
PPP協商
PPTP協議使用增強的GRE通道傳輸PPP幀,IP頭部的協議欄位protocol為47表明為GRE協議,GRE頭部的協議型別為0x880b表明為PPP協議。pptpctrl程序在處理完客戶端的OUT_CALL_RQST(7)請求之後,建立一個RAW套介面(pptp_gre_init)監聽客戶端的GRE-PPP報文,同時建立pppd程序(startCall)處理後續的ppp資料報文。
static void pptp_handle_ctrl_connection(char **pppaddrs, struct in_addr *inetaddrs) { if (FD_ISSET(clientSocket, &fds)) { switch (read_pptp_packet(clientSocket, (unsigned char *) packet, rply_packet, &rply_size)) { case OUT_CALL_RQST: pty_fd = startCall(pppaddrs, inetaddrs); if (pty_fd > maxfd) maxfd = pty_fd; if ((gre_fd = pptp_gre_init(call_id_pair, pty_fd, inetaddrs)) > maxfd) maxfd = gre_fd; break; } } }
有一點需要注意的是,pptpd並沒有採用Linux核心的pptp驅動(AF_PPPOX套介面)處理資料包,而是使用通用的RAW套介面,參見pptp_gre_init程式碼。建立套介面時指定了GRE協議號PPTP_PROTO(47),並且將套介面關聯了對端的IP地址。
函式startCall建立pppd程序之前,建立了一對pty/tty主從裝置,pptpctrl程序關閉tty從裝置,保留主裝置pty。同時將tty從裝置複製為pppd程序的標準輸入和標準輸出,兩個程序即可通過這一對pty/tty裝置通訊了。
int pptp_gre_init(u_int32_t call_id_pair, int pty_fd, struct in_addr *inetaddrs)
{
gre_fd = vrf_socket(vrf, AF_INET, SOCK_RAW, PPTP_PROTO);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr = inetaddrs[0];
addr.sin_port = 0;
bind(gre_fd, (struct sockaddr *) &addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr = inetaddrs[1];
addr.sin_port = 0;
connect(gre_fd, (struct sockaddr *) &addr, sizeof(addr));
}
由RAW套介面接收到的GRE-PPP資料幀,pptpctrl程序呼叫decaps_gre函式,進行GRE相關校驗,去掉GRE頭部資料,其餘PPP協議資料進行HDLC編碼,由pty裝置送到pppd程序處理。與之相反,pppd處理完成回覆的資料,經tty從裝置發回pptpctrl程序,由decaps_hdlc函式去掉HDLC編碼,加上GRE頭部資訊,通過RAW套介面返回給客戶端。
核心RAW套介面
程序pptpctrl使用的RAW套介面,由引數協議族AF_INET、型別SOCK_RAW和協議號PPTP_PROTO(47)指定,核心inet_create函式負責建立此套介面。由以下inetsw_array中SOCK_RAW型別的定義可見,其支援的協議為IPPROTO_IP,並不是PPTP_PROTO。其實這裡的IPPROTO_IP是個通配型別,在inet_create函式中都能匹配上。
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
};
對於協議號PPTP_PROTO的處理,有兩個地方,其一賦值給了inet層套介面的成員inet_num;其二,由於inet_num的存在,將此套介面連結到了以inet_num的值為索引的RAW套介面HASH雜湊表中,參見函式raw_hash_sk。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
if (SOCK_RAW == sock->type) {
inet->inet_num = protocol;
}
if (inet->inet_num) {
err = sk->sk_prot->hash(sk);
}
}
int raw_hash_sk(struct sock *sk)
{
struct raw_hashinfo *h = sk->sk_prot->h.raw_hash;
head = &h->ht[inet_sk(sk)->inet_num & (RAW_HTABLE_SIZE - 1)];
sk_add_node(sk, head);
}
核心的RAW系統在接收到客戶端的GRE-PPP資料包時,通過資料包中的協議欄位protocol,在雜湊列表中查詢本機是否有註冊的套介面監聽此協議資料包。為真將資料包送往raw_v4_input函式處理。
int raw_local_deliver(struct sk_buff *skb, int protocol)
{
hash = protocol & (RAW_HTABLE_SIZE - 1);
raw_sk = sk_head(&raw_v4_hashinfo.ht[hash]);
if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash))
raw_sk = NULL;
}
raw_v4_input函式使用__raw_v4_lookup查詢具體的監聽套介面,pptpctrl建立的套介面,指定了協議號inet_num,和本機、對端的IP地址,只有符合這三個條件的資料包送往應用層pptpctrl處理。
struct sock *__raw_v4_lookup(struct net *net, struct sock *sk, unsigned short num, __be32 raddr, __be32 laddr, int dif, int sdif)
{
sk_for_each_from(sk) {
struct inet_sock *inet = inet_sk(sk);
if (net_eq(sock_net(sk), net) && inet->inet_num == num &&
!(inet->inet_daddr && inet->inet_daddr != raddr) &&
!(inet->inet_rcv_saddr && inet->inet_rcv_saddr != laddr) &&
!(sk->sk_bound_dev_if && sk->sk_bound_dev_if != dif &&
sk->sk_bound_dev_if != sdif))
goto found; /* gotcha */
}
sk = NULL;
found:
return sk;
}
PPTP控制報文流程
|----------------|
| |
IN ---->| KERNEL network |-----> PPTPD ---> pty |---->--->-- OUT
| | | |
|----------------| | |
| |
| |------------------|
|----------------| | | KERNEL NETWORK |
| KERNEL | | |------------------|
|-<--| PPP subsys |----<----------<---| |
| | | |
| |----------------| PPTPD
| |
|------------------->---- PPPD ------->-- tty ----->---|
完。