Commit 8dcfc9e3 authored by Rusty Russell's avatar Rusty Russell Committed by David S. Miller

[NETFILTER]: Linearize iptables matches.

Adjusts the IPTables matches to handle non-linear packets.
Untested: ipt_ah and ipt_esp.
parent 4b5fb65e
...@@ -35,14 +35,16 @@ match(const struct sk_buff *skb, ...@@ -35,14 +35,16 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ahhdr *ah = hdr; struct ahhdr ah;
const struct ipt_ah *ahinfo = matchinfo; const struct ipt_ah *ahinfo = matchinfo;
if (offset == 0 && datalen < sizeof(struct ahhdr)) { /* Must not be a fragment. */
if (offset)
return 0;
if (skb_copy_bits(skb, skb->nh.iph->ihl*4, &ah, sizeof(ah)) < 0) {
/* We've been asked to examine this packet, and we /* We've been asked to examine this packet, and we
can't. Hence, no choice but to drop. */ can't. Hence, no choice but to drop. */
duprintf("Dropping evil AH tinygram.\n"); duprintf("Dropping evil AH tinygram.\n");
...@@ -50,10 +52,8 @@ match(const struct sk_buff *skb, ...@@ -50,10 +52,8 @@ match(const struct sk_buff *skb,
return 0; return 0;
} }
/* Must not be a fragment. */ return spi_match(ahinfo->spis[0], ahinfo->spis[1],
return !offset ntohl(ah.spi),
&& spi_match(ahinfo->spis[0], ahinfo->spis[1],
ntohl(ah->spi),
!!(ahinfo->invflags & IPT_AH_INV_SPI)); !!(ahinfo->invflags & IPT_AH_INV_SPI));
} }
......
...@@ -14,8 +14,6 @@ match(const struct sk_buff *skb, ...@@ -14,8 +14,6 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_conntrack_info *sinfo = matchinfo; const struct ipt_conntrack_info *sinfo = matchinfo;
......
...@@ -19,8 +19,7 @@ MODULE_LICENSE("GPL"); ...@@ -19,8 +19,7 @@ MODULE_LICENSE("GPL");
static int match(const struct sk_buff *skb, const struct net_device *in, static int match(const struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, const void *matchinfo, const struct net_device *out, const void *matchinfo,
int offset, const void *hdr, u_int16_t datalen, int offset, int *hotdrop)
int *hotdrop)
{ {
const struct ipt_dscp_info *info = matchinfo; const struct ipt_dscp_info *info = matchinfo;
const struct iphdr *iph = skb->nh.iph; const struct iphdr *iph = skb->nh.iph;
......
...@@ -19,34 +19,40 @@ MODULE_DESCRIPTION("IP tables ECN matching module"); ...@@ -19,34 +19,40 @@ MODULE_DESCRIPTION("IP tables ECN matching module");
MODULE_LICENSE("GPL"); MODULE_LICENSE("GPL");
static inline int match_ip(const struct sk_buff *skb, static inline int match_ip(const struct sk_buff *skb,
const struct iphdr *iph,
const struct ipt_ecn_info *einfo) const struct ipt_ecn_info *einfo)
{ {
return ((iph->tos&IPT_ECN_IP_MASK) == einfo->ip_ect); return ((skb->nh.iph->tos&IPT_ECN_IP_MASK) == einfo->ip_ect);
} }
static inline int match_tcp(const struct sk_buff *skb, static inline int match_tcp(const struct sk_buff *skb,
const struct iphdr *iph, const struct ipt_ecn_info *einfo,
const struct ipt_ecn_info *einfo) int *hotdrop)
{ {
struct tcphdr *tcph = (void *)iph + iph->ihl*4; struct tcphdr tcph;
/* In practice, TCP match does this, so can't fail. But let's
be good citizens. */
if (skb_copy_bits(skb, skb->nh.iph->ihl*4, &tcph, sizeof(tcph)) < 0) {
*hotdrop = 0;
return 0;
}
if (einfo->operation & IPT_ECN_OP_MATCH_ECE) { if (einfo->operation & IPT_ECN_OP_MATCH_ECE) {
if (einfo->invert & IPT_ECN_OP_MATCH_ECE) { if (einfo->invert & IPT_ECN_OP_MATCH_ECE) {
if (tcph->ece == 1) if (tcph.ece == 1)
return 0; return 0;
} else { } else {
if (tcph->ece == 0) if (tcph.ece == 0)
return 0; return 0;
} }
} }
if (einfo->operation & IPT_ECN_OP_MATCH_CWR) { if (einfo->operation & IPT_ECN_OP_MATCH_CWR) {
if (einfo->invert & IPT_ECN_OP_MATCH_CWR) { if (einfo->invert & IPT_ECN_OP_MATCH_CWR) {
if (tcph->cwr == 1) if (tcph.cwr == 1)
return 0; return 0;
} else { } else {
if (tcph->cwr == 0) if (tcph.cwr == 0)
return 0; return 0;
} }
} }
...@@ -56,20 +62,18 @@ static inline int match_tcp(const struct sk_buff *skb, ...@@ -56,20 +62,18 @@ static inline int match_tcp(const struct sk_buff *skb,
static int match(const struct sk_buff *skb, const struct net_device *in, static int match(const struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, const void *matchinfo, const struct net_device *out, const void *matchinfo,
int offset, const void *hdr, u_int16_t datalen, int offset, int *hotdrop)
int *hotdrop)
{ {
const struct ipt_ecn_info *info = matchinfo; const struct ipt_ecn_info *info = matchinfo;
const struct iphdr *iph = skb->nh.iph;
if (info->operation & IPT_ECN_OP_MATCH_IP) if (info->operation & IPT_ECN_OP_MATCH_IP)
if (!match_ip(skb, iph, info)) if (!match_ip(skb, info))
return 0; return 0;
if (info->operation & (IPT_ECN_OP_MATCH_ECE|IPT_ECN_OP_MATCH_CWR)) { if (info->operation & (IPT_ECN_OP_MATCH_ECE|IPT_ECN_OP_MATCH_CWR)) {
if (iph->protocol != IPPROTO_TCP) if (skb->nh.iph->protocol != IPPROTO_TCP)
return 0; return 0;
if (!match_tcp(skb, iph, info)) if (!match_tcp(skb, info, hotdrop))
return 0; return 0;
} }
......
...@@ -35,14 +35,16 @@ match(const struct sk_buff *skb, ...@@ -35,14 +35,16 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct esphdr *esp = hdr; struct esphdr esp;
const struct ipt_esp *espinfo = matchinfo; const struct ipt_esp *espinfo = matchinfo;
if (offset == 0 && datalen < sizeof(struct esphdr)) { /* Must not be a fragment. */
if (offset)
return 0;
if (skb_copy_bits(skb, skb->nh.iph->ihl*4, &esp, sizeof(esp)) < 0) {
/* We've been asked to examine this packet, and we /* We've been asked to examine this packet, and we
can't. Hence, no choice but to drop. */ can't. Hence, no choice but to drop. */
duprintf("Dropping evil ESP tinygram.\n"); duprintf("Dropping evil ESP tinygram.\n");
...@@ -50,10 +52,8 @@ match(const struct sk_buff *skb, ...@@ -50,10 +52,8 @@ match(const struct sk_buff *skb,
return 0; return 0;
} }
/* Must not be a fragment. */ return spi_match(espinfo->spis[0], espinfo->spis[1],
return !offset ntohl(esp.spi),
&& spi_match(espinfo->spis[0], espinfo->spis[1],
ntohl(esp->spi),
!!(espinfo->invflags & IPT_ESP_INV_SPI)); !!(espinfo->invflags & IPT_ESP_INV_SPI));
} }
......
...@@ -28,8 +28,6 @@ match(const struct sk_buff *skb, ...@@ -28,8 +28,6 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_helper_info *info = matchinfo; const struct ipt_helper_info *info = matchinfo;
......
...@@ -15,8 +15,6 @@ match(const struct sk_buff *skb, ...@@ -15,8 +15,6 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_length_info *info = matchinfo; const struct ipt_length_info *info = matchinfo;
......
...@@ -47,8 +47,6 @@ ipt_limit_match(const struct sk_buff *skb, ...@@ -47,8 +47,6 @@ ipt_limit_match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
struct ipt_rateinfo *r = ((struct ipt_rateinfo *)matchinfo)->master; struct ipt_rateinfo *r = ((struct ipt_rateinfo *)matchinfo)->master;
......
...@@ -12,8 +12,6 @@ match(const struct sk_buff *skb, ...@@ -12,8 +12,6 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_mac_info *info = matchinfo; const struct ipt_mac_info *info = matchinfo;
......
...@@ -11,8 +11,6 @@ match(const struct sk_buff *skb, ...@@ -11,8 +11,6 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_mark_info *info = matchinfo; const struct ipt_mark_info *info = matchinfo;
......
...@@ -39,15 +39,18 @@ match(const struct sk_buff *skb, ...@@ -39,15 +39,18 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct udphdr *udp = hdr; u16 ports[2];
const struct ipt_multiport *multiinfo = matchinfo; const struct ipt_multiport *multiinfo = matchinfo;
/* Must be big enough to read ports. */ /* Must not be a fragment. */
if (offset == 0 && datalen < sizeof(struct udphdr)) { if (offset)
return 0;
/* Must be big enough to read ports (both UDP and TCP have
them at the start). */
if (skb_copy_bits(skb, skb->nh.iph->ihl*4, ports, sizeof(ports)) < 0) {
/* We've been asked to examine this packet, and we /* We've been asked to examine this packet, and we
can't. Hence, no choice but to drop. */ can't. Hence, no choice but to drop. */
duprintf("ipt_multiport:" duprintf("ipt_multiport:"
...@@ -56,11 +59,9 @@ match(const struct sk_buff *skb, ...@@ -56,11 +59,9 @@ match(const struct sk_buff *skb,
return 0; return 0;
} }
/* Must not be a fragment. */ return ports_match(multiinfo->ports,
return !offset
&& ports_match(multiinfo->ports,
multiinfo->flags, multiinfo->count, multiinfo->flags, multiinfo->count,
ntohs(udp->source), ntohs(udp->dest)); ntohs(ports[0]), ntohs(ports[1]));
} }
/* Called when user tries to insert an entry of this type. */ /* Called when user tries to insert an entry of this type. */
......
...@@ -115,8 +115,6 @@ match(const struct sk_buff *skb, ...@@ -115,8 +115,6 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_owner_info *info = matchinfo; const struct ipt_owner_info *info = matchinfo;
...@@ -170,8 +168,11 @@ checkentry(const char *tablename, ...@@ -170,8 +168,11 @@ checkentry(const char *tablename,
return 0; return 0;
} }
if (matchsize != IPT_ALIGN(sizeof(struct ipt_owner_info))) if (matchsize != IPT_ALIGN(sizeof(struct ipt_owner_info))) {
printk("Matchsize %u != %u\n", matchsize,
IPT_ALIGN(sizeof(struct ipt_owner_info)));
return 0; return 0;
}
return 1; return 1;
} }
......
...@@ -13,8 +13,6 @@ static int match(const struct sk_buff *skb, ...@@ -13,8 +13,6 @@ static int match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_pkttype_info *info = matchinfo; const struct ipt_pkttype_info *info = matchinfo;
......
...@@ -13,8 +13,6 @@ match(const struct sk_buff *skb, ...@@ -13,8 +13,6 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_state_info *sinfo = matchinfo; const struct ipt_state_info *sinfo = matchinfo;
......
...@@ -11,24 +11,32 @@ ...@@ -11,24 +11,32 @@
/* Returns 1 if the mss option is set and matched by the range, 0 otherwise */ /* Returns 1 if the mss option is set and matched by the range, 0 otherwise */
static inline int static inline int
mssoption_match(u_int16_t min, u_int16_t max, mssoption_match(u_int16_t min, u_int16_t max,
const struct tcphdr *tcp, const struct sk_buff *skb,
u_int16_t datalen,
int invert, int invert,
int *hotdrop) int *hotdrop)
{ {
unsigned int i; struct tcphdr tcph;
const u_int8_t *opt = (u_int8_t *)tcp; /* tcp.doff is only 4 bits, ie. max 15 * 4 bytes */
u8 opt[15 * 4 - sizeof(tcph)];
unsigned int i, optlen;
/* If we don't have the whole header, drop packet. */ /* If we don't have the whole header, drop packet. */
if (tcp->doff * 4 > datalen) { if (skb_copy_bits(skb, skb->nh.iph->ihl*4, &tcph, sizeof(tcph)) < 0)
*hotdrop = 1; goto dropit;
return 0;
} /* Malformed. */
if (tcph.doff*4 < sizeof(tcph))
for (i = sizeof(struct tcphdr); i < tcp->doff * 4; ) { goto dropit;
if ((opt[i] == TCPOPT_MSS)
&& ((tcp->doff * 4 - i) >= TCPOLEN_MSS) optlen = tcph.doff*4 - sizeof(tcph);
&& (opt[i+1] == TCPOLEN_MSS)) { /* Truncated options. */
if (skb_copy_bits(skb, skb->nh.iph->ihl*4+sizeof(tcph), opt, optlen)<0)
goto dropit;
for (i = 0; i < optlen; ) {
if (opt[i] == TCPOPT_MSS
&& (optlen - i) >= TCPOLEN_MSS
&& opt[i+1] == TCPOLEN_MSS) {
u_int16_t mssval; u_int16_t mssval;
mssval = (opt[i+2] << 8) | opt[i+3]; mssval = (opt[i+2] << 8) | opt[i+3];
...@@ -38,8 +46,11 @@ mssoption_match(u_int16_t min, u_int16_t max, ...@@ -38,8 +46,11 @@ mssoption_match(u_int16_t min, u_int16_t max,
if (opt[i] < 2) i++; if (opt[i] < 2) i++;
else i += opt[i+1]?:1; else i += opt[i+1]?:1;
} }
return invert; return invert;
dropit:
*hotdrop = 1;
return 0;
} }
static int static int
...@@ -48,15 +59,11 @@ match(const struct sk_buff *skb, ...@@ -48,15 +59,11 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_tcpmss_match_info *info = matchinfo; const struct ipt_tcpmss_match_info *info = matchinfo;
const struct tcphdr *tcph = (void *)skb->nh.iph + skb->nh.iph->ihl*4;
return mssoption_match(info->mss_min, info->mss_max, tcph, return mssoption_match(info->mss_min, info->mss_max, skb,
skb->len - skb->nh.iph->ihl*4,
info->invert, hotdrop); info->invert, hotdrop);
} }
......
...@@ -11,14 +11,11 @@ match(const struct sk_buff *skb, ...@@ -11,14 +11,11 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
const struct ipt_tos_info *info = matchinfo; const struct ipt_tos_info *info = matchinfo;
const struct iphdr *iph = skb->nh.iph;
return (iph->tos == info->tos) ^ info->invert; return (skb->nh.iph->tos == info->tos) ^ info->invert;
} }
static int static int
......
...@@ -19,24 +19,22 @@ MODULE_LICENSE("GPL"); ...@@ -19,24 +19,22 @@ MODULE_LICENSE("GPL");
static int match(const struct sk_buff *skb, const struct net_device *in, static int match(const struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, const void *matchinfo, const struct net_device *out, const void *matchinfo,
int offset, const void *hdr, u_int16_t datalen, int offset, int *hotdrop)
int *hotdrop)
{ {
const struct ipt_ttl_info *info = matchinfo; const struct ipt_ttl_info *info = matchinfo;
const struct iphdr *iph = skb->nh.iph;
switch (info->mode) { switch (info->mode) {
case IPT_TTL_EQ: case IPT_TTL_EQ:
return (iph->ttl == info->ttl); return (skb->nh.iph->ttl == info->ttl);
break; break;
case IPT_TTL_NE: case IPT_TTL_NE:
return (!(iph->ttl == info->ttl)); return (!(skb->nh.iph->ttl == info->ttl));
break; break;
case IPT_TTL_LT: case IPT_TTL_LT:
return (iph->ttl < info->ttl); return (skb->nh.iph->ttl < info->ttl);
break; break;
case IPT_TTL_GT: case IPT_TTL_GT:
return (iph->ttl > info->ttl); return (skb->nh.iph->ttl > info->ttl);
break; break;
default: default:
printk(KERN_WARNING "ipt_ttl: unknown mode %d\n", printk(KERN_WARNING "ipt_ttl: unknown mode %d\n",
......
...@@ -31,16 +31,17 @@ struct icmp_info ...@@ -31,16 +31,17 @@ struct icmp_info
}; };
static int static int
check_ip(struct iphdr *iph, size_t length, int embedded); check_ip(const struct sk_buff *skb, unsigned int offset);
/* ICMP-specific checks. */ /* ICMP-specific checks. */
static int static int
check_icmp(const struct icmphdr *icmph, check_icmp(const struct sk_buff *skb,
u_int16_t datalen,
unsigned int offset, unsigned int offset,
unsigned int fragoff,
int more_frags, int more_frags,
int embedded) int embedded)
{ {
struct icmphdr icmph;
static struct icmp_info info[] static struct icmp_info info[]
= { [ICMP_ECHOREPLY] = { [ICMP_ECHOREPLY]
= { 8, 65536, ICMP_NOT_ERROR, 0, 0 }, = { 8, 65536, ICMP_NOT_ERROR, 0, 0 },
...@@ -76,92 +77,95 @@ check_icmp(const struct icmphdr *icmph, ...@@ -76,92 +77,95 @@ check_icmp(const struct icmphdr *icmph,
= { 12, 12, ICMP_NOT_ERROR, 0, 0 } }; = { 12, 12, ICMP_NOT_ERROR, 0, 0 } };
/* Can't do anything if it's a fragment. */ /* Can't do anything if it's a fragment. */
if (offset) if (fragoff)
return 1; return 1;
/* Must cover type and code. */ /* CHECK: Must have whole header.. */
if (datalen < 2) { if (skb_copy_bits(skb, offset, &icmph, sizeof(icmph)) < 0) {
limpk("ICMP len=%u too short\n", datalen); limpk("ICMP len=%u too short\n", skb->len - offset);
return 0; return 0;
} }
/* If not embedded. */ /* If not embedded in an ICMP error already. */
if (!embedded) { if (!embedded) {
/* Bad checksum? Don't print, just ignore. */
if (!more_frags
&& ip_compute_csum((unsigned char *) icmph, datalen) != 0)
return 0;
/* CHECK: Truncated ICMP (even if first fragment). */ /* CHECK: Truncated ICMP (even if first fragment). */
if (icmph->type < sizeof(info)/sizeof(struct icmp_info) if (icmph.type < sizeof(info)/sizeof(struct icmp_info)
&& info[icmph->type].min_len != 0 && info[icmph.type].min_len != 0
&& datalen < info[icmph->type].min_len) { && skb->len - offset < info[icmph.type].min_len) {
limpk("ICMP type %u len %u too short\n", limpk("ICMP type %u len %u too short\n",
icmph->type, datalen); icmph.type, skb->len - offset);
return 0; return 0;
} }
/* CHECK: Check within known error ICMPs. */ /* CHECK: Check within known error ICMPs. */
if (icmph->type < sizeof(info)/sizeof(struct icmp_info) if (icmph.type < sizeof(info)/sizeof(struct icmp_info)
&& info[icmph->type].err == ICMP_IS_ERROR) { && info[icmph.type].err == ICMP_IS_ERROR) {
/* Max IP header size = 60 */
char inner[60 + 8];
struct iphdr *inner_ip = (struct iphdr *)inner;
/* CHECK: Embedded packet must be at least /* CHECK: Embedded packet must be at least
length of iph + 8 bytes. */ length of iph + 8 bytes. */
struct iphdr *inner = (void *)icmph + 8; if (skb_copy_bits(skb, offset + sizeof(icmph),
inner, sizeof(struct iphdr)+8) < 0) {
/* datalen > 8 since all ICMP_IS_ERROR types
have min length > 8 */
if (datalen - 8 < sizeof(struct iphdr)) {
limpk("ICMP error internal way too short\n"); limpk("ICMP error internal way too short\n");
return 0; return 0;
} }
if (datalen - 8 < inner->ihl*4 + 8) {
/* iphhdr may actually be longer: still need 8
actual protocol bytes. */
if (offset + sizeof(icmph) + inner_ip->ihl*4 + 8
> skb->len) {
limpk("ICMP error internal too short\n"); limpk("ICMP error internal too short\n");
return 0; return 0;
} }
if (!check_ip(inner, datalen - 8, 1)) if (!check_ip(skb, offset + sizeof(icmph)))
return 0; return 0;
} }
} else { } else {
/* CHECK: Can't embed ICMP unless known non-error. */ /* CHECK: Can't embed ICMP unless known non-error. */
if (icmph->type >= sizeof(info)/sizeof(struct icmp_info) if (icmph.type >= sizeof(info)/sizeof(struct icmp_info)
|| info[icmph->type].err != ICMP_NOT_ERROR) { || info[icmph.type].err != ICMP_NOT_ERROR) {
limpk("ICMP type %u not embeddable\n", limpk("ICMP type %u not embeddable\n",
icmph->type); icmph.type);
return 0; return 0;
} }
} }
/* CHECK: Invalid ICMP codes. */ /* CHECK: Invalid ICMP codes. */
if (icmph->type < sizeof(info)/sizeof(struct icmp_info) if (icmph.type < sizeof(info)/sizeof(struct icmp_info)
&& (icmph->code < info[icmph->type].min_code && (icmph.code < info[icmph.type].min_code
|| icmph->code > info[icmph->type].max_code)) { || icmph.code > info[icmph.type].max_code)) {
limpk("ICMP type=%u code=%u\n", limpk("ICMP type=%u code=%u\n",
icmph->type, icmph->code); icmph.type, icmph.code);
return 0; return 0;
} }
/* CHECK: Above maximum length. */ /* CHECK: Above maximum length. */
if (icmph->type < sizeof(info)/sizeof(struct icmp_info) if (icmph.type < sizeof(info)/sizeof(struct icmp_info)
&& info[icmph->type].max_len != 0 && info[icmph.type].max_len != 0
&& datalen > info[icmph->type].max_len) { && skb->len - offset > info[icmph.type].max_len) {
limpk("ICMP type=%u too long: %u bytes\n", limpk("ICMP type=%u too long: %u bytes\n",
icmph->type, datalen); icmph.type, skb->len - offset);
return 0; return 0;
} }
switch (icmph->type) { switch (icmph.type) {
case ICMP_PARAMETERPROB: { case ICMP_PARAMETERPROB: {
/* CHECK: Problem param must be within error packet's /* CHECK: Problem param must be within error packet's
* IP header. */ * IP header. */
struct iphdr *iph = (void *)icmph + 8; u_int32_t arg = ntohl(icmph.un.gateway);
u_int32_t arg = ntohl(icmph->un.gateway);
if (icmph->code == 0) { if (icmph.code == 0) {
/* We've already made sure it's long enough. */
struct iphdr iph;
skb_copy_bits(skb, offset + sizeof(icmph), &iph,
sizeof(iph));
/* Code 0 means that upper 8 bits is pointer /* Code 0 means that upper 8 bits is pointer
to problem. */ to problem. */
if ((arg >> 24) >= iph->ihl*4) { if ((arg >> 24) >= iph.ihl*4) {
limpk("ICMP PARAMETERPROB ptr = %u\n", limpk("ICMP PARAMETERPROB ptr = %u\n",
ntohl(icmph->un.gateway) >> 24); ntohl(icmph.un.gateway) >> 24);
return 0; return 0;
} }
arg &= 0x00FFFFFF; arg &= 0x00FFFFFF;
...@@ -179,9 +183,9 @@ check_icmp(const struct icmphdr *icmph, ...@@ -179,9 +183,9 @@ check_icmp(const struct icmphdr *icmph,
case ICMP_TIME_EXCEEDED: case ICMP_TIME_EXCEEDED:
case ICMP_SOURCE_QUENCH: case ICMP_SOURCE_QUENCH:
/* CHECK: Unused must be zero. */ /* CHECK: Unused must be zero. */
if (icmph->un.gateway != 0) { if (icmph.un.gateway != 0) {
limpk("ICMP type=%u unused = %u\n", limpk("ICMP type=%u unused = %u\n",
icmph->type, ntohl(icmph->un.gateway)); icmph.type, ntohl(icmph.un.gateway));
return 0; return 0;
} }
break; break;
...@@ -192,32 +196,26 @@ check_icmp(const struct icmphdr *icmph, ...@@ -192,32 +196,26 @@ check_icmp(const struct icmphdr *icmph,
/* UDP-specific checks. */ /* UDP-specific checks. */
static int static int
check_udp(const struct iphdr *iph, check_udp(const struct sk_buff *skb,
const struct udphdr *udph,
u_int16_t datalen,
unsigned int offset, unsigned int offset,
unsigned int fragoff,
int more_frags, int more_frags,
int embedded) int embedded)
{ {
struct udphdr udph;
/* Can't do anything if it's a fragment. */ /* Can't do anything if it's a fragment. */
if (offset) if (fragoff)
return 1; return 1;
/* CHECK: Must cover UDP header. */ /* CHECK: Must cover UDP header. */
if (datalen < sizeof(struct udphdr)) { if (skb_copy_bits(skb, offset, &udph, sizeof(udph)) < 0) {
limpk("UDP len=%u too short\n", datalen); limpk("UDP len=%u too short\n", skb->len - offset);
return 0; return 0;
} }
/* Bad checksum? Don't print, just say it's unclean. */
/* FIXME: SRC ROUTE packets won't match checksum --RR */
if (!more_frags && !embedded && udph->check
&& csum_tcpudp_magic(iph->saddr, iph->daddr, datalen, IPPROTO_UDP,
csum_partial((char *)udph, datalen, 0)) != 0)
return 0;
/* CHECK: Destination port can't be zero. */ /* CHECK: Destination port can't be zero. */
if (!udph->dest) { if (!udph.dest) {
limpk("UDP zero destination port\n"); limpk("UDP zero destination port\n");
return 0; return 0;
} }
...@@ -225,24 +223,24 @@ check_udp(const struct iphdr *iph, ...@@ -225,24 +223,24 @@ check_udp(const struct iphdr *iph,
if (!more_frags) { if (!more_frags) {
if (!embedded) { if (!embedded) {
/* CHECK: UDP length must match. */ /* CHECK: UDP length must match. */
if (ntohs(udph->len) != datalen) { if (ntohs(udph.len) != skb->len - offset) {
limpk("UDP len too short %u vs %u\n", limpk("UDP len too short %u vs %u\n",
ntohs(udph->len), datalen); ntohs(udph.len), skb->len - offset);
return 0; return 0;
} }
} else { } else {
/* CHECK: UDP length be >= this truncated pkt. */ /* CHECK: UDP length be >= this truncated pkt. */
if (ntohs(udph->len) < datalen) { if (ntohs(udph.len) < skb->len - offset) {
limpk("UDP len too long %u vs %u\n", limpk("UDP len too long %u vs %u\n",
ntohs(udph->len), datalen); ntohs(udph.len), skb->len - offset);
return 0; return 0;
} }
} }
} else { } else {
/* CHECK: UDP length must be > this frag's length. */ /* CHECK: UDP length must be > this frag's length. */
if (ntohs(udph->len) <= datalen) { if (ntohs(udph.len) <= skb->len - offset) {
limpk("UDP fragment len too short %u vs %u\n", limpk("UDP fragment len too short %u vs %u\n",
ntohs(udph->len), datalen); ntohs(udph.len), skb->len - offset);
return 0; return 0;
} }
} }
...@@ -250,104 +248,104 @@ check_udp(const struct iphdr *iph, ...@@ -250,104 +248,104 @@ check_udp(const struct iphdr *iph,
return 1; return 1;
} }
#define TH_FIN 0x01
#define TH_SYN 0x02
#define TH_RST 0x04
#define TH_PUSH 0x08
#define TH_ACK 0x10
#define TH_URG 0x20
#define TH_ECE 0x40
#define TH_CWR 0x80
/* TCP-specific checks. */ /* TCP-specific checks. */
static int static int
check_tcp(const struct iphdr *iph, check_tcp(const struct sk_buff *skb,
const struct tcphdr *tcph,
u_int16_t datalen,
unsigned int offset, unsigned int offset,
unsigned int fragoff,
int more_frags, int more_frags,
int embedded) int embedded)
{ {
u_int8_t *opt = (u_int8_t *)tcph; struct tcphdr tcph;
u_int8_t *endhdr = (u_int8_t *)tcph + tcph->doff * 4; unsigned char opt[15 * 4 - sizeof(struct tcphdr)];
u_int8_t tcpflags; u32 tcpflags;
int end_of_options = 0; int end_of_options = 0;
size_t i; unsigned int i, optlen;
/* CHECK: Can't have offset=1: used to override TCP syn-checks. */ /* CHECK: Can't have offset=1: used to override TCP syn-checks. */
/* In fact, this is caught below (offset < 516). */ /* In fact, this is caught below (offset < 516). */
/* Can't do anything if it's a fragment. */ /* Can't do anything if it's a fragment. */
if (offset) if (fragoff)
return 1; return 1;
/* CHECK: Smaller than minimal TCP hdr. */ /* CHECK: Smaller than minimal TCP hdr. */
if (datalen < sizeof(struct tcphdr)) { if (skb_copy_bits(skb, offset, &tcph, sizeof(tcph)) < 0) {
u16 ports[2];
if (!embedded) { if (!embedded) {
limpk("Packet length %u < TCP header.\n", datalen); limpk("Packet length %u < TCP header.\n",
skb->len - offset);
return 0; return 0;
} }
/* Must have ports available (datalen >= 8), from /* Must have ports available (datalen >= 8), from
check_icmp which set embedded = 1 */ check_icmp which set embedded = 1 */
/* CHECK: TCP ports inside ICMP error */ /* CHECK: TCP ports inside ICMP error */
if (!tcph->source || !tcph->dest) { skb_copy_bits(skb, offset, ports, sizeof(ports));
if (!ports[0] || !ports[1]) {
limpk("Zero TCP ports %u/%u.\n", limpk("Zero TCP ports %u/%u.\n",
htons(tcph->source), htons(tcph->dest)); htons(ports[0]), htons(ports[1]));
return 0; return 0;
} }
return 1; return 1;
} }
/* CHECK: Smaller than actual TCP hdr. */ /* CHECK: TCP header claims tiny size. */
if (datalen < tcph->doff * 4) { if (tcph.doff * 4 < sizeof(tcph)) {
limpk("TCP header claims tiny size %u\n", tcph.doff * 4);
return 0;
}
/* CHECK: Packet smaller than actual TCP hdr. */
optlen = tcph.doff*4 - sizeof(tcph);
if (skb_copy_bits(skb, offset + sizeof(tcph), opt, optlen) < 0) {
if (!embedded) { if (!embedded) {
limpk("Packet length %u < actual TCP header.\n", limpk("Packet length %u < actual TCP header.\n",
datalen); skb->len - offset);
return 0; return 0;
} else } else
return 1; return 1;
} }
/* Bad checksum? Don't print, just say it's unclean. */
/* FIXME: SRC ROUTE packets won't match checksum --RR */
if (!more_frags && !embedded
&& csum_tcpudp_magic(iph->saddr, iph->daddr, datalen, IPPROTO_TCP,
csum_partial((char *)tcph, datalen, 0)) != 0)
return 0;
/* CHECK: TCP ports non-zero */ /* CHECK: TCP ports non-zero */
if (!tcph->source || !tcph->dest) { if (!tcph.source || !tcph.dest) {
limpk("Zero TCP ports %u/%u.\n", limpk("Zero TCP ports %u/%u.\n",
htons(tcph->source), htons(tcph->dest)); htons(tcph.source), htons(tcph.dest));
return 0; return 0;
} }
tcpflags = tcp_flag_word(&tcph);
/* CHECK: TCP reserved bits zero. */ /* CHECK: TCP reserved bits zero. */
if(tcp_flag_word(tcph) & TCP_RESERVED_BITS) { if (tcpflags & TCP_RESERVED_BITS) {
limpk("TCP reserved bits not zero\n"); limpk("TCP reserved bits not zero\n");
return 0; return 0;
} }
tcpflags &= ~(TCP_DATA_OFFSET | TCP_FLAG_CWR | TCP_FLAG_ECE
| __constant_htonl(0x0000FFFF));
/* CHECK: TCP flags. */ /* CHECK: TCP flags. */
tcpflags = (((u_int8_t *)tcph)[13] & ~(TH_ECE|TH_CWR)); if (tcpflags != TCP_FLAG_SYN
if (tcpflags != TH_SYN && tcpflags != (TCP_FLAG_SYN|TCP_FLAG_ACK)
&& tcpflags != (TH_SYN|TH_ACK) && tcpflags != TCP_FLAG_RST
&& tcpflags != TH_RST && tcpflags != (TCP_FLAG_RST|TCP_FLAG_ACK)
&& tcpflags != (TH_RST|TH_ACK) && tcpflags != (TCP_FLAG_RST|TCP_FLAG_ACK|TCP_FLAG_PSH)
&& tcpflags != (TH_RST|TH_ACK|TH_PUSH) && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK)
&& tcpflags != (TH_FIN|TH_ACK) && tcpflags != TCP_FLAG_ACK
&& tcpflags != TH_ACK && tcpflags != (TCP_FLAG_ACK|TCP_FLAG_PSH)
&& tcpflags != (TH_ACK|TH_PUSH) && tcpflags != (TCP_FLAG_ACK|TCP_FLAG_URG)
&& tcpflags != (TH_ACK|TH_URG) && tcpflags != (TCP_FLAG_ACK|TCP_FLAG_URG|TCP_FLAG_PSH)
&& tcpflags != (TH_ACK|TH_URG|TH_PUSH) && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK|TCP_FLAG_PSH)
&& tcpflags != (TH_FIN|TH_ACK|TH_PUSH) && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK|TCP_FLAG_URG)
&& tcpflags != (TH_FIN|TH_ACK|TH_URG) && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK|TCP_FLAG_URG
&& tcpflags != (TH_FIN|TH_ACK|TH_URG|TH_PUSH)) { |TCP_FLAG_PSH)) {
limpk("TCP flags bad: %u\n", tcpflags); limpk("TCP flags bad: 0x%04X\n", ntohl(tcpflags) >> 16);
return 0; return 0;
} }
for (i = sizeof(struct tcphdr); i < tcph->doff * 4; ) { for (i = 0; i < optlen; ) {
switch (opt[i]) { switch (opt[i]) {
case 0: case 0:
end_of_options = 1; end_of_options = 1;
...@@ -364,7 +362,7 @@ check_tcp(const struct iphdr *iph, ...@@ -364,7 +362,7 @@ check_tcp(const struct iphdr *iph,
return 0; return 0;
} }
/* CHECK: options at tail. */ /* CHECK: options at tail. */
else if (i+1 >= tcph->doff * 4) { else if (i+1 >= optlen) {
limpk("TCP option %u at tail\n", limpk("TCP option %u at tail\n",
opt[i]); opt[i]);
return 0; return 0;
...@@ -376,8 +374,8 @@ check_tcp(const struct iphdr *iph, ...@@ -376,8 +374,8 @@ check_tcp(const struct iphdr *iph,
return 0; return 0;
} }
/* CHECK: oversize options. */ /* CHECK: oversize options. */
else if (&opt[i] + opt[i+1] > endhdr) { else if (i + opt[i+1] > optlen) {
limpk("TCP option %u at %Zu too long\n", limpk("TCP option %u at %u too long\n",
(unsigned int) opt[i], i); (unsigned int) opt[i], i);
return 0; return 0;
} }
...@@ -392,34 +390,44 @@ check_tcp(const struct iphdr *iph, ...@@ -392,34 +390,44 @@ check_tcp(const struct iphdr *iph,
/* Returns 1 if ok */ /* Returns 1 if ok */
/* Standard IP checks. */ /* Standard IP checks. */
static int static int
check_ip(struct iphdr *iph, size_t length, int embedded) check_ip(const struct sk_buff *skb, unsigned int offset)
{ {
u_int8_t *opt = (u_int8_t *)iph;
u_int8_t *endhdr = (u_int8_t *)iph + iph->ihl * 4;
int end_of_options = 0; int end_of_options = 0;
void *protoh; unsigned int datalen, optlen;
size_t datalen;
unsigned int i; unsigned int i;
unsigned int offset; unsigned int fragoff;
struct iphdr iph;
unsigned char opt[15 * 4 - sizeof(struct iphdr)];
int embedded = offset;
/* Should only happen for local outgoing raw-socket packets. */ /* Should only happen for local outgoing raw-socket packets. */
/* CHECK: length >= ip header. */ /* CHECK: length >= ip header. */
if (length < sizeof(struct iphdr) || length < iph->ihl * 4) { if (skb_copy_bits(skb, offset, &iph, sizeof(iph)) < 0) {
limpk("Packet length %Zu < IP header.\n", length); limpk("Packet length %u < IP header.\n", skb->len - offset);
return 0;
}
if (iph.ihl * 4 < sizeof(iph)) {
limpk("IP len %u < minimum IP header.\n", iph.ihl*4);
return 0;
}
optlen = iph.ihl * 4 - sizeof(iph);
if (skb_copy_bits(skb, offset+sizeof(struct iphdr), opt, optlen)<0) {
limpk("Packet length %u < IP header %u.\n",
skb->len - offset, iph.ihl * 4);
return 0; return 0;
} }
offset = ntohs(iph->frag_off) & IP_OFFSET; fragoff = (ntohs(iph.frag_off) & IP_OFFSET);
protoh = (void *)iph + iph->ihl * 4; datalen = skb->len - (offset + sizeof(struct iphdr) + optlen);
datalen = length - iph->ihl * 4;
/* CHECK: Embedded fragment. */ /* CHECK: Embedded fragment. */
if (embedded && offset) { if (offset && fragoff) {
limpk("Embedded fragment.\n"); limpk("Embedded fragment.\n");
return 0; return 0;
} }
for (i = sizeof(struct iphdr); i < iph->ihl * 4; ) { for (i = 0; i < optlen; ) {
switch (opt[i]) { switch (opt[i]) {
case 0: case 0:
end_of_options = 1; end_of_options = 1;
...@@ -436,7 +444,7 @@ check_ip(struct iphdr *iph, size_t length, int embedded) ...@@ -436,7 +444,7 @@ check_ip(struct iphdr *iph, size_t length, int embedded)
return 0; return 0;
} }
/* CHECK: options at tail. */ /* CHECK: options at tail. */
else if (i+1 >= iph->ihl * 4) { else if (i+1 >= optlen) {
limpk("IP option %u at tail\n", limpk("IP option %u at tail\n",
opt[i]); opt[i]);
return 0; return 0;
...@@ -448,7 +456,7 @@ check_ip(struct iphdr *iph, size_t length, int embedded) ...@@ -448,7 +456,7 @@ check_ip(struct iphdr *iph, size_t length, int embedded)
return 0; return 0;
} }
/* CHECK: oversize options. */ /* CHECK: oversize options. */
else if (&opt[i] + opt[i+1] > endhdr) { else if (i + opt[i+1] > optlen) {
limpk("IP option %u at %u too long\n", limpk("IP option %u at %u too long\n",
opt[i], i); opt[i], i);
return 0; return 0;
...@@ -461,30 +469,30 @@ check_ip(struct iphdr *iph, size_t length, int embedded) ...@@ -461,30 +469,30 @@ check_ip(struct iphdr *iph, size_t length, int embedded)
/* Fragment checks. */ /* Fragment checks. */
/* CHECK: More fragments, but doesn't fill 8-byte boundary. */ /* CHECK: More fragments, but doesn't fill 8-byte boundary. */
if ((ntohs(iph->frag_off) & IP_MF) if ((ntohs(iph.frag_off) & IP_MF)
&& (ntohs(iph->tot_len) % 8) != 0) { && (ntohs(iph.tot_len) % 8) != 0) {
limpk("Truncated fragment %u long.\n", ntohs(iph->tot_len)); limpk("Truncated fragment %u long.\n", ntohs(iph.tot_len));
return 0; return 0;
} }
/* CHECK: Oversize fragment a-la Ping of Death. */ /* CHECK: Oversize fragment a-la Ping of Death. */
if (offset * 8 + datalen > 65535) { if (fragoff * 8 + datalen > 65535) {
limpk("Oversize fragment to %u.\n", offset * 8); limpk("Oversize fragment to %u.\n", fragoff * 8);
return 0; return 0;
} }
/* CHECK: DF set and offset or MF set. */ /* CHECK: DF set and fragoff or MF set. */
if ((ntohs(iph->frag_off) & IP_DF) if ((ntohs(iph.frag_off) & IP_DF)
&& (offset || (ntohs(iph->frag_off) & IP_MF))) { && (fragoff || (ntohs(iph.frag_off) & IP_MF))) {
limpk("DF set and offset=%u, MF=%u.\n", limpk("DF set and offset=%u, MF=%u.\n",
offset, ntohs(iph->frag_off) & IP_MF); fragoff, ntohs(iph.frag_off) & IP_MF);
return 0; return 0;
} }
/* CHECK: Zero-sized fragments. */ /* CHECK: Zero-sized fragments. */
if ((offset || (ntohs(iph->frag_off) & IP_MF)) if ((fragoff || (ntohs(iph.frag_off) & IP_MF))
&& datalen == 0) { && datalen == 0) {
limpk("Zero size fragment offset=%u\n", offset); limpk("Zero size fragment offset=%u\n", fragoff);
return 0; return 0;
} }
...@@ -500,52 +508,54 @@ check_ip(struct iphdr *iph, size_t length, int embedded) ...@@ -500,52 +508,54 @@ check_ip(struct iphdr *iph, size_t length, int embedded)
here. */ here. */
#define MIN_LIKELY_MTU 128 #define MIN_LIKELY_MTU 128
/* CHECK: Min size of first frag = 128. */ /* CHECK: Min size of first frag = 128. */
if ((ntohs(iph->frag_off) & IP_MF) if ((ntohs(iph.frag_off) & IP_MF)
&& offset == 0 && fragoff == 0
&& ntohs(iph->tot_len) < MIN_LIKELY_MTU) { && ntohs(iph.tot_len) < MIN_LIKELY_MTU) {
limpk("First fragment size %u < %u\n", ntohs(iph->tot_len), limpk("First fragment size %u < %u\n", ntohs(iph.tot_len),
MIN_LIKELY_MTU); MIN_LIKELY_MTU);
return 0; return 0;
} }
/* CHECK: Min offset of frag = 128 - IP hdr len. */ /* CHECK: Min offset of frag = 128 - IP hdr len. */
if (offset && offset * 8 < MIN_LIKELY_MTU - iph->ihl * 4) { if (fragoff && fragoff * 8 < MIN_LIKELY_MTU - iph.ihl * 4) {
limpk("Fragment starts at %u < %u\n", offset * 8, limpk("Fragment starts at %u < %u\n", fragoff * 8,
MIN_LIKELY_MTU - iph->ihl * 4); MIN_LIKELY_MTU - iph.ihl * 4);
return 0; return 0;
} }
/* CHECK: Protocol specification non-zero. */ /* CHECK: Protocol specification non-zero. */
if (iph->protocol == 0) { if (iph.protocol == 0) {
limpk("Zero protocol\n"); limpk("Zero protocol\n");
return 0; return 0;
} }
/* FIXME: This is already checked for in "Oversize fragment"
above --RR */
/* CHECK: Do not use what is unused. /* CHECK: Do not use what is unused.
* First bit of fragmentation flags should be unused. * First bit of fragmentation flags should be unused.
* May be used by OS fingerprinting tools. * May be used by OS fingerprinting tools.
* 04 Jun 2002, Maciej Soltysiak, solt@dns.toxicfilms.tv * 04 Jun 2002, Maciej Soltysiak, solt@dns.toxicfilms.tv
*/ */
if (ntohs(iph->frag_off)>>15) { if (ntohs(iph.frag_off)>>15) {
limpk("IP unused bit set\n"); limpk("IP unused bit set\n");
return 0; return 0;
} }
/* Per-protocol checks. */ /* Per-protocol checks. */
switch (iph->protocol) { switch (iph.protocol) {
case IPPROTO_ICMP: case IPPROTO_ICMP:
return check_icmp(protoh, datalen, offset, return check_icmp(skb, offset + iph.ihl*4, fragoff,
(ntohs(iph->frag_off) & IP_MF), (ntohs(iph.frag_off) & IP_MF),
embedded); embedded);
case IPPROTO_UDP: case IPPROTO_UDP:
return check_udp(iph, protoh, datalen, offset, return check_udp(skb, offset + iph.ihl*4, fragoff,
(ntohs(iph->frag_off) & IP_MF), (ntohs(iph.frag_off) & IP_MF),
embedded); embedded);
case IPPROTO_TCP: case IPPROTO_TCP:
return check_tcp(iph, protoh, datalen, offset, return check_tcp(skb, offset + iph.ihl*4, fragoff,
(ntohs(iph->frag_off) & IP_MF), (ntohs(iph.frag_off) & IP_MF),
embedded); embedded);
default: default:
/* Ignorance is bliss. */ /* Ignorance is bliss. */
...@@ -559,11 +569,9 @@ match(const struct sk_buff *skb, ...@@ -559,11 +569,9 @@ match(const struct sk_buff *skb,
const struct net_device *out, const struct net_device *out,
const void *matchinfo, const void *matchinfo,
int offset, int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop) int *hotdrop)
{ {
return !check_ip(skb->nh.iph, skb->len, 0); return !check_ip(skb, 0);
} }
/* Called when user tries to insert an entry of this type. */ /* Called when user tries to insert an entry of this type. */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment