歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Unix知識 >> Unix基礎知識

UNIX網絡編程:UDP回射服務器程序(初級版本)及漏洞分析

該函數提供的是一個迭代服務器,而不是像TCP服務器那樣可以提供一個並發服務器。其中沒有對fork的調用,因此單個服務器進程就得處理所有客戶。一般來說,大多數TCP服務器是並發的,而大多數UDP服務器是迭代的。

對於本套接字,UDP層中隱含有排隊發生。事實上每個UDP套接字都有一個接收緩沖區,到達該套接字的每個數據報都進入這個套接字接收緩沖區。當進程調用recvfrom時,緩沖區中的下一個數據報以FIFO(先入先出)順序返回給進程。

服務器程序:

#include<stdio.h>  
#include<stdlib.h>  
#include<unistd.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
#include<string.h>  
      
#define SERV_PORT 3333  
#define MAXLINE 1024  
      
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)  
      
typedef struct sockaddr SA;  
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)  
{  
    int         n;  
    socklen_t   len;  
    char        mesg[MAXLINE];  
      
    for ( ; ; ) {  
        len = clilen;  
        n = recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);  
      
        sendto(sockfd, mesg, n, 0, pcliaddr, len);  
    }  
}  
      
      
int
main(int argc, char **argv)  
{  
    int                 sockfd;  
    struct sockaddr_in  servaddr, cliaddr;  
      
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);  
      
    bzero(&servaddr, sizeof(servaddr));  
    servaddr.sin_family      = AF_INET;  
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    servaddr.sin_port        = htons(SERV_PORT);  
      
    bind(sockfd, (SA *) &servaddr, sizeof(servaddr));  
      
    dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));  
}

查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/

客戶端程序:

#include <unistd.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <errno.h>  
#include <string.h>  
      
#define SERV_PORT 3333  
#define MAXLINE 1024  
#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)  
      
typedef struct sockaddr SA;  
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)  
{  
    int n;  
    char    sendline[MAXLINE], recvline[MAXLINE + 1];  
      
    while (fgets(sendline, MAXLINE, fp) != NULL) {  
      
        sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);  
      
        n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);  
      
        recvline[n] = 0;    /* null terminate */
        fputs(recvline, stdout);  
    }  
}  
      
int main(int argc, char **argv)  
{  
    int                 sockfd;  
    struct sockaddr_in  servaddr;  
      
    if (argc != 2)  
        ERR_EXIT("usage: udpcli <IPaddress>");  
      
    bzero(&servaddr, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(SERV_PORT);  
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);  
      
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);  
      
    dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));  
      
    exit(0);  
}

1.數據報的丟失

我們的UDP客戶/服務器例子是不可靠的。如果一個客戶數據報丟失(譬如說,被客戶主機與服務器主機之間的某個路由器丟失),客戶將永遠阻塞於dg_cli函數中的recvfrom調用,等待一個永遠不會達到的服務器應答。類似的,如果客戶數據報到達服務器,但是服務器的應答丟失了,客戶也將永遠阻塞於recvfrom調用。防止這樣永遠阻塞的一般方法是給客戶的recvfrom調用設置一個超時。

僅僅給recvfrom調用設置超時並不是完整的解決辦法。舉例來說,如果確實超時了,我們將無從判定超時原因是我們的數據報沒有到達服務器,還是服務器的應答沒有回到客戶。所以我們可以增加UDP客戶/服務器程序的可靠性。(後面會有講解)

2.服務器進程未運行

我們下一個要檢查的情形是在不啟動服務器的前提下啟動客戶。如果我們這麼做後在客戶上鍵入一行文本,那麼什麼也不發生。客戶永遠阻塞於它的recvfrom調用,等待一個永遠不出現的服務器應答。

經過抓包分析,服務器主機響應的是一個“port unreachable”(端口不可達)ICMP消息。不過這個ICMP錯誤不返回給客戶進程。我們稱這個ICMP錯誤為異步錯誤。該錯誤由sendto引起,但是sendto本身卻成功返回。我們知道從UDP輸出操作成功返回僅僅表示在輸出隊列中具有存放所形成IP數據報的空間。該ICMP錯誤直到後來才返回,這就是稱其為異步的原因。

一個基本規則是:對於一個UDP套接字,由它引發的異步錯誤卻並不返回給它,除非它已連接。僅在進程已將其UDP套接字連接到恰恰一個對端後,這些異步錯誤才返回給進程。

注:只要SO_BSDCOMPAT 套接字選項沒有開啟,linux甚至對未連接的套接字也返回大多數ICMP(目的地不可達)錯誤。

查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/

3.驗證接收到的響應

知道客戶臨時端口號的任何進程都可往客戶發送數據報,而且這些數據報會與正常的服務器應答混雜。

我們的解決辦法是修改recvfrom調用以返回數據報發送者的IP地址和端口號,保留來自數據報所發往服務器的應答,而忽略任何其他數據報。

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)  
{  
    int             n;  
    char            sendline[MAXLINE], recvline[MAXLINE + 1];  
    socklen_t       len;  
    struct sockaddr_in  *preply_addr;  
      
    preply_addr = malloc(servlen);  
      
    while (fgets(sendline, MAXLINE, fp) != NULL) {  
      
        sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);  
      
        len = servlen;  
        n = recvfrom(sockfd, recvline, MAXLINE, 0, (SA*)preply_addr, &len);  
        if (len != servlen || memcmp(pservaddr, (SA*)preply_addr, len) != 0) {  
            printf("reply from %s (ignored)\n",inet_ntoa(preply_addr->sin_addr));  
            continue;  
        }  
      
        recvline[n] = 0;    /* null terminate */
        fputs(recvline, stdout);  
    }  
}

然而這樣做照樣存在一些缺陷,如果服務器運行在一個只有單個IP地址的主機上,那麼這個版本的客戶工作正常。然而如果服務器主機是多宿的(多個IP地址),該客戶就有可能失敗。例如服務器有2個IP地址(172.24.37.94和135.197.17.100),客戶連接服務器(135.197.17.100),但是服務器響應的IP地址是(172.24.37.94),這樣我們的程序就出問題。

一個解決辦法是:得到由recvfrom返回的IP地址後,客戶通過在DNS中查找服務器主機的名字來驗證該主機的域名(而不是它的IP地址)。

另一個解決辦法是:UDP服務器給服務器主機上配置的每個IP地址創建一個套接字,用bind捆綁每個IP地址到各自的套接字,然後再所有這些套接字上使用select(等待其中任何一個變得可讀),再從可讀的套接字給出應答。既然用於給出應答的套接字上綁定的IP地址就是客戶請求的目的IP地址(否則該數據報不會被投遞到達該套接字),這就保證應答的源地址與請求的目的地址相同。

作者:csdn博客 ctthuangcheng

Copyright © Linux教程網 All Rights Reserved