1. 程式人生 > >系統技術非業餘研究 » gen_tcp如何限制封包大小

系統技術非業餘研究 » gen_tcp如何限制封包大小

我們在做tcp伺服器的時候,通常會從安全考慮,限制封包的大小,預防被無端攻擊或者避免極端的請求對業務造成損害。
我們的tcp伺服器通常是erlang做的,那麼就涉及到gen_tcp如何限制封包的大小.

gen_tcp對封包的獲取有2種方式:
1. {active, false} 封包透過gen_tcp:recv(Socket, Length) -> {ok, Packet} | {error, Reason} 來接收。
2. {active, true} 封包以訊息方式投遞。

對於第一種方式:gen_tcp:recv(Socket, Length) 我們開看下程式碼實現:

/* inet_drv.c */
#define TCP_MAX_PACKET_SIZE 0x4000000  /* 64 M */

/* TCP requests from Erlang */
static ErlDrvSSizeT tcp_inet_ctl(ErlDrvData e, unsigned int cmd,
                                 char* buf, ErlDrvSizeT len,
                                 char** rbuf, ErlDrvSizeT rsize)
{
...
case TCP_REQ_RECV: {
        unsigned timeout;
        char tbuf[2];
        int n;

        DEBUGF(("tcp_inet_ctl(%ld): RECV\r\n", (long)desc->inet.port));
        /* INPUT: Timeout(4),  Length(4) */
        if (!IS_CONNECTED(INETP(desc))) {
            if (desc->tcp_add_flags & TCP_ADDF_DELAYED_CLOSE_RECV) {
                desc->tcp_add_flags &= ~(TCP_ADDF_DELAYED_CLOSE_RECV|
                                         TCP_ADDF_DELAYED_CLOSE_SEND);         
                return ctl_reply(INET_REP_ERROR, "closed", 6, rbuf, rsize);
            }
            return ctl_error(ENOTCONN, rbuf, rsize);
        }
        if (desc->inet.active || (len != 8))
            return ctl_error(EINVAL, rbuf, rsize);
        timeout = get_int32(buf);
        buf += 4;
        n = get_int32(buf);
        DEBUGF(("tcp_inet_ctl(%ld) timeout = %d, n = %d\r\n",
                (long)desc->inet.port,timeout,n));
        (long)desc->inet.port,timeout,n));
        if ((desc->inet.htype != TCP_PB_RAW) && (n != 0))
            return ctl_error(EINVAL, rbuf, rsize);
        if (n > TCP_MAX_PACKET_SIZE)
            return ctl_error(ENOMEM, rbuf, rsize);
        if (enq_async(INETP(desc), tbuf, TCP_REQ_RECV) < 0)
            return ctl_error(EALREADY, rbuf, rsize);
        if (INETP(desc)->is_ignored || tcp_recv(desc, n) == 0) {
...
}

邏輯上很簡單,如果封包的型別是TCP_PB_RAW,就需要顯式的指定長度,否則封包的長度是對端決定的,長度只能設定為0。然後就呼叫tcp_recv來非同步接收資料。在前面的 博文 裡面講過,tcp_recv資料的時候,需要分配接收緩衝區,緩衝區的大小正是n, 所以這裡做了個限定,不能超過TCP_MAX_PACKET_SIZE, 也就是說最大64M, 超過了會報ENOMEM錯誤!

對於第二種訊息主動投遞的情況:

{packet_size, Integer}(TCP/IP sockets)
Sets the max allowed length of the packet body. If the packet header indicates that the length of the packet is longer than the max allowed length, the packet is considered invalid. The same happens if the packet header is too big for the socket receive buffer.

For line oriented protocols (line,http*), option packet_size also guarantees that lines up to the indicated length are accepted and not considered invalid due to internal buffer limitations.

那這個限定如何發揮作用呢,同樣看程式碼:

/*inet_drv.c */
typedef struct {
...
    unsigned int psize;         /* max packet size (TCP only?) */
...
} inet_descriptor;

static int inet_set_opts(inet_descriptor* desc, char* ptr, int len)
{
...
        case INET_LOPT_PACKET_SIZE:
            DEBUGF(("inet_set_opts(%ld): s=%d, PACKET_SIZE=%d\r\n",
                    (long)desc->port, desc->s, ival));
            desc->psize = (unsigned int)ival;
            continue;
...
}

/* Allocate descriptor */
static ErlDrvData inet_start(ErlDrvPort port, int size, int protocol)
{
...
    desc->psize = 0;                   /* no size check *
...  
}

/* Copy a descriptor, by creating a new port with same settings                                                           
 * as the descriptor desc.                                                                                                
 * return NULL on error (SYSTEM_LIMIT no ports avail)                                                                     
 */
static tcp_descriptor* tcp_inet_copy(tcp_descriptor* desc,SOCKET s,
                                     ErlDrvTermData owner, int* err)
{
...
 copy_desc->inet.psize    = desc->inet.psize;
...
}

static int tcp_remain(tcp_descriptor* desc, int* len)
{
...
    DEBUGF(("tcp_remain(%ld): s=%d, n=%d, nfill=%d nsz=%d\r\n",
            (long)desc->inet.port, desc->inet.s, n, nfill, nsz));

    tlen = packet_get_length(desc->inet.htype, ptr, n,
                             desc->inet.psize, desc->i_bufsz,
                             &desc->http_state);
    if (tlen > 0) {
        if (tlen <= n) { /* got a packet */
            *len = tlen;
            DEBUGF((" => nothing remain packet=%d\r\n", tlen));
            return 0;
        }
        else { /* need known more */
            if (tcp_expand_buffer(desc, tlen) < 0)
                return -1;
            *len = tlen - n;
            DEBUGF((" => remain=%d\r\n", *len));
            return *len;
        }
    }    
    else if (tlen == 0) { /* need unknown more */
        *len = 0;
        if (nsz == 0) {
             if (nfill == n) {
                if (desc->inet.psize != 0 && desc->inet.psize > nfill) {
                    if (tcp_expand_buffer(desc, desc->inet.psize) < 0)
                        return -1;
                    return desc->inet.psize;
                }
                else
                    goto error;
            }
            DEBUGF((" => restart more=%d\r\n", nfill - n));
            return nfill - n;
        }
        else {
            DEBUGF((" => more=%d \r\n", nsz));
            return nsz;
        }
    }
error:
    DEBUGF((" => packet error\r\n"));
    return -1;
...
}

static int tcp_recv(tcp_descriptor* desc, int request_len)
{
...
else if (desc->i_remain == 0) {  /* poll remain from buffer data */
        if ((nread = tcp_remain(desc, &len)) < 0)
            return tcp_recv_error(desc, EMSGSIZE);
        else if (nread == 0)
            return tcp_deliver(desc, len);
        else if (len > 0)
            desc->i_remain = len;  /* set remain */
    }
...
}

/* packet_parser.c */

/* Return > 0 Total packet length.in bytes                                                                                
 *        = 0 Length unknown, need more data.                                                                             
 *        < 0 Error, invalid format.                                                                                      
 */
int packet_get_length(enum PacketParseType htype,
                      const char* ptr, unsigned n, /* Bytes read so far */
                      unsigned max_plen,     /* Max packet length, 0=no limit */
                      unsigned trunc_len,    /* Truncate (lines) if longer, 0=no limit */
                      int*     statep)       /* Protocol specific state */
{
...
    case TCP_PB_2:
        /* TCP_PB_2:    [L1,L0 | Data] */
        hlen = 2;
        if (n < hlen) goto more;
        plen = get_int16(ptr);
        goto remain;

    case TCP_PB_4:
        /* TCP_PB_4:    [L3,L2,L1,L0 | Data] */
        hlen = 4;
        if (n < hlen) goto more;
        plen = get_int32(ptr);
        goto remain;
...
case TCP_PB_LINE_LF: {
        /* TCP_PB_LINE_LF:  [Data ... \n]  */
        const char* ptr2;
        if ((ptr2 = memchr(ptr, '\n', n)) == NULL) {
            if (n > max_plen && max_plen != 0) { /* packet full */
                DEBUGF((" => packet full (no NL)=%d\r\n", n));
                goto error;
            }
            else if (n >= trunc_len && trunc_len!=0) { /* buffer full */
                DEBUGF((" => line buffer full (no NL)=%d\r\n", n));
                return trunc_len;
            }
            goto more;
        }
...

case TCP_PB_HTTPH:
    case TCP_PB_HTTPH_BIN:
        *statep = !0;
    case TCP_PB_HTTP:
    case TCP_PB_HTTP_BIN:
        /* TCP_PB_HTTP:  data \r\n(SP data\r\n)*  */
        plen = n;
        if (((plen == 1) && NL(ptr)) || ((plen == 2) && CRNL(ptr)))
            goto done;
        else {
            const char* ptr1 = ptr;
            int   len = plen;

            if (!max_plen) {
    /* This is for backward compatibility with old user of decode_packet                                      
                 * that might use option 'line_length' to limit accepted length of                                        
                 * http lines.                                                                                            
                 */
                max_plen = trunc_len;
            }

            while (1) {
                const char* ptr2 = memchr(ptr1, '\n', len);

                if (ptr2 == NULL) {
                    if (max_plen != 0) {
                        if (n >= max_plen) /* packet full */
                            goto error;
                    }
                    goto more;
                }
                else {
                    plen = (ptr2 - ptr) + 1;

                    if (*statep == 0) {
                        if (max_plen != 0 && plen > max_plen)
                            goto error;
                        goto done;
                    }
...
remain:
    {
        int tlen = hlen + plen;
        if ((max_plen != 0 && plen > max_plen)
            || tlen < (int)hlen) { /* wrap-around protection */
            return -1;
        }                
        return tlen;
    }
}

從程式碼我們可以看出在主動模式下包限制的幾點:
1. 預設情況下 psize為0, 代表不限制包長度。
2. psize是繼承的,也就是說accept出來的gen_tcp會繼承listen的那個gen_tcp的屬性。
3. 如文件所說,psize會限制 http/line類包的最大行的長度, 限制{packet, 1 | 2 | 4} 型別的包的長度。
4. 如果超過限制,返回的錯誤碼是EMSGSIZE.

所以總結起來就是packet_size用來限制包的大小,預設不限制, 在被動模式下除了主動模式的限制外還有最大64M的限制。
觸碰到限制後,返回的出錯碼是EMSGSIZE或者ENOMEM, 需要程式來判定。

小結: 原始碼面前無祕密!

祝玩得開心!

Post Footer automatically generated by wp-posturl plugin for wordpress.

相關推薦

系統技術業餘研究 » gen_tcp如何限制大小

我們在做tcp伺服器的時候,通常會從安全考慮,限制封包的大小,預防被無端攻擊或者避免極端的請求對業務造成損害。 我們的tcp伺服器通常是erlang做的,那麼就涉及到gen_tcp如何限制封包的大小. gen_tcp對封包的獲取有2種方式: 1. {active, false} 封包透過gen_

系統技術業餘研究 » gen_tcp呼叫程序收到{empty_out_q, Port}訊息奇怪行為分析

今天有同學在gmail裡面問了一個Erlang的問題,問題描述的非常好, 如下: 問題的背景是: 1、我開發了一個服務端程式,接收客戶端的連線。同一時刻會有多個客戶端來連線,連線後,接收客戶端請求後,再發送響應訊息,然後客戶端主動斷連。

系統技術業餘研究 » gen_tcp接受連結時enfile的問題分析及解決

最近我們為了安全方面的原因,在RDS伺服器上做了個代理程式把普通的MYSQL TCP連線變成了SSL連結,在測試的時候,皓庭同學發現Tsung發起了幾千個TCP連結後Erlang做的SSL PROXY老是報告gen_tcp:accept返回{error, enfile}錯誤。針對這個問題,我展開了

系統技術業餘研究 » gen_tcp:send的深度解刨和使用指南(初稿)

在大家的印象中, gen_tcp:send是個很樸素的函式, 一呼叫資料就喀嚓喀嚓到了對端. 這是個很大的誤解, Erlang的otp文件寫的很不清楚. 而且這個功能對於大部分的網路程式是至關重要的, 它的使用對否極大了影響了應用的效能. 我聽到很多同學在抱怨erlang的效能低或者出了很奇怪的問

系統技術業餘研究 » gen_tcp容易誤用的一點解釋

前天有同學在玩erlang gen_tcp的時候碰到了點小麻煩,描述如下: 比如說連線到baidu.com,發個http請求,然後馬上接收資料,發現接收出錯,wireshark抓包發現數據都有往返傳送,比較鬱悶。 我把問題演示下: $ erl Erlang R14B03 (erts-5.8

系統技術業餘研究 » gen_tcp傳送程序被掛起起因分析及對策

最近有同學在gmail上問關於gen_tcp傳送程序被掛起的問題,問題描述的非常好,見底下: 第一個問題是關於port_command和gen_tcp:send的。從專案上線至今,我在tcp傳送的地方遇到過兩次問題,都跟port_command有關係。 起初程式的效能不好,我從各方面嘗試分析和優化

系統技術業餘研究 » ulimit限制之nproc問題

前兩天微博上的@王關勝同學問了個問題: #ulimit問題# 關於nproc設定:centos6,核心版本是2.6.32. 預設情況下,ulimit -u的值為1024,是/etc/security/limits.d/90-nproc.conf的值限制;註釋掉這個限制後,值為95044;手工設定

系統技術業餘研究 » gen_tcp接收緩衝區易混淆概念糾正

Erlang的每個TCP網路連結是由相應的gen_tcp物件來表示的,說白了就是個port, 實現Erlang網路相關的邏輯,其實現程式碼位於erts/emulator/drivers/common/inet_drv.c 參照inet:setopts文件,它有三個buffer相關的選項,非常讓人

系統技術業餘研究 » gen_tcp連線半關閉問題

很久之前我發在javaeye論壇上,預防丟了抄過來: 原文:http://erlang.group.iteye.com/group/wiki/1422-gen_tcp-half-closed 當tcp對端呼叫shutdown(RD/WR) 時候, 宿主程序預設將收到{tcp_closed, Soc

系統技術業餘研究 » gen_tcp傳送緩衝區以及水位線問題分析

前段時間有同學在線上問了個問題: 伺服器端我是這樣設的:gen_tcp:listen(8000, [{active, false}, {recbuf,1}, {buffer,1}]). 客戶端是這樣設的:gen_tcp:connect(“localhost”, 8000, [{active, f

系統技術業餘研究 » 突破systemtap指令碼對資源使用的限制

我們在使用指令碼收集系統資訊的時候經常會用到map這樣的資料結構存放結果,但是stap指令碼在使用過程中經常會提升說”ERROR: Array overflow, check MAXMAPENTRIES near identifier ‘a’ at test.stp:6:5″ 類似這樣的資訊,然後

系統技術業餘研究 » 很容易忽略的ETS表個數限制問題

最近經常碰到ets表使用的數目超過系統的限制導致Erlang應用異常的案例。 比如說神鋒同學報告說在ssh模組裡面,最多隻能開啟500個左右連結,系統空閒的很,但是無法繼續加大連結。 浩庭同學報告說mnesia的事務只能開1千多,多了就上不去了。這些問題看起來沒有關聯。但是其實和ets都有很大的關

系統技術業餘研究 » dets在64位系統還是有可恥的2G限制

原本以為在 64位作業系統下 檔案的讀寫指標都改成64位的 避免了2G的限制。經過測試 disk_log和file都支援超過2G的資料檔案,但是 dets還是可恥的失敗了。 經過檢視原始碼 有2個問題: 1. key segment的問題。 dets在設計的時候 是針對32位的 那麼它的空間限

系統技術業餘研究 » Erlang叢集未公開特性:IP網段限制

Erlang叢集二個節點之間的通訊是通過一個tcp長連線進行的,而且是全聯通的,一旦cookie論證通過了,任何一個節點就獲得全叢集的訪問權,可以參考Erlang分佈的核心技術淺析 。erlang的這個授權模式特定搞的這麼簡單,但是在實際使用中還是有安全性的問題。我們退而求其次,來個IP網段限制,

系統技術業餘研究 » Erlang如何限制節點對叢集的訪問之net_kernel:allow

預設情況下Erlang的叢集訪問是全授權的,只要cookie認證過了後,新加入的節點可以訪問叢集裡面的任何機器,這給運維帶來很大風險。目前erlang有二種方法可以限制 1. IP網段限制,參看這裡 2. 節點名稱限制。這個是通過net_kernel:allow來實現的,參看: allow/1 L

系統技術業餘研究 » 未公開的gen_tcp:unrecv以及接收緩衝區行為分析

gen_tcp:unrecv是個未公開的函式,作用是往tcp的接收緩衝區裡面填入指定的資料。別看這小小的函式,用起來很舒服的。 我們先看下它的程式碼實現,Erlang程式碼部分: %%gen_tcp.erl:L299 unrecv(S, Data) when is_port(S) ->

系統技術業餘研究 » Erlang gen_tcp相關問題彙編索引

gen_tcp是erlang做網路應用最核心的一個模組,實踐中使用起來會有很多問題,我把團隊和我自己過去碰到的問題彙編下,方便大家對症下藥. 以下是gen_tcp,tcp,port相關的博文: 待續,歡迎補充! 祝玩得開心! Post Footer automatically generate

系統技術業餘研究

ItPub寫的文章“2017 年度 DB-Engines 資料庫冠軍得主:PostgreSQL 封王!”, 點選 這裡 進一步閱讀 升的最快的幾個資料庫,我簡單的無責任點評: PG資料庫是很老的資料庫,不過這幾年冉冉升起,因為是學院派的,有很好的學術和智力的支援,一直以來在資料庫的體系結構,程式碼

系統技術業餘研究 » MySQL資料庫架構的演化觀察

MySQL資料庫架構的演化觀察 December 14th, 2017 Categories: 資料庫 Tags: mysql

系統技術業餘研究 » inet_dist_connect_options

Erlang 17.5版本引入了inet_dist_{listen,connect}_options,對於結點間的互聯socket可以有更精細的控制,RPC的時候效能可以微調: raimo/inet_tcp_dist-priority-option/OTP-12476: Document ke