nfqws: improve postnat workaround

This commit is contained in:
bol-van 2024-04-08 19:34:01 +03:00
parent c7ccce37fe
commit d1389f2527
13 changed files with 117 additions and 92 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -321,40 +321,60 @@ static void reasm_orig_fin(t_ctrack *ctrack)
}
static packet_process_result ct_new_postnat_fix(const t_ctrack *ctrack, struct ip *ip, packet_process_result res)
static uint8_t ct_new_postnat_fix(const t_ctrack *ctrack, struct ip *ip, struct ip6_hdr *ip6, uint8_t proto, struct udphdr *udp, struct tcphdr *tcp, size_t *len_pkt)
{
#ifdef __linux__
// if used in postnat chain, dropping initial packet will cause conntrack connection teardown
// so we need to workaround this.
// we can't use low ttl for UDP because TCP/IP stack listens to ttl expired ICMPs and notify socket
// we also can't use TCP fooling because DPI would accept fooled packets
if (ip && ctrack && ctrack->pcounter_orig==1)
// we can't use low ttl because TCP/IP stack listens to ttl expired ICMPs and notify socket
// we also can't use fooling because DPI would accept fooled packets
if (ctrack && ctrack->pcounter_orig==1)
{
// routers will drop IP frames with invalid checksum
if (ip->ip_p==IPPROTO_TCP)
DLOG("applying linux postnat conntrack workaround\n")
if (proto==IPPROTO_UDP && udp && len_pkt)
{
// linux recalc ip checksum in tcp
// need another limiter
ip->ip_ttl=1;
// make malformed udp packet with zero length and invalid checksum
udp->len = 0; // invalid length. must be >=8
udp_fix_checksum(udp,sizeof(struct udphdr),ip,ip6);
udp->check ^= htons(0xBEAF);
// truncate packet
*len_pkt = (uint8_t*)udp - (ip ? (uint8_t*)ip : (uint8_t*)ip6) + sizeof(struct udphdr);
if (ip)
{
ip->ip_len = htons((uint16_t)*len_pkt);
ip4_fix_checksum(ip);
}
else if (ip6)
ip6->ip6_ctlun.ip6_un1.ip6_un1_plen = (uint16_t)htons(sizeof(struct udphdr));
}
else
ip->ip_sum ^= htons(0xBEAF);
return res==frag ? modfrag : modify;
else if (proto==IPPROTO_TCP && tcp)
{
// only SYN here is expected
// make flags invalid and also corrupt checksum
tcp->th_flags = 0;
}
if (ip) ip->ip_sum ^= htons(0xBEAF);
return VERDICT_MODIFY | VERDICT_NOCSUM;
}
else
#endif
// ipv6 does not have checksum
// consider we are free of NAT in ipv6 case. just drop
// BSDs also do not need this
return drop;
return VERDICT_DROP;
}
static uint8_t ct_new_postnat_fix_tcp(const t_ctrack *ctrack, struct ip *ip, struct ip6_hdr *ip6, struct tcphdr *tcphdr)
{
return ct_new_postnat_fix(ctrack,ip,ip6,IPPROTO_TCP,NULL,tcphdr,NULL);
}
static uint8_t ct_new_postnat_fix_udp(const t_ctrack *ctrack, struct ip *ip, struct ip6_hdr *ip6, struct udphdr *udphdr, size_t *len_pkt)
{
return ct_new_postnat_fix(ctrack,ip,ip6,IPPROTO_UDP,udphdr,NULL,len_pkt);
}
// result : true - drop original packet, false = dont drop
packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct tcphdr *tcphdr, size_t len_tcp, uint8_t *data_payload, size_t len_payload)
uint8_t dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t *len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct tcphdr *tcphdr, size_t len_tcp, uint8_t *data_payload, size_t len_payload)
{
packet_process_result res=pass;
uint8_t res=VERDICT_PASS;
t_ctrack *ctrack=NULL;
bool bReverse=false;
@ -377,7 +397,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
if (params.wsize && tcp_synack_segment(tcphdr))
{
tcp_rewrite_winsize(tcphdr, params.wsize, params.wscale);
res=modify;
res=VERDICT_MODIFY;
}
if (bReverse)
@ -450,7 +470,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
{
if (params.wssize_cutoff) DLOG("wssize-cutoff not reached (mode %c): %llu/%u\n", params.wssize_cutoff_mode, (unsigned long long)cutoff_get_limit(ctrack,params.wssize_cutoff_mode), params.wssize_cutoff);
tcp_rewrite_winsize(tcphdr, params.wssize, params.wsscale);
res=modify;
res=VERDICT_MODIFY;
}
}
else
@ -508,7 +528,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
if (!rawsend_rep((struct sockaddr *)&dst, desync_fwmark, ifout , pkt1, pkt1_len))
return res;
res = ct_new_postnat_fix(ctrack, ip, drop);
res = ct_new_postnat_fix_tcp(ctrack, ip, ip6hdr, tcphdr);
break;
}
// can do nothing else with SYN packet
@ -663,14 +683,14 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
{
DLOG("modifying Host: => %c%c%c%c:\n", params.hostspell[0], params.hostspell[1], params.hostspell[2], params.hostspell[3])
memcpy(phost + 2, params.hostspell, 4);
res=modify;
res=VERDICT_MODIFY;
}
if (params.domcase)
{
DLOG("mixing domain case\n");
for (p = phost+7; p < (data_payload + len_payload) && *p != '\r' && *p != '\n'; p++)
*p = (((size_t)p) & 1) ? tolower(*p) : toupper(*p);
res=modify;
res=VERDICT_MODIFY;
}
uint8_t *pua;
if (params.hostnospace &&
@ -688,7 +708,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
memmove(pua + 1, pua, phost - pua + 7);
*pua = ' ';
}
res=modify;
res=VERDICT_MODIFY;
}
}
@ -756,7 +776,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , pkt1, pkt1_len))
return res;
// this mode is final, no other options available
return drop;
return VERDICT_DROP;
}
desync_mode = params.desync_mode2;
}
@ -769,23 +789,23 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
{
if (params.desync_retrans)
{
DLOG("dropping original packet to force retransmission. len=%zu len_payload=%zu\n", len_pkt, len_payload)
DLOG("dropping original packet to force retransmission. len=%zu len_payload=%zu\n", *len_pkt, len_payload)
}
else
{
DLOG("reinjecting original packet. len=%zu len_payload=%zu\n", len_pkt, len_payload)
DLOG("reinjecting original packet. len=%zu len_payload=%zu\n", *len_pkt, len_payload)
#ifdef __FreeBSD__
// FreeBSD tend to pass ipv6 frames with wrong checksum
if (res==modify || ip6hdr)
if ((res & VERDICT_MASK)==VERDICT_MODIFY || ip6hdr)
#else
// if original packet was tampered earlier it needs checksum fixed
if (res==modify)
if ((res & VERDICT_MASK)==VERDICT_MODIFY)
#endif
tcp_fix_checksum(tcphdr,len_tcp,ip,ip6hdr);
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , data_pkt, len_pkt))
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , data_pkt, *len_pkt))
return res;
}
return drop;
return VERDICT_DROP;
}
desync_mode = params.desync_mode2;
}
@ -845,7 +865,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
return res;
}
return drop;
return VERDICT_DROP;
}
break;
case DESYNC_SPLIT:
@ -898,17 +918,17 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
return res;
}
return drop;
return VERDICT_DROP;
}
break;
case DESYNC_IPFRAG2:
{
#ifdef __FreeBSD__
// FreeBSD tend to pass ipv6 frames with wrong checksum
if (res==modify || ip6hdr)
if ((res & VERDICT_MASK)==VERDICT_MODIFY || ip6hdr)
#else
// if original packet was tampered earlier it needs checksum fixed
if (res==modify)
if ((res & VERDICT_MASK)==VERDICT_MODIFY)
#endif
tcp_fix_checksum(tcphdr,len_tcp,ip,ip6hdr);
@ -924,14 +944,14 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
if (ip6hdr && (fooling_orig==FOOL_HOPBYHOP || fooling_orig==FOOL_DESTOPT))
{
pkt_orig_len = sizeof(pkt3);
if (!ip6_insert_simple_hdr(fooling_orig==FOOL_HOPBYHOP ? IPPROTO_HOPOPTS : IPPROTO_DSTOPTS, data_pkt, len_pkt, pkt3, &pkt_orig_len))
if (!ip6_insert_simple_hdr(fooling_orig==FOOL_HOPBYHOP ? IPPROTO_HOPOPTS : IPPROTO_DSTOPTS, data_pkt, *len_pkt, pkt3, &pkt_orig_len))
return res;
pkt_orig = pkt3;
}
else
{
pkt_orig = data_pkt;
pkt_orig_len = len_pkt;
pkt_orig_len = *len_pkt;
}
if (!ip_frag(pkt_orig, pkt_orig_len, ipfrag_pos, ident, pkt1, &pkt1_len, pkt2, &pkt2_len))
@ -947,7 +967,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , pkt1, pkt1_len))
return res;
return frag;
return VERDICT_DROP;
}
}
@ -958,9 +978,9 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout,
packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct udphdr *udphdr, uint8_t *data_payload, size_t len_payload)
uint8_t dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t *len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct udphdr *udphdr, uint8_t *data_payload, size_t len_payload)
{
packet_process_result res=pass;
uint8_t res=VERDICT_PASS;
t_ctrack *ctrack=NULL;
bool bReverse=false;
@ -1156,7 +1176,7 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout,
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , pkt1, pkt1_len))
return res;
// this mode is final, no other options available
return ct_new_postnat_fix(ctrack, ip, drop);
return ct_new_postnat_fix_udp(ctrack, ip, ip6hdr, udphdr, len_pkt);
}
desync_mode = params.desync_mode2;
break;
@ -1166,18 +1186,18 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout,
{
if (params.desync_mode2==DESYNC_NONE || !desync_valid_second_stage_udp(params.desync_mode2))
{
DLOG("reinjecting original packet. len=%zu len_payload=%zu\n", len_pkt, len_payload)
DLOG("reinjecting original packet. len=%zu len_payload=%zu\n", *len_pkt, len_payload)
#ifdef __FreeBSD__
// FreeBSD tend to pass ipv6 frames with wrong checksum
if (res==modify || ip6hdr)
if ((res & VERDICT_MASK)==VERDICT_MODIFY || ip6hdr)
#else
// if original packet was tampered earlier it needs checksum fixed
if (res==modify)
if ((res & VERDICT_MASK)==VERDICT_MODIFY)
#endif
udp_fix_checksum(udphdr,sizeof(struct udphdr)+len_payload,ip,ip6hdr);
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , data_pkt, len_pkt))
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , data_pkt, *len_pkt))
return res;
return ct_new_postnat_fix(ctrack, ip, drop);
return ct_new_postnat_fix_udp(ctrack, ip, ip6hdr, udphdr, len_pkt);
}
desync_mode = params.desync_mode2;
}
@ -1194,7 +1214,7 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout,
DLOG("resending original packet with increased by %d length\n", params.udplen_increment);
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , pkt1, pkt1_len))
return res;
return ct_new_postnat_fix(ctrack, ip, drop);
return ct_new_postnat_fix_udp(ctrack, ip, ip6hdr, udphdr, len_pkt);
case DESYNC_TAMPER:
if (IsDhtD1(data_payload,len_payload))
{
@ -1219,7 +1239,7 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout,
DLOG("resending tampered DHT\n");
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , pkt1, pkt1_len))
return res;
return ct_new_postnat_fix(ctrack, ip, drop);
return ct_new_postnat_fix_udp(ctrack, ip, ip6hdr, udphdr, len_pkt);
}
else
{
@ -1231,10 +1251,10 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout,
#ifdef __FreeBSD__
// FreeBSD tend to pass ipv6 frames with wrong checksum
if (res==modify || ip6hdr)
if ((res & VERDICT_MASK)==VERDICT_MODIFY || ip6hdr)
#else
// if original packet was tampered earlier it needs checksum fixed
if (res==modify)
if ((res & VERDICT_MASK)==VERDICT_MODIFY)
#endif
udp_fix_checksum(udphdr,sizeof(struct udphdr)+len_payload,ip,ip6hdr);
@ -1252,14 +1272,14 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout,
if (ip6hdr && (fooling_orig==FOOL_HOPBYHOP || fooling_orig==FOOL_DESTOPT))
{
pkt_orig_len = sizeof(pkt3);
if (!ip6_insert_simple_hdr(fooling_orig==FOOL_HOPBYHOP ? IPPROTO_HOPOPTS : IPPROTO_DSTOPTS, data_pkt, len_pkt, pkt3, &pkt_orig_len))
if (!ip6_insert_simple_hdr(fooling_orig==FOOL_HOPBYHOP ? IPPROTO_HOPOPTS : IPPROTO_DSTOPTS, data_pkt, *len_pkt, pkt3, &pkt_orig_len))
return res;
pkt_orig = pkt3;
}
else
{
pkt_orig = data_pkt;
pkt_orig_len = len_pkt;
pkt_orig_len = *len_pkt;
}
if (!ip_frag(pkt_orig, pkt_orig_len, ipfrag_pos, ident, pkt1, &pkt1_len, pkt2, &pkt2_len))
@ -1275,7 +1295,7 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout,
if (!rawsend((struct sockaddr *)&dst, desync_fwmark, ifout , pkt1, pkt1_len))
return res;
return ct_new_postnat_fix(ctrack, ip, frag);
return ct_new_postnat_fix_udp(ctrack, ip, ip6hdr, udphdr, len_pkt);
}
}

View File

@ -51,5 +51,5 @@ bool desync_valid_second_stage_tcp(enum dpi_desync_mode mode);
bool desync_valid_second_stage_udp(enum dpi_desync_mode mode);
void desync_init(void);
packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct tcphdr *tcphdr, size_t len_tcp, uint8_t *data_payload, size_t len_payload);
packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct udphdr *udphdr, uint8_t *data_payload, size_t len_payload);
uint8_t dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t *len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct tcphdr *tcphdr, size_t len_tcp, uint8_t *data_payload, size_t len_payload);
uint8_t dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t *len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct udphdr *udphdr, uint8_t *data_payload, size_t len_payload);

View File

@ -80,15 +80,15 @@ static void onusr2(int sig)
static packet_process_result processPacketData(uint32_t *mark, const char *ifout, uint8_t *data_pkt, size_t len_pkt)
static uint8_t processPacketData(uint32_t *mark, const char *ifout, uint8_t *data_pkt, size_t *len_pkt)
{
struct ip *ip = NULL;
struct ip6_hdr *ip6hdr = NULL;
struct tcphdr *tcphdr = NULL;
struct udphdr *udphdr = NULL;
size_t len = len_pkt, len_with_th;
size_t len = *len_pkt, len_with_th;
uint8_t *data = data_pkt;
packet_process_result res = pass;
uint8_t res = VERDICT_PASS;
uint8_t proto;
#ifdef __linux__
@ -146,9 +146,9 @@ static packet_process_result processPacketData(uint32_t *mark, const char *ifout
// ipv6 packets were with incorrect checksum
#ifdef __FreeBSD__
// FreeBSD tend to pass ipv6 frames with wrong checksum
if (res==modify || res!=frag && res!=modfrag && ip6hdr)
if (!(res & VERDICT_NOCSUM) && ((res & VERDICT_MASK)==VERDICT_MODIFY || ip6hdr && (res & VERDICT_MASK)==VERDICT_PASS))
#else
if (res==modify)
if (!(res & VERDICT_NOCSUM) && (res & VERDICT_MASK)==VERDICT_MODIFY)
#endif
tcp_fix_checksum(tcphdr,len_with_th,ip,ip6hdr);
}
@ -169,9 +169,9 @@ static packet_process_result processPacketData(uint32_t *mark, const char *ifout
res = dpi_desync_udp_packet(*mark, ifout, data_pkt, len_pkt, ip, ip6hdr, udphdr, data, len);
#ifdef __FreeBSD__
// FreeBSD tend to pass ipv6 frames with wrong checksum
if (res==modify || res!=frag && res!=modfrag && ip6hdr)
if (!(res & VERDICT_NOCSUM) && ((res & VERDICT_MASK)==VERDICT_MODIFY || ip6hdr && (res & VERDICT_MASK)==VERDICT_PASS))
#else
if (res==modify)
if (!(res & VERDICT_NOCSUM) && (res & VERDICT_MASK)==VERDICT_MODIFY)
#endif
udp_fix_checksum(udphdr,len_with_th,ip,ip6hdr);
}
@ -187,8 +187,8 @@ static packet_process_result processPacketData(uint32_t *mark, const char *ifout
#ifdef __linux__
static int nfq_cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfa, void *cookie)
{
int id;
int len;
int id, ilen;
size_t len;
struct nfqnl_msg_packet_hdr *ph;
uint8_t *data;
uint32_t ifidx;
@ -198,7 +198,7 @@ static int nfq_cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_da
id = ph ? ntohl(ph->packet_id) : 0;
uint32_t mark = nfq_get_nfmark(nfa);
len = nfq_get_payload(nfa, &data);
ilen = nfq_get_payload(nfa, &data);
*ifout=0;
if (params.bind_fix4 || params.bind_fix6)
@ -206,21 +206,21 @@ static int nfq_cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_da
ifidx = nfq_get_outdev(nfa);
if (ifidx) if_indextoname(ifidx,ifout);
DLOG("packet: id=%d len=%d ifout=%s(%u)\n", id, len, ifout, ifidx)
DLOG("packet: id=%d len=%d mark=%08X ifout=%s(%u)\n", id, ilen, mark, ifout, ifidx)
}
else
// save some syscalls
DLOG("packet: id=%d len=%d\n", id, len)
if (len >= 0)
DLOG("packet: id=%d len=%d mark=%08X\n", id, ilen, mark)
if (ilen >= 0)
{
switch (processPacketData(&mark, ifout, data, len))
len = ilen;
uint8_t res = processPacketData(&mark, ifout, data, &len);
switch(res & VERDICT_MASK)
{
case modify:
case modfrag:
DLOG("packet: id=%d pass modified\n", id);
return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, len, data);
case drop:
case frag:
case VERDICT_MODIFY:
DLOG("packet: id=%d pass modified. len=%zu\n", id, len);
return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, (uint32_t)len, data);
case VERDICT_DROP:
DLOG("packet: id=%d drop\n", id);
return nfq_set_verdict2(qh, id, NF_DROP, mark, 0, NULL);
}
@ -343,7 +343,6 @@ static int dvt_main(void)
unsigned int id=0;
socklen_t socklen;
ssize_t rd,wr;
packet_process_result ppr;
fd_set fdset;
{
@ -439,18 +438,24 @@ static int dvt_main(void)
else if (rd>0)
{
uint32_t mark=0;
DLOG("packet: id=%u len=%zd\n", id, rd)
ppr = processPacketData(&mark, NULL, buf, rd);
switch (ppr)
uint8_t res;
size_t len = rd;
DLOG("packet: id=%u len=%zu\n", id, len)
res = processPacketData(&mark, NULL, buf, &len);
switch (res & VERDICT_MASK)
{
case pass:
case modify:
DLOG(ppr==pass ? "packet: id=%u reinject unmodified\n" : "packet: id=%u reinject modified\n", id);
wr = sendto(fd[i], buf, rd, 0, (struct sockaddr*)&sa_from, socklen);
case VERDICT_PASS:
case VERDICT_MODIFY:
if ((res & VERDICT_MASK)==VERDICT_PASS)
DLOG("packet: id=%u reinject unmodified\n", id)
else
DLOG("packet: id=%u reinject modified len=%zu\n", id, len)
wr = sendto(fd[i], buf, len, 0, (struct sockaddr*)&sa_from, socklen);
if (wr<0)
perror("reinject sendto");
else if (wr!=rd)
fprintf(stderr,"reinject sendto: not all data was reinjected. received %zd, sent %zd\n", rd, wr);
else if (wr!=len)
fprintf(stderr,"reinject sendto: not all data was reinjected. received %zu, sent %zd\n", len, wr);
break;
default:
DLOG("packet: id=%u drop\n", id);

View File

@ -1,7 +1,7 @@
#pragma once
typedef enum
{
// frag=drop but do not fix checksum
pass = 0, modify, drop, frag, modfrag
} packet_process_result;
#define VERDICT_PASS 0
#define VERDICT_MODIFY 1
#define VERDICT_DROP 2
#define VERDICT_MASK 3
#define VERDICT_NOCSUM 4