我是靠谱客的博主 机智大白,最近开发中收集的这篇文章主要介绍NAT SIP helper,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

函数nf_nat_sip_init注册SIP协议的NAT helper,用于修改协议报文中的地址和端口信息。如下nf_nat_sip_hooks结构变量sip_hooks,赋值于nf_nat_sip_hooks,在SIP连接跟踪中使用。msg指针函数主要用于修改SIP消息中的字段,其它sdp开头的指针函数修改SDP消息中的字段。

static const struct nf_nat_sip_hooks sip_hooks = {
    .msg        = nf_nat_sip,
    .seq_adjust = nf_nat_sip_seq_adjust,
    .expect     = nf_nat_sip_expect,
    .sdp_addr   = nf_nat_sdp_addr,
    .sdp_port   = nf_nat_sdp_port,
    .sdp_session    = nf_nat_sdp_session,
    .sdp_media  = nf_nat_sdp_media,
};

static int __init nf_nat_sip_init(void)
{
    BUG_ON(nf_nat_sip_hooks != NULL);
    nf_nat_helper_register(&nat_helper_sip);
    RCU_INIT_POINTER(nf_nat_sip_hooks, &sip_hooks);
    nf_ct_helper_expectfn_register(&sip_nat);

另外,还注册了以下SIP协议的expectation处理函数。

static struct nf_ct_helper_expectfn sip_nat = {
    .name       = "sip",
    .expectfn   = nf_nat_sip_expected,
}; 

如下拓扑,测试SIP helper:

              |-------------------------|
              |                         |
              |           192.168.1.127 |-----|--- 192.168.1.109
 50.1.1.2 ----| 50.1.1.1                |     |
              |                         |     |
              |-------------------------|  192.168.1.126

配置如下,两个SIP客户端50.1.1.2和192.168.1.126,SIP服务器192.168.1.109,以下涉及到的报文都是在NAT网关上获得,测试由SIP客户端50.1.1.2发起,其账号为1001,呼叫1002,其为客户端192.168.1.126的账号:

# echo 1 >  /proc/sys/net/ipv4/ip_forward
#
# iptables -t nat -A POSTROUTING -s 50.1.1.2 -o enp6s0 -j SNAT --to-source 192.168.1.127
#
# iptables -A PREROUTING -t raw -p tcp --dport 5060 -j CT --helper sip
#
# modprobe nf_nat_sip sip_direct_media=0
#
# sysctl net.netfilter.nf_conntrack_helper=1

SIP信令处理

如下SIP信令的请求和响应部分开始内容:

INVITE sip:1001@192.168.1.109 SIP/2.0
Max-Forwards: 20
Via: SIP/2.0/UDP 50.1.1.2:55719;rport;branch=z9hG4bK1972895532

--------

SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.1.127:55719;received=192.168.1.127;rport=55719;branch=z9hG4bK1972895532

首先处理Request-Line字段,函数nf_nat_sip通过判断SIP信令的第一行Request-Line,确定是请求还是回复,对于请求信令,解析Request-Line。并且,尝试映射Request-Line中解析到的地址和端口,参见map_addr函数,如下为注册时建立的连接跟踪。

udp      17 3582 src=50.1.1.2 dst=192.168.1.109 sport=55719 dport=5060 src=192.168.1.109 dst=192.168.1.127 sport=5060 dport=55719 [ASSURED] mark=0 helper=sip use=1

其中对Request-Line的地址先进行判断,对于本示例,其为服务器地址192.168.1.109,端口号没有指定,使用默认的5060。其等于以上连接跟踪的目的地址和端口号,将其替换为连接跟踪反方向的源地址和源端口(192.168.1.109::5060),二者相同,并不执行地址替换。

static unsigned int nf_nat_sip(struct sk_buff *skb, unsigned int protoff,
                   unsigned int dataoff, const char **dptr, unsigned int *datalen)
{
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct);

    if (strncasecmp(*dptr, "SIP/2.0", strlen("SIP/2.0")) != 0) {
        if (ct_sip_parse_request(ct, *dptr, *datalen,
                     &matchoff, &matchlen, &addr, &port) > 0 &&
            !map_addr(skb, protoff, dataoff, dptr, datalen,
                  matchoff, matchlen, &addr, port)) {
            nf_ct_helper_log(skb, ct, "cannot mangle SIP message");
            return NF_DROP;
        }
        request = 1;
    } else
        request = 0;

接下来,解析Via信息(TCP或者UDP)字段,在具有多个Via字段的情况下这里仅处理第一个。对于请求信令,如果Via中地址不等于连接跟踪中记录的同方向的源地址,或者Via端口不等于源端口;或者,对于响应信令,Via中地址或端口不等于连接跟踪中保存的目的地址或者目的端口,表明当前报文不属于此连接,不进行处理。

    if (nf_ct_protonum(ct) == IPPROTO_TCP)
        hdr = SIP_HDR_VIA_TCP;
    else
        hdr = SIP_HDR_VIA_UDP;

    /* Translate topmost Via header and parameters */
    if (ct_sip_parse_header_uri(ct, *dptr, NULL, *datalen,
                    hdr, NULL, &matchoff, &matchlen, &addr, &port) > 0) {

        /* We're only interested in headers related to this connection */
        if (request) {
            if (!nf_inet_addr_cmp(&addr, &ct->tuplehash[dir].tuple.src.u3) ||
                port != ct->tuplehash[dir].tuple.src.u.udp.port)
                goto next;
        } else {
            if (!nf_inet_addr_cmp(&addr, &ct->tuplehash[dir].tuple.dst.u3) ||
                port != ct->tuplehash[dir].tuple.dst.u.udp.port)
                goto next;
        }

否则,尝试替换报文Via字段中的地址和端口,新的端口号由连接跟踪的反方向tuple中获得,参见函数map_addr。如下为内核创建的连接跟踪。

# conntrack -L 
udp      17 3582 src=50.1.1.2 dst=192.168.1.109 sport=55719 dport=5060 src=192.168.1.109 dst=192.168.1.127 sport=5060 dport=55719 [ASSURED] mark=0 helper=sip use=1

对于SIP请求,将Via替换为连接跟踪反方向的目的地址和端口,如上将Via中的50.1.1.2替换为192.168.1.127(NAT后地址);对于SIP响应,将Via替换为反方向的源地址和端口,即替换为50.1.1.2,用于还原NAT之前的地址。

        olen = *datalen;
        if (!map_addr(skb, protoff, dataoff, dptr, datalen,
                  matchoff, matchlen, &addr, port)) {
            nf_ct_helper_log(skb, ct, "cannot mangle Via header");
            return NF_DROP;
        }

接下来解析Via中的maddr字段,如果其中的地址与连接跟踪当前方向的源地址相等,并且不等于反方向的目的地址,表明源地址需要执行NAT变换,使用反方向的目的地址替换maddr字段中的地址。其中的原理与以上相同,参见map_addr函数。

        matchend = matchoff + matchlen + *datalen - olen;

        /* The maddr= parameter (RFC 2361) specifies where to send the reply. */
        if (ct_sip_parse_address_param(ct, *dptr, matchend, *datalen,
                           "maddr=", &poff, &plen, &addr, true) > 0 &&
            nf_inet_addr_cmp(&addr, &ct->tuplehash[dir].tuple.src.u3) &&
            !nf_inet_addr_cmp(&addr, &ct->tuplehash[!dir].tuple.dst.u3)) {

            buflen = sip_sprintf_addr(ct, buffer, &ct->tuplehash[!dir].tuple.dst.u3, true);
            if (!mangle_packet(skb, protoff, dataoff, dptr, datalen,
                       poff, plen, buffer, buflen)) {
                nf_ct_helper_log(skb, ct, "cannot mangle maddr");
                return NF_DROP;
            }
        }

接下来,解析Via中的received字段,服务器使用此字段表明自身接收到的报文的源地址。如果此地址等于连接跟踪当前方向上的目的地址(服务器->客户端,如果192.168.1.127),并且此地址不等于反方向(客户端->服务器)上的源地址(50.1.1.2),表明客户端位于NAT之后,其地址执行了NAT变换。

# conntrack -L 
udp      17 3582 src=50.1.1.2 dst=192.168.1.109 sport=55719 dport=5060 src=192.168.1.109 dst=192.168.1.127 sport=5060 dport=55719 [ASSURED] mark=0 helper=sip use=1

将received中地址还原为NAT之前的地址。

        /* The received= parameter (RFC 2361) contains the address
         * from which the server received the request. */
        if (ct_sip_parse_address_param(ct, *dptr, matchend, *datalen,
                           "received=", &poff, &plen, &addr, false) > 0 &&
            nf_inet_addr_cmp(&addr, &ct->tuplehash[dir].tuple.dst.u3) &&
            !nf_inet_addr_cmp(&addr, &ct->tuplehash[!dir].tuple.src.u3)) {
            buflen = sip_sprintf_addr(ct, buffer,
                    &ct->tuplehash[!dir].tuple.src.u3,
                    false);
            if (!mangle_packet(skb, protoff, dataoff, dptr, datalen,
                       poff, plen, buffer, buflen)) {
                nf_ct_helper_log(skb, ct, "cannot mangle received");
                return NF_DROP;
            }
        }

解析Via中rport字段,服务器使用此字段表明自身接收到的报文的源端口,如果此端口号等于连接跟踪当前方向上的目的端口(服务器->客户端,如下55719),并且此端口号不等于反方向(客户端->服务器)上的源端口(55719),表明客户端位于NAT之后,其端口执行了NAT变换。

# conntrack -L 
udp      17 3582 src=50.1.1.2 dst=192.168.1.109 sport=55719 dport=5060 src=192.168.1.109 dst=192.168.1.127 sport=5060 dport=55719 [ASSURED] mark=0 helper=sip use=1

将rport中端口号还原为NAT之前的端口号。注意,以本示例中,NAT变化了源地址,源端口没有改变,因此不需要以下的还原操作。

        /* The rport= parameter (RFC 3581) contains the port number
         * from which the server received the request. */
        if (ct_sip_parse_numerical_param(ct, *dptr, matchend, *datalen,
                         "rport=", &poff, &plen, &n) > 0 &&
            htons(n) == ct->tuplehash[dir].tuple.dst.u.udp.port &&
            htons(n) != ct->tuplehash[!dir].tuple.src.u.udp.port) {
            __be16 p = ct->tuplehash[!dir].tuple.src.u.udp.port;
            buflen = sprintf(buffer, "%u", ntohs(p));
            if (!mangle_packet(skb, protoff, dataoff, dptr, datalen,
                       poff, plen, buffer, buflen)) {
                nf_ct_helper_log(skb, ct, "cannot mangle rport");
                return NF_DROP;
            }
        }
    }

至此,Via字段处理完成。接下来,解析报文Contact字段。如下为网关接收到的INVITE请求和响应报文的部分字段:

INVITE sip:1001@192.168.1.109 SIP/2.0
Max-Forwards: 20
Via: SIP/2.0/UDP 50.1.1.2:55719;rport;branch=z9hG4bK1972895532
From: <sip:1002@192.168.1.109>;tag=82488285
To: <sip:1001@192.168.1.109>
Call-ID: 1612402514@192.168.1.109
CSeq: 10 INVITE
User-Agent: YATE/6.1.0
Contact: <sip:1002@50.1.1.2:55719>

SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.1.127:55719;received=192.168.1.127;rport=55719;branch=z9hG4bK1972895532
Record-Route: <sip:192.168.1.109;lr>
From: <sip:1002@192.168.1.109>;tag=82488285
To: <sip:1001@192.168.1.109>;tag=370640073
Call-ID: 1612402514@192.168.1.109
CSeq: 10 INVITE
Server: YATE/6.1.0
Contact: <sip:1001@192.168.1.126:65369>

其中可能包含多个地址,逐一进行解析,由函数map_addr尝试替换其中的地址和端口。INVITE请求中的Contact替换为了192.168.1.127:55719发送给了SIP服务器。INVITE响应报文中的Contact没有进行替换,因为其地址192.168.1.126(另外SIP客户端),并不等于连接跟踪中的任何地址。

next:
    /* Translate Contact headers */
    coff = 0;
    in_header = 0;
    while (ct_sip_parse_header_uri(ct, *dptr, &coff, *datalen,
                       SIP_HDR_CONTACT, &in_header,
                       &matchoff, &matchlen, &addr, &port) > 0) {
        if (!map_addr(skb, protoff, dataoff, dptr, datalen,
                  matchoff, matchlen, &addr, port)) {
            nf_ct_helper_log(skb, ct, "cannot mangle contact");
            return NF_DROP;
        }
    }

以下解析报文from和to字段,以下为INVITE请求和响应消息中的from和to字段示例:

INVITE sip:1001@192.168.1.109 SIP/2.0
Max-Forwards: 20
Via: SIP/2.0/UDP 50.1.1.2:55719;rport;branch=z9hG4bK1972895532
From: <sip:1002@192.168.1.109>;tag=82488285
To: <sip:1001@192.168.1.109>

SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.1.127:55719;received=192.168.1.127;rport=55719;branch=z9hG4bK1972895532
Record-Route: <sip:192.168.1.109;lr>
From: <sip:1002@192.168.1.109>;tag=82488285
To: <sip:1001@192.168.1.109>;tag=370640073

由函数map_sip_addr进行解析和地址替换,内部调用map_addr执行地址转换。对于INVITE请求,与Request-Line原理相同,由于其地址与连接跟踪反方向的源地址相同,替换之后还是原值。

# conntrack -L
udp      17 3582 src=50.1.1.2 dst=192.168.1.109 sport=55719 dport=5060 src=192.168.1.109 dst=192.168.1.127 sport=5060 dport=55719 [ASSURED] mark=0 helper=sip use=1

对于INVITE响应,FROM和TO中的地址与连接跟踪反方向的目的地址相同,替换之后还是原值。

    if (!map_sip_addr(skb, protoff, dataoff, dptr, datalen, SIP_HDR_FROM) ||
        !map_sip_addr(skb, protoff, dataoff, dptr, datalen, SIP_HDR_TO)) {
        nf_ct_helper_log(skb, ct, "cannot mangle SIP from/to");
        return NF_DROP;
    }

对于Cisco的一些IP电话,其在端口forced_dport监听响应报文,forced_dport在连接跟踪中解析Via字段中的端口得到,此处,将报文的目的端口修改为forced_dport。

    /* Mangle destination port for Cisco phones, then fix up checksums */
    if (dir == IP_CT_DIR_REPLY && ct_sip_info->forced_dport) {
        struct udphdr *uh;

        if (skb_ensure_writable(skb, skb->len)) {
            nf_ct_helper_log(skb, ct, "cannot mangle packet");
            return NF_DROP;
        }

        uh = (void *)skb->data + protoff;
        uh->dest = ct_sip_info->forced_dport;

        if (!nf_nat_mangle_udp_packet(skb, ct, ctinfo, protoff, 0, 0, NULL, 0)) {
            nf_ct_helper_log(skb, ct, "cannot mangle packet");
            return NF_DROP;
        }
    }

    return NF_ACCEPT;

SIP信令地址替换

函数map_sip_addr用于解析报文中的from和to头部,由函数map_addr尝试替换其中的地址和端口。

static int map_sip_addr(struct sk_buff *skb, unsigned int protoff,
            unsigned int dataoff, const char **dptr,
            unsigned int *datalen, enum sip_header_types type)
{
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);

    if (ct_sip_parse_header_uri(ct, *dptr, NULL, *datalen, type, NULL,
                    &matchoff, &matchlen, &addr, &port) <= 0)
        return 1;
    return map_addr(skb, protoff, dataoff, dptr, datalen,
            matchoff, matchlen, &addr, port);

如下核心的地址端口映射函数map_addr,如果地址和端口分别与连接跟踪本方向的源地址和源端口相等,替换为反方向的目的地址和目的端口,这样如果此方向执行了SNAT,报文中的字段也执行相应的地址替换。反之,如果地址和端口分别与连接跟踪本方向的目的地址和目的端口相等,还原为反方向的源地址和源端口,即还原为NAT之前的地址和端口。

如果新地址和新端口与旧地址和端口相等,直接返回。否则,替换报文中的相应内容。

另外,对于SIP注册报文,由于在函数nf_nat_sip_expect中已经修改Contact中地址和端口,所以,对于Contact字段中的地址和端口,不等于连接跟踪的源地址和端口,更不等于连接跟踪的目的地址和端口,返回1。

static int map_addr(struct sk_buff *skb, unsigned int protoff,
            unsigned int dataoff,
            const char **dptr, unsigned int *datalen,
            unsigned int matchoff, unsigned int matchlen,
            union nf_inet_addr *addr, __be16 port)
{
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct);

    if (nf_inet_addr_cmp(&ct->tuplehash[dir].tuple.src.u3, addr) &&
        ct->tuplehash[dir].tuple.src.u.udp.port == port) {
        newaddr = ct->tuplehash[!dir].tuple.dst.u3;
        newport = ct->tuplehash[!dir].tuple.dst.u.udp.port;
    } else if (nf_inet_addr_cmp(&ct->tuplehash[dir].tuple.dst.u3, addr) &&
           ct->tuplehash[dir].tuple.dst.u.udp.port == port) {
        newaddr = ct->tuplehash[!dir].tuple.src.u3;
        newport = ct_sip_info->forced_dport ? : ct->tuplehash[!dir].tuple.src.u.udp.port;
    } else
        return 1;

    if (nf_inet_addr_cmp(&newaddr, addr) && newport == port)
        return 1;

    buflen = sip_sprintf_addr_port(ct, buffer, &newaddr, ntohs(newport));
    return mangle_packet(skb, protoff, dataoff, dptr, datalen,
                 matchoff, matchlen, buffer, buflen);

SDP媒体消息

对于SDP消息,需要替换其中的Owner/连接/媒体的地址和端口号,如下为INVITE消息示例,必要时更新Content-Length,其表示SDP消息的长度。

INVITE sip:1001@192.168.1.109 SIP/2.0
Max-Forwards: 20
Via: SIP/2.0/UDP 50.1.1.2:55719;rport;branch=z9hG4bK1972895532
From: <sip:1002@192.168.1.109>;tag=82488285
To: <sip:1001@192.168.1.109>
Call-ID: 1612402514@192.168.1.109
CSeq: 10 INVITE
User-Agent: YATE/6.1.0
Contact: <sip:1002@50.1.1.2:55719>
Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO
Content-Type: application/sdp
Content-Length: 496

v=0
o=yate 1643511407 1643511407 IN IP4 50.1.1.2
s=SIP Call
c=IN IP4 50.1.1.2
t=0 0
m=audio 21254 RTP/AVP 0 8 3 11 98 97 102 103 104 105 106 101

函数nf_nat_sdp_addr,用于修改媒体(m=)中包含的连接信息(以上示例m=中不包含连接信息),此函数不处理全局的连接信息(以上示例使用的为全局c=信息)。参数type为SDP_HDR_CONNECTION,term为SDP_HDR_MEDIA,参数addr为转换之后的新地址。函数mangle_sdp_packet将其转换为字符串替换报文中的旧地址字符串。

另外,以上替换可能修改SDP报文的长度,需要更新SIP消息中的Content-Length字段。

static unsigned int nf_nat_sdp_addr(struct sk_buff *skb, unsigned int protoff,
                    unsigned int dataoff,
                    const char **dptr, unsigned int *datalen,
                    unsigned int sdpoff,
                    enum sdp_header_types type,
                    enum sdp_header_types term,
                    const union nf_inet_addr *addr)
{
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    char buffer[INET6_ADDRSTRLEN];

    buflen = sip_sprintf_addr(ct, buffer, addr, false);
    if (mangle_sdp_packet(skb, protoff, dataoff, dptr, datalen,
                  sdpoff, type, term, buffer, buflen))
        return 0;

    return mangle_content_len(skb, protoff, dataoff, dptr, datalen);

查找报文中的SDP_HDR_CONNECTION字段,将其替换。

static int mangle_sdp_packet(struct sk_buff *skb, unsigned int protoff,
                 unsigned int dataoff,
                 const char **dptr, unsigned int *datalen,
                 unsigned int sdpoff,
                 enum sdp_header_types type,
                 enum sdp_header_types term,
                 char *buffer, int buflen)
{            
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    unsigned int matchlen, matchoff;

    if (ct_sip_get_sdp_header(ct, *dptr, sdpoff, *datalen, type, term,
                  &matchoff, &matchlen) <= 0)
        return -ENOENT; 
    return mangle_packet(skb, protoff, dataoff, dptr, datalen,
                 matchoff, matchlen, buffer, buflen) ? 0 : -EINVAL;   

函数nf_nat_sdp_port用于修改报文中的媒体端口(m=),参数port为新的端口号。

static unsigned int nf_nat_sdp_port(struct sk_buff *skb, unsigned int protoff,
                    unsigned int dataoff,
                    const char **dptr, unsigned int *datalen,
                    unsigned int matchoff, unsigned int matchlen,
                    u_int16_t port)
{
    buflen = sprintf(buffer, "%u", port);
    if (!mangle_packet(skb, protoff, dataoff, dptr, datalen,
               matchoff, matchlen, buffer, buflen))
        return 0;

    return mangle_content_len(skb, protoff, dataoff, dptr, datalen);

函数nf_nat_sdp_session用于修改报文中的SDP_HDR_OWNER和SDP_HDR_CONNECTION字段(全局的连接信息字段,非媒体m=字段中的连接信息)。参数addr为新的地址。

static unsigned int nf_nat_sdp_session(struct sk_buff *skb, unsigned int protoff,
                       unsigned int dataoff, const char **dptr, unsigned int *datalen,
                       unsigned int sdpoff, const union nf_inet_addr *addr)
{
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);

    /* Mangle session description owner and contact addresses */
    buflen = sip_sprintf_addr(ct, buffer, addr, false);
    if (mangle_sdp_packet(skb, protoff, dataoff, dptr, datalen, sdpoff,
                  SDP_HDR_OWNER, SDP_HDR_MEDIA, buffer, buflen))
        return 0;

    switch (mangle_sdp_packet(skb, protoff, dataoff, dptr, datalen, sdpoff,
                  SDP_HDR_CONNECTION, SDP_HDR_MEDIA, buffer, buflen)) {
    case 0:
    /*
     * RFC 2327:
     * Session description
     *
     * c=* (connection information - not required if included in all media)
     */
    case -ENOENT:
        break;
    default:
        return 0;
    }
    return mangle_content_len(skb, protoff, dataoff, dptr, datalen);

以上介绍了SDP中地址和端口的替换,其中使用的新地址和新端口由以下函数nf_nat_sdp_media来确定,过程中还是使用如下的连接跟踪信息。

# conntrack -L
udp      17 3582 src=50.1.1.2 dst=192.168.1.109 sport=55719 dport=5060 src=192.168.1.109 dst=192.168.1.127 sport=5060 dport=55719 [ASSURED] mark=0 helper=sip use=1

对于原始方向,如本方向src=50.1.1.2回复方向dst=192.168.1.127,两者不相等,新媒体地址rtp_addr等于连接跟踪的反方向的目的地址(NAT之后地址192.168.1.127)。此处要建立的为与当前报文方向相反方向的expectation连接,这里仅确定此反向expectation连接的目的地址,源地址已经在连接跟踪中确定(参见set_expected_rtp_rtcp)。如果执行了NAT,目的地址应当为连接跟踪的反方向目的地址(NAT之后地址),而不是本方向的源地址。

反之,对于回复方向,本方向的src=192.168.1.109而反方向的dst=192.168.1.109,两个相同,表明地址没有变化,那么新目的地址rtp_addr等于rtp_exp的目的地址(一般情况下为报文中通告的地址,函数set_expected_rtp_rtcp可能将其改变)。如下,回复消息通告的媒体地址为192.168.1.126,为SIP客户端的真实地址,不需要改变。

SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.1.127:55719;received=192.168.1.127;rport=55719;branch=z9hG4bK1972895532
Record-Route: <sip:192.168.1.109;lr>
From: <sip:1002@192.168.1.109>;tag=82488285
To: <sip:1001@192.168.1.109>;tag=370640073
Call-ID: 1612402514@192.168.1.109
CSeq: 10 INVITE
Server: YATE/6.1.0
Contact: <sip:1001@192.168.1.126:65369>
Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO
Content-Type: application/sdp
Content-Length: 506

v=0
o=yate 1643511411 1643511411 IN IP4 192.168.1.126
s=SIP Call
c=IN IP4 192.168.1.126
t=0 0
m=audio 25510 RTP/AVP 0 8 3 11 98 97 102 103 104 105 106 101

以下在修改目的地址之前,保存连接原本的目的地址和端口号。

static unsigned int nf_nat_sdp_media(struct sk_buff *skb, unsigned int protoff,
                     unsigned int dataoff, const char **dptr, unsigned int *datalen,
                     struct nf_conntrack_expect *rtp_exp, struct nf_conntrack_expect *rtcp_exp,
                     unsigned int mediaoff, unsigned int medialen,
                     union nf_inet_addr *rtp_addr)
{
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);

    /* Connection will come from reply */
    if (nf_inet_addr_cmp(&ct->tuplehash[dir].tuple.src.u3,
                 &ct->tuplehash[!dir].tuple.dst.u3))
        *rtp_addr = rtp_exp->tuple.dst.u3;
    else
        *rtp_addr = ct->tuplehash[!dir].tuple.dst.u3;

    rtp_exp->saved_addr = rtp_exp->tuple.dst.u3;
    rtp_exp->tuple.dst.u3 = *rtp_addr;
    rtp_exp->saved_proto.udp.port = rtp_exp->tuple.dst.u.udp.port;
    rtp_exp->dir = !dir;
    rtp_exp->expectfn = nf_nat_sip_expected;

    rtcp_exp->saved_addr = rtcp_exp->tuple.dst.u3;
    rtcp_exp->tuple.dst.u3 = *rtp_addr;
    rtcp_exp->saved_proto.udp.port = rtcp_exp->tuple.dst.u.udp.port;
    rtcp_exp->dir = !dir;
    rtcp_exp->expectfn = nf_nat_sip_expected;

以下确定媒体的新端口号,优先使用通告的媒体端口(如以上INVITE请求通告的端口号:21254)。将RTP和RTCP两个expectation链接到系统中,如果存在冲突,更换端口,注意RTP使用偶数端口,RTCP使用紧邻的奇数端口。

    /* Try to get same pair of ports: if not, try to change them. */
    for (port = ntohs(rtp_exp->tuple.dst.u.udp.port); port != 0; port += 2) {
        int ret;

        rtp_exp->tuple.dst.u.udp.port = htons(port);
        ret = nf_ct_expect_related(rtp_exp, NF_CT_EXP_F_SKIP_MASTER);
        if (ret == -EBUSY)
            continue;
        else if (ret < 0) {
            port = 0;
            break;
        }
        rtcp_exp->tuple.dst.u.udp.port = htons(port + 1);
        ret = nf_ct_expect_related(rtcp_exp, NF_CT_EXP_F_SKIP_MASTER);
        if (ret == 0)
            break;
        else if (ret == -EBUSY) {
            nf_ct_unexpect_related(rtp_exp);
            continue;
        } else if (ret < 0) {
            nf_ct_unexpect_related(rtp_exp);
            port = 0;
            break;
        }
    }
    if (port == 0) {
        nf_ct_helper_log(skb, ct, "all ports in use for SDP media");
        goto err1;
    }

内核分别为RTP和RTCP创建了如下的两个expectation。注意这里sport等于0,即SIP客户端192.168.1.126可使用任意端口,所有当INVITE响应到达时,不再为其媒体端口(如以上25510)创建expectation。

# conntrack -E expect
    [NEW] 180 proto=17 src=192.168.1.109 dst=192.168.1.127 sport=0 dport=21254 mask-src=255.255.255.255 mask-dst=255.255.255.255 sport=0 dport=65535 master-src=50.1.1.2 master-dst=192.168.1.109 sport=55719 dport=5060 class=1 helper=sip
    [NEW] 180 proto=17 src=192.168.1.109 dst=192.168.1.127 sport=0 dport=21255 mask-src=255.255.255.255 mask-dst=255.255.255.255 sport=0 dport=65535 master-src=50.1.1.2 master-dst=192.168.1.109 sport=55719 dport=5060 class=1 helper=sip

使用以上确定的媒体端口号更新报文(m=)字段中的端口信息。

    /* Update media port. */
    if (rtp_exp->tuple.dst.u.udp.port != rtp_exp->saved_proto.udp.port &&
        !nf_nat_sdp_port(skb, protoff, dataoff, dptr, datalen,
                 mediaoff, medialen, port)) {
        nf_ct_helper_log(skb, ct, "cannot mangle SDP message");
        goto err2;
    }

注册信令处理

函数nf_nat_sip_expect用于处理SIP注册信令相关的expectation创建。首先确定新的目的地址,原理同以上的nf_nat_sdp_media函数。如果连接跟踪本方向的源地址和反方向的目的地址相等,表明NAT没有变换地址,新地址等于等于在Contact中解析到的地址(即保存在exp->tuple.dst.u3中的地址)。

# conntrack -L
udp      17 3582 src=50.1.1.2 dst=192.168.1.109 sport=55719 dport=5060 src=192.168.1.109 dst=192.168.1.127 sport=5060 dport=55719 [ASSURED] mark=0 helper=sip use=1

反之,例如连接跟踪本方向源地址50.1.1.2不等于反方向目的地址192.168.1.127,新目的地址等于连接跟踪反方向的目的地址(执行了NAT转换之后的地址,即192.168.1.127)。

static unsigned int nf_nat_sip_expect(struct sk_buff *skb, unsigned int protoff,
                      unsigned int dataoff,
                      const char **dptr, unsigned int *datalen,
                      struct nf_conntrack_expect *exp,
                      unsigned int matchoff, unsigned int matchlen)
{
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct);

    /* Connection will come from reply */
    if (nf_inet_addr_cmp(&ct->tuplehash[dir].tuple.src.u3,
                 &ct->tuplehash[!dir].tuple.dst.u3))
        newaddr = exp->tuple.dst.u3;
    else
        newaddr = ct->tuplehash[!dir].tuple.dst.u3;

如果expectation中初始的目的端口(即exp->tuple.dst.u.udp.port,由Contact中获得,如55719)等于连接跟踪本方向的源端口(sport=55719),本示例就是这种情况,将新的目的端口设置为连接跟踪反方向的目的端口(NAT之后的端口号,对于本示例也是dport=55719);反之,新的目的端口等于exp的目的端口。

    /* If the signalling port matches the connection's source port in the
     * original direction, try to use the destination port in the opposite
     * direction. */
    srcport = ct_sip_info->forced_dport ? : ct->tuplehash[dir].tuple.src.u.udp.port;
    if (exp->tuple.dst.u.udp.port == srcport)
        port = ntohs(ct->tuplehash[!dir].tuple.dst.u.udp.port);
    else
        port = ntohs(exp->tuple.dst.u.udp.port);

    exp->saved_addr = exp->tuple.dst.u3;
    exp->tuple.dst.u3 = newaddr;
    exp->saved_proto.udp.port = exp->tuple.dst.u.udp.port;
    exp->dir = !dir;
    exp->expectfn = nf_nat_sip_expected;

检查端口的可用性。之后将expectation链接到系统中。

    for (; port != 0; port++) {
        exp->tuple.dst.u.udp.port = htons(port);
        ret = nf_ct_expect_related(exp, NF_CT_EXP_F_SKIP_MASTER);
        if (ret == 0)
            break;
        else if (ret != -EBUSY) {
            port = 0;
            break;
        }
    }
    if (port == 0) {
        nf_ct_helper_log(skb, ct, "all ports in use for SIP");
        return NF_DROP;
    }

修改Contact字段中的地址和端口号,以便服务端的响应报文得到目的地址和目的端口号。

    if (!nf_inet_addr_cmp(&exp->tuple.dst.u3, &exp->saved_addr) ||
        exp->tuple.dst.u.udp.port != exp->saved_proto.udp.port) {
        buflen = sip_sprintf_addr_port(ct, buffer, &newaddr, port);
        if (!mangle_packet(skb, protoff, dataoff, dptr, datalen,
                   matchoff, matchlen, buffer, buflen)) {
            nf_ct_helper_log(skb, ct, "cannot mangle packet");
            goto err;
        }
    }
    return NF_ACCEPT;

本示例的客户端50.1.1.2的注册信令,源信令和转换之后的信令,如下:

REGISTER sip:192.168.1.109 SIP/2.0
Contact: <sip:1002@50.1.1.2:55719>
To: <sip:1002@192.168.1.109>
Via: SIP/2.0/UDP 50.1.1.2:55719;rport;branch=z9hG4bK47338961
From: <sip:1002@192.168.1.109>;tag=954429717
Call-ID: 460361071@192.168.1.109
CSeq: 9 REGISTER

REGISTER sip:192.168.1.109 SIP/2.0
Contact: <sip:1002@192.168.1.127:55719>
To: <sip:1002@192.168.1.109>
Via: SIP/2.0/UDP 192.168.1.127:55719;rport;branch=z9hG4bK47338961
From: <sip:1002@192.168.1.109>;tag=954429717
Call-ID: 460361071@192.168.1.109
CSeq: 9 REGISTER

内核创建了如下的永久expectation。在注册请求信令时生成永久和INACTIVE属性的expectation,在收到注册响应时,去掉INACTIVE。

# conntrack -E expect
    [NEW] 180 proto=17 src=192.168.1.109 dst=192.168.1.127 sport=0 dport=55719 mask-src=255.255.255.255 mask-dst=255.255.255.255 sport=0 dport=65535 master-src=50.1.1.2 master-dst=192.168.1.109 sport=55719 dport=5060 PERMANENT,INACTIVE class=0 helper=sip
#
# conntrack -L expect
529 proto=17 src=192.168.1.109 dst=192.168.1.127 sport=0 dport=55719 mask-src=255.255.255.255 mask-dst=255.255.255.255 sport=0 dport=65535 master-src=50.1.1.2 master-dst=192.168.1.109 sport=55719 dport=5060 PERMANENT class=0 helper=sip

SEQ序号调整

在sip连接跟踪函数sip_help_tcp的最后,调整序号。

static int sip_help_tcp(struct sk_buff *skb, unsigned int protoff,
            struct nf_conn *ct, enum ip_conntrack_info ctinfo)
{
    if (ret == NF_ACCEPT && ct->status & IPS_NAT_MASK) {
        const struct nf_nat_sip_hooks *hooks;

        hooks = rcu_dereference(nf_nat_sip_hooks);
        if (hooks)
            hooks->seq_adjust(skb, protoff, tdiff);
    }
    return ret;

如下根据报文长度的变化,设置seqadj扩展。

static void nf_nat_sip_seq_adjust(struct sk_buff *skb, unsigned int protoff, s16 off)
{
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    const struct tcphdr *th;

    if (nf_ct_protonum(ct) != IPPROTO_TCP || off == 0)
        return;

    th = (struct tcphdr *)(skb->data + protoff);
    nf_ct_seqadj_set(ct, ctinfo, th->seq, off);

内核版本 5.10

最后

以上就是机智大白为你收集整理的NAT SIP helper的全部内容,希望文章能够帮你解决NAT SIP helper所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(81)

评论列表共有 0 条评论

立即
投稿
返回
顶部