IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

gen_tcp如何限制封包大小

系统技术非业余研究 2013-05-15 22:56:33 累计浏览 3,424 次
本机暂存

我们在做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错误!

对于第二种消息主动投递的情况:

从文档inet:setopts 可以看到:

{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, 需要程序来判定。

小结: 源码面前无秘密!

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. gen_tcp发送进程被挂起起因分析及对策 (累计阅读 37,821)
  2. TCP 的那些事儿(上) (累计阅读 22,696)
  3. 从输入 URL 到页面加载完成的过程中都发生了什么事情? (累计阅读 15,933)
  4. 自建DNS以防止GFW干扰 (累计阅读 13,125)
  5. 彻底屏蔽优酷广告 (累计阅读 12,975)
  6. 浅谈TCP优化 (累计阅读 11,081)
  7. 推荐一些socket工具,TCP、UDP调试、抓包工具 (累计阅读 10,843)
  8. SSL证书的分类(按功能) (累计阅读 10,271)
  9. 查看 Apache并发请求数及其TCP连接状态 (累计阅读 10,068)
  10. 推荐一些socket工具,TCP、UDP调试、抓包工具 (累计阅读 8,840)