内核SYNPROXY实现

SYNPROXY配置如下,首先标记需要处理的报文,这里需要两个配置,一是关闭nf_conntrack_tcp_loose,这样无端的TCP报文(非SYN报文)将会被conntrack标记为INVALID状态。二是设置notrack标志,对TCP的SYN报文不继续连接跟踪,其状态为UNTRACKED。

    echo 0 > /proc/sys/net/netfilter/nf_conntrack_tcp_loose
	
    iptables -t raw -A PREROUTING -i eth0 -p tcp --dport 80
        --syn -j CT --notrack

其次,将以上两类报文导入SYNPROXY模块处理,

    iptables -A INPUT -i eth0 -p tcp --dport 80
        -m state --state UNTRACKED,INVALID -j SYNPROXY
        --sack-perm --timestamp --mss 1460 --wscale 9
或者:
    iptables -A FORWARD -i eth0 -p tcp --dport 80
        -m state --state UNTRACKED,INVALID -j SYNPROXY
        --sack-perm --timestamp --mss 1460 --wscale 9

最后,对于以上未处理的报文,如TCP报文的flags中包含错误标志组合,类似SYN+FIN,SYN+ACK等,进行丢弃处理。

    iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state INVALID -j DROP

x

初始化

初始化由xt_register_target函数将SYNPROXY的目标结构synproxy_tg4_reg注册到netfilter系统中。SYNPROXY可作用在NF_INET_LOCAL_IN或者NF_INET_FORWARD挂载点,不但可用于保护本机的TCP服务,也可保护位于之后的服务器。

static struct xt_target synproxy_tg4_reg __read_mostly = {
    .name       = "SYNPROXY",
    .family     = NFPROTO_IPV4,
    .hooks      = (1 << NF_INET_LOCAL_IN) | (1 << NF_INET_FORWARD),
    .target     = synproxy_tg4,
    .targetsize = sizeof(struct xt_synproxy_info),
    .checkentry = synproxy_tg4_check,
	
static int __init synproxy_tg4_init(void)
{
    return xt_register_target(&synproxy_tg4_reg);

TCP流量检测

由以上注册的函数synproxy_tg4_check完成,对于非TCP流量返回错误。否则,注册netfilter的hook点操作函数结构ipv4_synproxy_ops,此用于处理SYNPROXY与服务端的交互,稍后会看到。

static int synproxy_tg4_check(const struct xt_tgchk_param *par)
{
    struct synproxy_net *snet = synproxy_pernet(par->net);
    const struct ipt_entry *e = par->entryinfo;

    if (e->ip.proto != IPPROTO_TCP || e->ip.invflags & XT_INV_PROTO)
        return -EINVAL;
    err = nf_ct_netns_get(par->net, par->family);

    if (snet->hook_ref4 == 0) {
        err = nf_register_net_hooks(par->net, ipv4_synproxy_ops,  ARRAY_SIZE(ipv4_synproxy_ops));
        ...
    }
    snet->hook_ref4++;

结构ipv4_synproxy_ops分别在hook点NF_INET_LOCAL_IN和NF_INET_POST_ROUTING注册了处理函数ipv4_synproxy_hook,

static const struct nf_hook_ops ipv4_synproxy_ops[] = {
    {
        .hook       = ipv4_synproxy_hook,
        .pf     = NFPROTO_IPV4,
        .hooknum    = NF_INET_LOCAL_IN,
        .priority   = NF_IP_PRI_CONNTRACK_CONFIRM - 1,
    },
    {
        .hook       = ipv4_synproxy_hook,
        .pf     = NFPROTO_IPV4,
        .hooknum    = NF_INET_POST_ROUTING,
        .priority   = NF_IP_PRI_CONNTRACK_CONFIRM - 1,
    },
};

客户端SYN选项报文处理

SYNPROXY的target处理函数为以上注册的synproxy_tg4,首先解析TCP报文中的选项信息,之后,对于SYN报文,如果TCP头部设置了ECE和CWR标志位,增加显式拥塞通知选项。将得到的TCP选项集合与命令行中配置的选项进行与操作,得到其中的共同支持的交集。

如果最终的TCP选项支持timestamp,使用函数synproxy_init_timestamp_cookie将WSCALE/SACK_PERM/ECN嵌入在timestamp选项数据中。否则,从选项集中清除这几个选项。

static unsigned int synproxy_tg4(struct sk_buff *skb, const struct xt_action_param *par)
{
    const struct xt_synproxy_info *info = par->targinfo;
    struct net *net = xt_net(par);
    struct synproxy_net *snet = synproxy_pernet(net);
    struct synproxy_options opts = {};

    if (!synproxy_parse_options(skb, par->thoff, th, &opts))
        return NF_DROP;

    if (th->syn && !(th->ack || th->fin || th->rst)) {
        /* Initial SYN from client */
        this_cpu_inc(snet->stats->syn_received);

        if (th->ece && th->cwr) opts.options |= XT_SYNPROXY_OPT_ECN;

        opts.options &= info->options;
        if (opts.options & XT_SYNPROXY_OPT_TIMESTAMP)
            synproxy_init_timestamp_cookie(info, &opts);
        else
            opts.options &= ~(XT_SYNPROXY_OPT_WSCALE | XT_SYNPROXY_OPT_SACK_PERM | XT_SYNPROXY_OPT_ECN);

在选项解析函数synproxy_parse_options中,记录TCP头部的4个选项,包括:TCPOPT_MSS、TCPOPT_WINDOW、TCPOPT_TIMESTAMP和TCPOPT_SACK_PERM。注意除了TCPOPT_TIMESTAMP选项,其与三个TCP选项仅在SYN和SYN+ACK报文中出现。

bool synproxy_parse_options(const struct sk_buff *skb, unsigned int doff, const struct tcphdr *th, struct synproxy_options *opts)
{
    int length = (th->doff * 4) - sizeof(*th);

    ptr = skb_header_pointer(skb, doff + sizeof(*th), length, buf);
    while (length > 0) {
        int opcode = *ptr++;
        switch (opcode) {
        ...
        default:
            opsize = *ptr++;
            switch (opcode) {
            case TCPOPT_MSS:
                if (opsize == TCPOLEN_MSS) {
                    opts->mss = get_unaligned_be16(ptr);
                    opts->options |= XT_SYNPROXY_OPT_MSS;
                }
                break;
            case TCPOPT_WINDOW:
                if (opsize == TCPOLEN_WINDOW) {
                    opts->wscale = *ptr;
                    if (opts->wscale > TCP_MAX_WSCALE)
                        opts->wscale = TCP_MAX_WSCALE;
                    opts->options |= XT_SYNPROXY_OPT_WSCALE;
                }
                break;
            case TCPOPT_TIMESTAMP:
                if (opsize == TCPOLEN_TIMESTAMP) {
                    opts->tsval = get_unaligned_be32(ptr);
                    opts->tsecr = get_unaligned_be32(ptr + 4);
                    opts->options |= XT_SYNPROXY_OPT_TIMESTAMP;
                }
                break;
            case TCPOPT_SACK_PERM:
                if (opsize == TCPOLEN_SACK_PERM)
                    opts->options |= XT_SYNPROXY_OPT_SACK_PERM;
                break;

如下函数synproxy_init_timestamp_cookie,清空timestamp选项的tsval的后6位,其中后四位记录客户端SYN报文中通告的WSCALE的值(其最大值为14),第五位记录SACK_PERM,最后一位记录ECN的值。SYN+ACK报文中TCP选项WSCALE使用命令行配置的本端的wscale值。

在接收到客户端之后的ACK报文后,可由其中的timestamp数据,还原客户端的TCP选项信息。对于客户端的MSS值,将封装在SYNACK报文的序列号中,内核的5.0版本的一个问题是,iptables命令行设置的MSS值没有生效,SYN+ACK报文中通告的服务端MSS值使用的是客户端的MSS值,检查了最新内核5.6.0版本,发现此问题已经修正。

void synproxy_init_timestamp_cookie(const struct xt_synproxy_info *info, struct synproxy_options *opts)
{   
    opts->tsecr = opts->tsval;
    opts->tsval = tcp_time_stamp_raw() & ~0x3f;

    if (opts->options & XT_SYNPROXY_OPT_WSCALE) {
        opts->tsval |= opts->wscale;
        opts->wscale = info->wscale;
    } else
        opts->tsval |= 0xf;
        
    if (opts->options & XT_SYNPROXY_OPT_SACK_PERM)
        opts->tsval |= 1 << 4;
            
    if (opts->options & XT_SYNPROXY_OPT_ECN)
        opts->tsval |= 1 << 5; 

如下位图所示为tsval的值组成:

    MSB                               LSB
    | 31 ...   6 |  5  |  4   | 3 2 1 0 |
    |  Timestamp | ECN | SACK | WScale  |

回复客户端SYNACK

在SYNPROXY目标处理函数synproxy_tg4中,将回复客户端的SYN报文,以下函数synproxy_send_client_synack回复客户端SYN+ACK报文。主要注意这里TCP的初始序列号由syncookie函数所生成。TCP头部的window值为零,这将阻止客户端发送报文,因为此时还没有和服务端建立连接。另外,由于之前命令行对SYN报文指定了NOTRACK状态,函数synproxy_send_tcp中设置IP_CT_ESTABLISHED_REPLY将没有任何作用。

static void synproxy_send_client_synack(struct net *net, const struct sk_buff *skb, const struct tcphdr *th,
                const struct synproxy_options *opts)
{
    iph = ip_hdr(skb);

    tcp_hdr_size = sizeof(*nth) + synproxy_options_size(opts);
    nskb = alloc_skb(sizeof(*niph) + tcp_hdr_size + MAX_TCP_HEADER, GFP_ATOMIC);
    skb_reserve(nskb, MAX_TCP_HEADER);

    niph = synproxy_build_ip(net, nskb, iph->daddr, iph->saddr);

    skb_reset_transport_header(nskb);
    nth = skb_put(nskb, tcp_hdr_size);
    nth->source = th->dest;
    nth->dest   = th->source;
    nth->seq    = htonl(__cookie_v4_init_sequence(iph, th, &mss));
    nth->ack_seq    = htonl(ntohl(th->seq) + 1);
    tcp_flag_word(nth) = TCP_FLAG_SYN | TCP_FLAG_ACK;
    if (opts->options & XT_SYNPROXY_OPT_ECN) tcp_flag_word(nth) |= TCP_FLAG_ECE;
    nth->doff   = tcp_hdr_size / 4;
    nth->window = 0;

    synproxy_build_options(nth, opts);

    synproxy_send_tcp(net, skb, nskb, skb_nfct(skb), IP_CT_ESTABLISHED_REPLY, niph, nth, tcp_hdr_size);

处理客户端ACK

客户端回复的ACK报文,也是在SYNPROXY目标函数synproxy_tg4中处理,主要由函数synproxy_recv_client_ack完成。

static unsigned int synproxy_tg4(struct sk_buff *skb, const struct xt_action_param *par)
{
    ...
    if (th->syn && !(th->ack || th->fin || th->rst)) {
        ...
        return NF_STOLEN;
    } else if (th->ack && !(th->fin || th->rst || th->syn)) {
        /* ACK from client */
        if (synproxy_recv_client_ack(net, skb, th, &opts, ntohl(th->seq))) {
            consume_skb(skb);
            return NF_STOLEN;
        } else {
            return NF_DROP;
        }
    }
    return XT_CONTINUE;

在处理客户端ACK报文时,使用syncookie函数验证其中的ack序号是否正确,并且使用函数synproxy_check_timestamp_cookie还原客户端的TCP选项。与客户端的三次握手完成,此时可以开始与真正的服务器建立TCP连接了,函数synproxy_send_server_syn发送SYN报文。

static bool synproxy_recv_client_ack(struct net *net, const struct sk_buff *skb, 
             const struct tcphdr *th, struct synproxy_options *opts, u32 recv_seq)
{
    struct synproxy_net *snet = synproxy_pernet(net);
    int mss;

    mss = __cookie_v4_check(ip_hdr(skb), th, ntohl(th->ack_seq) - 1);
    if (mss == 0) {
        this_cpu_inc(snet->stats->cookie_invalid);
        return false;
    }

    this_cpu_inc(snet->stats->cookie_valid);
    opts->mss = mss;
    opts->options |= XT_SYNPROXY_OPT_MSS;

    if (opts->options & XT_SYNPROXY_OPT_TIMESTAMP)
        synproxy_check_timestamp_cookie(opts);

    synproxy_send_server_syn(net, skb, th, opts, recv_seq);
    return true;

发送SYN到服务端

SYN报文的序列号为客户端ACK报文的序列号减去一,即为客户端原始的序列号,这样客户端到服务端的序号,以及服务端到客户端的确认序号就可保存一致,无需SYNPROXY进行干预。ack_seq序列号设置为客户端ACK报文中的ACK序列号值减去一,即为当前SYNPROXY使用的初始序列号。新的SYN报文中的窗口值使用客户端ACK报文中的窗口值。

另外,在函数synproxy_build_options中,TCP时间戳选项的tsval设置为客户端ACK报文中的tsval值,tsecr值设置为其中的tsecr值,即SYNPROXY的时间戳,以便在随后的ipv4_synproxy_hook挂载点函数中,将记录此时间值,稍后做介绍。

static void synproxy_send_server_syn(struct net *net, const struct sk_buff *skb, const struct tcphdr *th,
             const struct synproxy_options *opts, u32 recv_seq)
{
    struct synproxy_net *snet = synproxy_pernet(net);

    iph = ip_hdr(skb);
    tcp_hdr_size = sizeof(*nth) + synproxy_options_size(opts);
    nskb = alloc_skb(sizeof(*niph) + tcp_hdr_size + MAX_TCP_HEADER, GFP_ATOMIC);
    skb_reserve(nskb, MAX_TCP_HEADER);

    niph = synproxy_build_ip(net, nskb, iph->saddr, iph->daddr);

    skb_reset_transport_header(nskb);
    nth = skb_put(nskb, tcp_hdr_size);
    nth->source = th->source;
    nth->dest   = th->dest;
    nth->seq    = htonl(recv_seq - 1);
    /* ack_seq is used to relay our ISN to the synproxy hook to initialize
     * sequence number translation once a connection tracking entry exists.
     */
    nth->ack_seq    = htonl(ntohl(th->ack_seq) - 1);
    tcp_flag_word(nth) = TCP_FLAG_SYN;
    if (opts->options & XT_SYNPROXY_OPT_ECN)
        tcp_flag_word(nth) |= TCP_FLAG_ECE | TCP_FLAG_CWR;
    nth->doff   = tcp_hdr_size / 4;
    nth->window = th->window;

    synproxy_build_options(nth, opts);
    synproxy_send_tcp(net, skb, nskb, &snet->tmpl->ct_general, IP_CT_NEW, niph, nth, tcp_hdr_size);

函数synproxy_send_tcp在发送时,使用了synproxy_net结构中的一个模板类型的nf_conntrack,其已经在synproxy_net_init函数中进行了初始化,所以这里可直接使用。并且将新状态设置为IP_CT_NEW。这样,在报文经过LOCAL_OUT挂载点,创建conntrack结构时,不至于被当做loopback或者untracked报文,不与创建connntrack。

unsigned int nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
{
    ...
    tmpl = nf_ct_get(skb, &ctinfo);
    if (tmpl || ctinfo == IP_CT_UNTRACKED) {
        /* Previously seen (loopback or untracked)?  Ignore. */
        if ((tmpl && !nf_ct_is_template(tmpl)) || ctinfo == IP_CT_UNTRACKED) {
            NF_CT_STAT_INC_ATOMIC(state->net, ignore);
            return NF_ACCEPT;
        }
        skb->_nfct = 0;
    }

由于SYNPROXY在初始化conntrack模板时,增加了两个extension,包括:seqadj和synproxy。所以,在以上函数nf_conntrack_in中,依照此模板创建conntrack时,新结构也将包含这两个extension。

static int __net_init synproxy_net_init(struct net *net)
{
    struct synproxy_net *snet = synproxy_pernet(net);
    struct nf_conn *ct;

    ct = nf_ct_tmpl_alloc(net, &nf_ct_zone_dflt, GFP_KERNEL);

    if (!nfct_seqadj_ext_add(ct))
        goto err2;
    if (!nfct_synproxy_ext_add(ct))
        goto err2;

    __set_bit(IPS_CONFIRMED_BIT, &ct->status);
    nf_conntrack_get(&ct->ct_general);
    snet->tmpl = ct;

如以下的连接跟踪初始化函数init_conntrack,调用nf_ct_add_synproxy函数,依据tmpl参数添加synproxy扩展。

static noinline struct nf_conntrack_tuple_hash *
init_conntrack(struct net *net, struct nf_conn *tmpl, const struct nf_conntrack_tuple *tuple, ...)
{
    ...
    zone = nf_ct_zone_tmpl(tmpl, skb, &tmp);
    ct = __nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC, hash);

    if (!nf_ct_add_synproxy(ct, tmpl)) {
        nf_conntrack_free(ct);
        return ERR_PTR(-ENOMEM);
    }

在函数nf_ct_add_synproxy中,如果模板tmpl包含synproxy扩展(NF_CT_EXT_SYNPROXY),为当前连接跟踪结构增加seqadj和synproxy扩展。

static inline bool nf_ct_add_synproxy(struct nf_conn *ct, const struct nf_conn *tmpl)
{        
    if (tmpl && nfct_synproxy(tmpl)) {
        if (!nfct_seqadj_ext_add(ct))
            return false; 

        if (!nfct_synproxy_ext_add(ct))
            return false;
    } 

处理服务端SYNACK

本文开头叙述了,synproxy在NF_INET_LOCAL_IN和NF_INET_POST_ROUTING点注册了挂载函数ipv4_synproxy_hook,并且优先级为NF_IP_PRI_CONNTRACK_CONFIRM - 1,即优先级高于连接跟踪确认处理的优先级。前者用于SYNPROXY代理本机的服务,SYNPROXY到服务的报文将通过loopback接口完成;后者用于保护本机之后的服务器。

static const struct nf_hook_ops ipv4_synproxy_ops[] = {
    {
        .hook       = ipv4_synproxy_hook,
        .pf     = NFPROTO_IPV4,
        .hooknum    = NF_INET_LOCAL_IN,
        .priority   = NF_IP_PRI_CONNTRACK_CONFIRM - 1,
    },
    {
        .hook       = ipv4_synproxy_hook,
        .pf     = NFPROTO_IPV4,
        .hooknum    = NF_INET_POST_ROUTING,
        .priority   = NF_IP_PRI_CONNTRACK_CONFIRM - 1,
    },
};

由于此前发送给客户端的SYN+ACK报文,没有创建连接跟踪结构,此函数将不做处理返回。而对于发往服务端的SYN报文,由于在LOCAL_OUT节点已经创建了连接跟踪结构,并且添加了synproxy扩展,以下的判断都通过,继续执行。

static unsigned int ipv4_synproxy_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *nhs)
{
    struct net *net = nhs->net;
    struct synproxy_net *snet = synproxy_pernet(net);

    ct = nf_ct_get(skb, &ctinfo);
    if (ct == NULL)
        return NF_ACCEPT;

    synproxy = nfct_synproxy(ct);
    if (synproxy == NULL)
        return NF_ACCEPT;

    if (nf_is_loopback_packet(skb) ||
        ip_hdr(skb)->protocol != IPPROTO_TCP)
        return NF_ACCEPT;

    thoff = ip_hdrlen(skb);
    th = skb_header_pointer(skb, thoff, sizeof(_th), &_th);

如果连接跟踪的TCP状态为TCP_CONNTRACK_SYN_SENT,说明为发送服务器的SYN报文,在这里解析选项信息,记录下TCP头部的确认序号ack_seq和timestamp的回复值。由于在SYN报文中并没有设置ACK位,TCP头部的ack_seq字段无效,但是在以上的synproxy_send_server_syn函数中,内核使用此字段保存了SYNPROXY使用的序列号,即初始序号(ISN),由于此并非真实服务器的ISN,随后SYNPROXY将在两者之间做转换。

类似的,TCP选项的tsecr字段在以上函数synproxy_send_server_syn中,保存的是SYNPROXY自身产生的timestamp值,也不是真实服务器的时间戳,SYNPROXY也需要对服务器的时间戳做转换,以保持时间的连续性。

另外,在TCP_CONNTRACK_SYN_SENT状态,由可能会接收到客户端的后续ACK报文,此类Keep-Alices类型的ACK,会重复使用之前的序号,使用synproxy_recv_client_ack函数处理时,回复正常序号。

    state = &ct->proto.tcp;
    switch (state->state) {
    case TCP_CONNTRACK_CLOSE:
        ...
        /* fall through */
    case TCP_CONNTRACK_SYN_SENT:
        if (!synproxy_parse_options(skb, thoff, th, &opts))
            return NF_DROP;

        if (!th->syn && th->ack && CTINFO2DIR(ctinfo) == IP_CT_DIR_ORIGINAL) {
            /* Keep-Alives are sent with SEG.SEQ = SND.NXT-1,
             * therefore we need to add 1 to make the SYN sequence number match the one of first SYN.
             */
            if (synproxy_recv_client_ack(net, skb, th, &opts, ntohl(th->seq) + 1)) {
                this_cpu_inc(snet->stats->cookie_retrans);
                consume_skb(skb);
                return NF_STOLEN;
            } else {
                return NF_DROP;
            }
        }

        synproxy->isn = ntohl(th->ack_seq);
        if (opts.options & XT_SYNPROXY_OPT_TIMESTAMP)
            synproxy->its = opts.tsecr;

        nf_conntrack_event_cache(IPCT_SYNPROXY, ct);
        break;

对于TCP_CONNTRACK_SYN_RECV状态,表明接收到了服务端回复的SYN+ACK报文。在这里回复服务端ACK报文,完成TCP三次握手。如果服务端的SYN+ACK报文携带了timestamp选项,计算其时间戳与SYNPROXY时间戳的差值。

另外,使用函数nf_ct_seqadj_init初始化序号调整功能,向客户端发送ACK报文,将服务端SYN+ACK报文中的窗口值,通告给客户端,以便开始数据传输。ACK报文中不能懈怠MSS/WSCALE/SACK_PERM选项,进行清查操作。

    case TCP_CONNTRACK_SYN_RECV:
        if (!th->syn || !th->ack)
            break;

        if (!synproxy_parse_options(skb, thoff, th, &opts))
            return NF_DROP;

        if (opts.options & XT_SYNPROXY_OPT_TIMESTAMP) {
            synproxy->tsoff = opts.tsval - synproxy->its;
            nf_conntrack_event_cache(IPCT_SYNPROXY, ct);
        }

        opts.options &= ~(XT_SYNPROXY_OPT_MSS | XT_SYNPROXY_OPT_WSCALE | XT_SYNPROXY_OPT_SACK_PERM);

        swap(opts.tsval, opts.tsecr);
        synproxy_send_server_ack(net, state, skb, th, &opts);

        nf_ct_seqadj_init(ct, ctinfo, synproxy->isn - ntohl(th->seq));
        nf_conntrack_event_cache(IPCT_SEQADJ, ct);

        swap(opts.tsval, opts.tsecr);
        synproxy_send_client_ack(net, skb, th, &opts);

        consume_skb(skb);
        return NF_STOLEN;
    default:
        break;
    }

最后,对于连接跟踪的TCP状态不属于以上情况,使用函数synproxy_tstamp_adjust调整TCP时间戳选项。


    synproxy_tstamp_adjust(skb, thoff, th, ct, ctinfo, synproxy);
    return NF_ACCEPT;

发送ACK到服务端

注意这里通告的窗口值为原始方向(IP_CT_DIR_ORIGINAL)的最大窗口值td_maxwin,此值由conntrack系统跟踪而来。

static void synproxy_send_server_ack(struct net *net, const struct ip_ct_tcp *state,
             const struct sk_buff *skb, const struct tcphdr *th, const struct synproxy_options *opts)
{   
    struct tcphdr *nth;
    unsigned int tcp_hdr_size;
    
    iph = ip_hdr(skb);
    tcp_hdr_size = sizeof(*nth) + synproxy_options_size(opts);
    nskb = alloc_skb(sizeof(*niph) + tcp_hdr_size + MAX_TCP_HEADER, GFP_ATOMIC);

    skb_reserve(nskb, MAX_TCP_HEADER);
    niph = synproxy_build_ip(net, nskb, iph->daddr, iph->saddr);
    
    skb_reset_transport_header(nskb);
    nth = skb_put(nskb, tcp_hdr_size);
    nth->source = th->dest;
    nth->dest   = th->source;
    nth->seq    = htonl(ntohl(th->ack_seq)); 
    nth->ack_seq    = htonl(ntohl(th->seq) + 1);
    tcp_flag_word(nth) = TCP_FLAG_ACK;
    nth->doff   = tcp_hdr_size / 4;
    nth->window = htons(state->seen[IP_CT_DIR_ORIGINAL].td_maxwin);

发送ACK到客户端

由于之前发送给客户端的SYN+ACK报文通告了零窗口值,此时在ACK报文中窗口的大小值,使用服务端SYN+ACK报文中的窗口值,右移wscale之后的结果值。由于SYN+ACK报文中的窗口值是不左移窗口系数wscale的,一般情况下为较大的值,此处的右移wscale,将TCP头部的窗口字段恢复为左移之前的基值。

需要注意的是这里的wscale值使用的是服务端的真实窗口系数,而客户端并不知晓此值,客户端接收到的wscale值为SYNPROXY在SYN+ACK报文中发送的值,即在iptables命令行中指定的值。这两个窗口系数应当相同,否则,客户端将不能准确计算服务器的窗口值。

static void synproxy_send_client_ack(struct net *net, const struct sk_buff *skb, const struct tcphdr *th,
             const struct synproxy_options *opts)
{
    iph = ip_hdr(skb);

    tcp_hdr_size = sizeof(*nth) + synproxy_options_size(opts);
    nskb = alloc_skb(sizeof(*niph) + tcp_hdr_size + MAX_TCP_HEADER, GFP_ATOMIC);

    skb_reserve(nskb, MAX_TCP_HEADER);

    niph = synproxy_build_ip(net, nskb, iph->saddr, iph->daddr);

    skb_reset_transport_header(nskb);
    nth = skb_put(nskb, tcp_hdr_size);
    nth->source = th->source;
    nth->dest   = th->dest;
    nth->seq    = htonl(ntohl(th->seq) + 1);
    nth->ack_seq    = th->ack_seq;
    tcp_flag_word(nth) = TCP_FLAG_ACK;
    nth->doff   = tcp_hdr_size / 4;
    nth->window = htons(ntohs(th->window) >> opts->wscale);

iptables文档中给出了获取服务器的TCP选项的命令,如下:

    tcpdump -pni eth0 -c 1 'tcp[tcpflags] == (tcp-syn|tcp-ack)' port 80 &
    telnet 192.0.2.42 80
    18:57:24.693307 IP 192.0.2.42.80 > 192.0.2.43.48757:
        Flags [S.], seq 360414582, ack 788841994, win 14480,
        options [mss 1460,sackOK,
        TS val 1409056151 ecr 9690221,
        nop,wscale 9],
        length 0

时间戳调整

如果SYNPROXY和真实服务器两者的时间戳差值为零,则无需进行调整。

unsigned int synproxy_tstamp_adjust(struct sk_buff *skb, unsigned int protoff, struct tcphdr *th,
                    struct nf_conn *ct, enum ip_conntrack_info ctinfo, const struct nf_conn_synproxy *synproxy)
{
    unsigned int optoff, optend;
    __be32 *ptr, old;

    if (synproxy->tsoff == 0) return 1;

    optoff = protoff + sizeof(struct tcphdr);
    optend = protoff + th->doff * 4;

否则,对于回复方向(IP_CT_DIR_REPLY)的报文,将真实服务器的时间戳减去差值tsoff,得到SYNPROXY的时间戳。对于原始方向(IP_CT_DIR_ORIGINAL)的报文,将时间戳加上差值,得到真实服务器的时间戳。最后,重新计算TCP校验和。

    while (optoff < optend) {
        unsigned char *op = skb->data + optoff;
        switch (op[0]) {
        ...
        default:
            if (optoff + 1 == optend || optoff + op[1] > optend || op[1] < 2)
                return 0;
            if (op[0] == TCPOPT_TIMESTAMP && op[1] == TCPOLEN_TIMESTAMP) {
                if (CTINFO2DIR(ctinfo) == IP_CT_DIR_REPLY) {
                    ptr = (__be32 *)&op[2];
                    old = *ptr;
                    *ptr = htonl(ntohl(*ptr) - synproxy->tsoff);
                } else {
                    ptr = (__be32 *)&op[6];
                    old = *ptr;
                    *ptr = htonl(ntohl(*ptr) + synproxy->tsoff);
                }
                inet_proto_csum_replace4(&th->check, skb, old, *ptr, false);
                return 1;
            }
            optoff += op[1];
        }

序列号调整

首先看一下在函数ipv4_synproxy_hook中调用的seqadj初始化函数,设置连接跟踪的IPS_SEQ_ADJUST_BIT标志位,并且初始化序号差值。注意SYNPROXY仅需要调整服务端到PROXY的序号,以及PROXY到服务端的确认序号。不需要调整客户端到SYNPROXY的序号,也无需调整PROXY到客户端的确认序号。

int nf_ct_seqadj_init(struct nf_conn *ct, enum ip_conntrack_info ctinfo, s32 off)              
{                                   
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct nf_conn_seqadj *seqadj;  
    struct nf_ct_seqadj *this_way;  
   
    if (off == 0) return 0;
   
    set_bit(IPS_SEQ_ADJUST_BIT, &ct->status);
   
    seqadj = nfct_seqadj(ct);       
    this_way = &seqadj->seq[dir];   
    this_way->offset_before  = off;
    this_way->offset_after   = off;

序列号的调整是使用连接跟踪的seqadj扩展完成的,对于IPv4协议,位于ipv4_confirm函数中。

static unsigned int ipv4_confirm(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    struct nf_conn *ct;
    enum ip_conntrack_info ctinfo;

    ct = nf_ct_get(skb, &ctinfo);
    if (!ct || ctinfo == IP_CT_RELATED_REPLY)
        goto out;

    /* adjust seqs for loopback traffic only in outgoing direction */
    if (test_bit(IPS_SEQ_ADJUST_BIT, &ct->status) && !nf_is_loopback_packet(skb)) {
        if (!nf_ct_seq_adjust(skb, ct, ctinfo, ip_hdrlen(skb))) {
            NF_CT_STAT_INC_ATOMIC(nf_ct_net(ct), drop);
            return NF_DROP;

函数nf_ct_seq_adjust完成具体的序号调整工作。对于服务器到SYNPROXY之间的报文,连接跟踪方向为回复方向(IP_CT_DIR_REPLY),进行服务器报文序号的调整,将其增加seqoff差值,转换为SYNPROXY的序号。

int nf_ct_seq_adjust(struct sk_buff *skb, struct nf_conn *ct, enum ip_conntrack_info ctinfo, unsigned int protoff)
{
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct nf_conn_seqadj *seqadj = nfct_seqadj(ct);

    this_way  = &seqadj->seq[dir];
    other_way = &seqadj->seq[!dir];
    ...
    tcph = (void *)skb->data + protoff;
    spin_lock_bh(&ct->lock);
    if (after(ntohl(tcph->seq), this_way->correction_pos))
        seqoff = this_way->offset_after;
    else
        seqoff = this_way->offset_before;

    newseq = htonl(ntohl(tcph->seq) + seqoff);
    inet_proto_csum_replace4(&tcph->check, skb, tcph->seq, newseq, false);
    pr_debug("Adjusting sequence number from %u->%u\n", ntohl(tcph->seq), ntohl(newseq));
    tcph->seq = newseq;

对于SYNPROXY到服务器之间报文,连接跟踪方向为原始方向(IP_CT_DIR_ORIGINAL),调整报文的确认序号,将SYNPROXY的确认序号减去差值ackoff,得到服务器确认序号。

    if (!tcph->ack) goto out;

    if (after(ntohl(tcph->ack_seq) - other_way->offset_before, other_way->correction_pos))
        ackoff = other_way->offset_after;
    else
        ackoff = other_way->offset_before;

    newack = htonl(ntohl(tcph->ack_seq) - ackoff);
    inet_proto_csum_replace4(&tcph->check, skb, tcph->ack_seq, newack, false);
    pr_debug("Adjusting ack number from %u->%u, ack from %u->%u\n",
         ntohl(tcph->seq), ntohl(newseq), ntohl(tcph->ack_seq), ntohl(newack));
    tcph->ack_seq = newack;

    res = nf_ct_sack_adjust(skb, protoff, ct, ctinfo);

对于SYNPROXY而言,nf_ct_seq_adjust函数每次仅调整TCP序号或者确认序号,不会同时进行调整。函数nf_ct_sack_adjust负责调整SACK选项中的序列号。

内核版本 5.0

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页