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