Consolidate TCP option parsing
Some checks are pending
nmap multiplatform autobuilds / build (arm64, gcc, ubuntu-latest-gcc-arm64, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (clang, freebsd-15-clang, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (clang, macos-15-clang, macos-15) (push) Waiting to run
nmap multiplatform autobuilds / build (clang, macos-26-clang, macos-26) (push) Waiting to run
nmap multiplatform autobuilds / build (clang, netbsd-10-clang, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (clang, openbsd-7-clang, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (clang, solaris-11-clang, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (clang, ubuntu-latest-clang, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (egcc, openbsd-7-gcc, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (gcc, freebsd-15-gcc, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (gcc, netbsd-10-gcc, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (gcc, solaris-11-gcc, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (gcc, ubuntu-latest-gcc, ubuntu-latest) (push) Waiting to run
nmap multiplatform autobuilds / build (msvc, windows-latest-msvc, windows-latest) (push) Waiting to run

This commit is contained in:
dmiller 2026-06-24 14:47:12 +00:00
parent 486fa2ebdd
commit c86a45ef6e
6 changed files with 367 additions and 294 deletions

View file

@ -813,6 +813,27 @@ const u8 *TCPHeader::getOptions(size_t *optslen) const {
return this->h.options;
} /* End of getOptions() */
struct tcpopt_atindex_ctx {
unsigned int index;
unsigned int found;
nping_tcp_opt_t result;
tcpopt_atindex_ctx() : index(0), found(0) {
memset(&result, 0, sizeof(result));
}
};
static bool tcpopt_atindex(u8 op, u8 oplen, const u8 *data, void *ctx)
{
tcpopt_atindex_ctx *args = static_cast<tcpopt_atindex_ctx *>(ctx);
if (args->index == args->found) {
args->result.type = op;
args->result.len = oplen;
args->result.value = data + 2;
return false;
}
args->found += 1;
return true;
}
/* Returns the index-th option in the TCP header. On success it returns a
* structure filled with option information. If there is no index-th option,
@ -824,61 +845,13 @@ const u8 *TCPHeader::getOptions(size_t *optslen) const {
* would be set to zero and the "value" field should NOT be accessed, as it
* will not contain reliable information. */
nping_tcp_opt_t TCPHeader::getOption(unsigned int index) const {
nping_tcp_opt_t *curr_opt=NULL;
u8 *curr_pnt=(u8 *)this->h.options;
int bytes_left=this->length - TCP_HEADER_LEN;
assert((this->length - TCP_HEADER_LEN) == this->tcpoptlen);
unsigned int optsfound=0;
nping_tcp_opt_t result;
memset(&result, 0, sizeof(nping_tcp_opt_t));
while(bytes_left>0){
/* Use the opts structure as a template to access current option. It is
* OK to use it because we only access the first two elements. */
curr_opt=(nping_tcp_opt_t *)curr_pnt;
/* If we are right in the option that the caller wants, just return it */
if(optsfound==index){
result.type=curr_opt->type;
if(result.type==TCPOPT_EOL || result.type==TCPOPT_NOOP)
result.len=1;
else
result.len=curr_opt->len;
result.value=(u8 *)curr_pnt+2;
return result;
}
/* Otherwise, we have to parse it, so we can skip it and access the next
* option */
switch(curr_opt->type){
/* EOL or NOOP
+-+-+-+-+-+-+-+-+
| X |
+-+-+-+-+-+-+-+-+ */
case TCPOPT_EOL:
goto out;
case TCPOPT_NOOP:
curr_pnt++; /* Skip one octet */
bytes_left--;
break;
/* TLV encoded option */
default:
/* If we don't have as many octets as the option advertises, the
* option is bogus. Return failure. */
if(bytes_left<curr_opt->len)
return result;
curr_pnt+=curr_opt->len;
bytes_left-=curr_opt->len;
break;
}
optsfound++;
TCPOptions opts;
tcpopt_atindex_ctx ctx;
if (opts.fromTCPHeader(*this)) {
ctx.index = index;
opts.foreachOpt(tcpopt_atindex, &ctx);
}
out:
return result;
return ctx.result;
}
@ -935,3 +908,71 @@ const char *TCPHeader::optcode2str(u8 optcode){
} /* End of optcode2str() */
bool TCPOptions::fromTCPPacket(const u8 *tcppkt, int tcplen)
{
tcpopts = NULL;
optslen = 0;
if (tcplen < TCP_HEADER_LEN)
return false;
u8 data_offset = tcppkt[12] >> 4;
if (data_offset < 5)
return false;
tcpopts = tcppkt + TCP_HEADER_LEN;
optslen = MIN(4 * data_offset, tcplen) - TCP_HEADER_LEN;
if (optslen == 0)
tcpopts = NULL;
return true;
}
bool TCPOptions::fromBuffer(const u8 *tcpoptions, int optionslen)
{
if (!tcpoptions || optionslen <= 0)
return false;
tcpopts = tcpoptions;
optslen = optionslen;
return true;
}
bool TCPOptions::fromTCPHeader(const TCPHeader &T)
{
size_t len = 0;
tcpopts = T.getOptions(&len);
if (len > INT_MAX) {
tcpopts = NULL;
return false;
}
optslen = len;
return tcpopts != NULL;
}
bool TCPOptions::foreachOpt(tcpopt_callback cb, void *ctx) const
{
const u8 *p = tcpopts;
int len = optslen;
while (len > 0) {
int op = p[0];
int oplen = 1;
switch (op) {
case 0: /* TCPOPT_EOL */
case 1: /* TCPOPT_NOP */
break;
default: /* TLV option */
if (len < 2)
return false;
oplen = p[1];
if (oplen < 2)
return false; /* No infinite loops, please */
if (oplen > len)
return false; /* Not enough space */
break;
}
if (!cb(op, oplen, p, ctx))
return true;
len -= oplen;
p += oplen;
}
return len == 0;
}

View file

@ -123,7 +123,7 @@
struct nping_tcp_opt {
u8 type; /* Option type code. */
u8 len; /* Option length. */
u8 *value; /* Option value */
const u8 *value; /* Option value */
}__attribute__((__packed__));
typedef struct nping_tcp_opt nping_tcp_opt_t;
@ -257,4 +257,26 @@ class TCPHeader : public TransportLayerElement {
}; /* End of class TCPHeader */
/* This callback will be called for each option in the buffer.
* Return true to continue processing options.
* Return false to stop processing options. */
typedef bool (*tcpopt_callback)(u8 op, u8 oplen, const u8 *data, void *ctx);
class TCPOptions {
public:
/* Note: this class parses in-place and does not make a copy of the data. The
* data pointed to by tcppkt must remain allocated as long as the TCPOptions
* object is in use. */
TCPOptions() : tcpopts(NULL), optslen(0) {}
bool fromTCPPacket(const u8 *tcppkt, int tcplen);
bool fromBuffer(const u8 *tcpoptions, int optionslen);
bool fromTCPHeader(const TCPHeader &T);
/* Returns true if no errors were encountered, even if the callback returns false. */
bool foreachOpt(tcpopt_callback cb, void *ctx) const;
private:
const u8 *tcpopts;
int optslen;
};
#endif /* __TCPHEADER_H__ */

View file

@ -120,6 +120,7 @@ typedef unsigned __int8 u_int8_t;
#endif
#include "netutil.h"
#include "TCPHeader.h"
#if HAVE_NET_IF_H
#ifndef NET_IF_H /* This guarding is needed for at least some versions of OpenBSD */
@ -1392,6 +1393,42 @@ const char *proto2ascii_uppercase(u8 proto) {
return proto2ascii_case(proto, 1);
}
struct tcpopt_info_ctx {
char *p;
char *end;
bool valid;
tcpopt_info_ctx() : p(NULL), end(NULL), valid(true) {}
bool check_length(size_t len) const {
return (end - p) >= len;
}
void put_str(const char *str) {
if (p >= end)
return;
int r = Snprintf(p, end - p, "%s", str);
if (r < 0)
p = end;
else {
p += r;
if (p > end)
p = end;
}
}
void fmt_num(const char *fmt, unsigned int n) {
if (p >= end)
return;
int r = Snprintf(p, end - p, fmt, n);
if (r < 0)
p = end;
else {
p += r;
if (p > end)
p = end;
}
}
};
static bool tcpopt_info(u8 opcode, u8 len, const u8 *data, void *ctx);
/* Get an ASCII information about a tcp option which is pointed by
optp, with a length of len. The result is stored in the result
buffer. The result may look like "<mss 1452,sackOK,timestamp
@ -1399,130 +1436,93 @@ const char *proto2ascii_uppercase(u8 proto) {
void tcppacketoptinfo(u8 *optp, int len, char *result, int bufsize) {
assert(optp);
assert(result);
char *p, ch;
u8 *q;
int opcode;
u16 tmpshort;
u32 tmpword1, tmpword2;
unsigned int i=0;
assert(bufsize > 0);
memset(result, 0, bufsize);
p = result;
*p = '\0';
q = optp;
ch = '<';
TCPOptions opts;
if (bufsize < 2 || !opts.fromBuffer(optp, len))
return;
tcpopt_info_ctx ctx;
ctx.p = result;
ctx.end = result + bufsize - 1;
ctx.put_str("<");
while (len > 0 && bufsize > 2) {
Snprintf(p, bufsize, "%c", ch);
bufsize--;
p++;
opcode = *q++;
if (!opcode) { /* End of List */
Snprintf(p, bufsize, "eol");
bufsize -= strlen(p);
p += strlen(p);
len--;
} else if (opcode == 1) { /* No Op */
Snprintf(p, bufsize, "nop");
bufsize -= strlen(p);
p += strlen(p);
len--;
} else if (opcode == 2) { /* MSS */
if (len < 4)
break; /* MSS has 4 bytes */
q++;
memcpy(&tmpshort, q, 2);
Snprintf(p, bufsize, "mss %hu", (unsigned short) ntohs(tmpshort));
bufsize -= strlen(p);
p += strlen(p);
q += 2;
len -= 4;
} else if (opcode == 3) { /* Window Scale */
if (len < 3)
break; /* Window Scale option has 3 bytes */
q++;
Snprintf(p, bufsize, "wscale %u", *q);
bufsize -= strlen(p);
p += strlen(p);
q++;
len -= 3;
} else if (opcode == 4) { /* SACK permitted */
if (len < 2)
break; /* SACK permitted option has 2 bytes */
Snprintf(p, bufsize, "sackOK");
bufsize -= strlen(p);
p += strlen(p);
q++;
len -= 2;
} else if (opcode == 5) { /* SACK */
unsigned sackoptlen = *q;
if ((unsigned) len < sackoptlen)
break;
/* This would break parsing, so it's best to just give up */
if (sackoptlen < 2)
break;
q++;
if ((sackoptlen - 2) == 0 || ((sackoptlen - 2) % 8 != 0)) {
Snprintf(p, bufsize, "malformed sack");
bufsize -= strlen(p);
p += strlen(p);
} else {
Snprintf(p, bufsize, "sack %d ", (sackoptlen - 2) / 8);
bufsize -= strlen(p);
p += strlen(p);
for (i = 0; i < sackoptlen - 2; i += 8) {
memcpy(&tmpword1, q + i, 4);
memcpy(&tmpword2, q + i + 4, 4);
Snprintf(p, bufsize, "{%u:%u}", tmpword1, tmpword2);
bufsize -= strlen(p);
p += strlen(p);
}
}
q += sackoptlen - 2;
len -= sackoptlen;
} else if (opcode == 8) { /* Timestamp */
if (len < 10)
break; /* Timestamp option has 10 bytes */
q++;
memcpy(&tmpword1, q, 4);
memcpy(&tmpword2, q + 4, 4);
Snprintf(p, bufsize, "timestamp %lu %lu", (unsigned long) ntohl(tmpword1),
(unsigned long) ntohl(tmpword2));
bufsize -= strlen(p);
p += strlen(p);
q += 8;
len -= 10;
}
ch = ',';
}
if (len > 0) {
*result = '\0';
if (!opts.foreachOpt(tcpopt_info, &ctx) || !ctx.valid) {
Snprintf(result, bufsize, "<Invalid TCP options>");
return;
}
Snprintf(p, bufsize, ">");
char *q = ctx.p - 1;
if (*q != ',')
q++;
*q++ = '>';
*q++ = '\0';
return;
}
static bool tcpopt_info(u8 opcode, u8 len, const u8 *data, void *ctx)
{
tcpopt_info_ctx *args = static_cast<tcpopt_info_ctx *>(ctx);
if (!args->check_length(4))
return false;
const u8 *q = data + 2;
switch (opcode) {
case 0: /* End of List */
args->put_str("eol,");
break;
case 1: /* No Op */
args->put_str("nop,");
break;
case 2: /* MSS */
if (len < 4) {
args->valid = false;
break; /* MSS has 4 bytes */
}
args->fmt_num("mss %u,", (q[0] << 8) + q[1]);
case 3: /* Window Scale */
if (len < 3) {
args->valid = false;
break; /* Window Scale option has 3 bytes */
}
args->fmt_num("wscale %u,", q[0]);
break;
case 4: /* SACK permitted */
if (len < 2) {
args->valid = false;
break; /* SACK permitted option has 2 bytes */
}
args->put_str("sackOK,");
break;
case 5: /* SACK */
if (len < 2) {
args->valid = false;
break;
}
if ((len - 2) == 0 || ((len - 2) % 8 != 0)) {
args->put_str("malformed sack,");
} else {
args->fmt_num("sack %u ", (len - 2) / 8);
for (int i = 0; i < len - 2; i += 8) {
args->fmt_num("{%u:", (q[i] << 24) + (q[i+1] << 16) + (q[i+2] << 8) + q[i+3]);
args->fmt_num("%u}", (q[i+4] << 24) + (q[i+5] << 16) + (q[i+6] << 8) + q[i+7]);
}
args->put_str(",");
}
break;
case 8: /* Timestamp */
if (len < 10) {
args->valid = false;
break; /* Timestamp option has 10 bytes */
}
args->fmt_num("timestamp %u ", (q[0] << 24) + (q[1] << 16) + (q[2] << 8) + q[3]);
args->fmt_num("%u,", (q[4] << 24) + (q[5] << 16) + (q[6] << 8) + q[7]);
break;
default:
break;
}
return args->valid;
}
/* A trivial function used with qsort to sort the routes by netmask and metric */